Skip to content

Commit

Permalink
Several fixes in templates and code
Browse files Browse the repository at this point in the history
- Simplify form_field_snippets templates by setting 'aria-describedby' in dsfr_input_class_attr filter
- Don't use 'field.label_tag' when specifying <label> or <legend>
- Add support inline checkboxes and radio buttons
- Add ability to inline fields with new 'dsfr_inline' filter
  • Loading branch information
christophehenry committed Dec 12, 2024
1 parent 48d5f81 commit 5c7efae
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 52 deletions.
8 changes: 7 additions & 1 deletion dsfr/enums.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from django import VERSION
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.version import PY311

if VERSION >= (4, 0):
from django.utils.version import PY311
else:
import sys

PY311 = sys.version_info >= (3, 11)

if PY311:
from enum import property as enum_property
Expand Down
10 changes: 2 additions & 8 deletions dsfr/templates/dsfr/form_field_snippets/checkbox_snippet.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
{% load widget_tweaks dsfr_tags %}
<div class="fr-checkbox-group{% if field.errors %} fr-checkbox-group--error{% endif %}{% if field.field.disabled %} fr-input-group--disabled{% endif %} fr-mb-2w">
{% if field.errors %}
{% with aria_describedby="aria-describedby:"|add:field.auto_id|add:"-desc-error" %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox"|attr:aria_describedby }}
{% endwith %}
{% else %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox" }}
{% endif %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox" }}
<label for="{{ field.id_for_label }}" class="fr-label">
{{ field.label_tag }}
{{ field.label }}
{% if field.help_text %}
<span class="fr-hint-text">{{ field.help_text|safe }}</span>
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
{% load widget_tweaks dsfr_tags %}
<fieldset class="fr-fieldset{% if field.errors %} fr-fieldset--error{% endif %}"
<fieldset class="fr-fieldset{% if field.errors %} fr-fieldset--error{% endif %} fr-fieldsets"
id="checkboxes-{{ field.auto_id }}"
aria-labelledby="{{ field.auto_id }}-legend{% if field.errors %} {{ field.auto_id }}-messages{% endif %}">
<legend class="fr-fieldset__legend fr-text--regular"
<legend class="fr-fieldset__legend fr-fieldset__legend--regular"
id="{{ field.auto_id }}-legend">
{{ field.label_tag }}
{{ field.label }}
{% if field.field.required %}
*
{% endif %}
{% if field.help_text %}
<span class="fr-hint-text">{{ field.help_text|safe }}</span>
{% endif %}
</legend>
<div class="fr-fieldset__content">
<div class="fr-checkbox-group{% if field.errors %} fr-checkbox-group--error{% endif %}{% if field.field.disabled %} fr-input-group--disabled{% endif %}">
{% if field.errors %}
{% with aria_describedby="aria-describedby:"|add:field.auto_id|add:"-desc-error" %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox"|attr:aria_describedby }}
{% endwith %}
{% else %}
{{ field|dsfr_input_class_attr|attr:"type:checkbox" }}
{% endif %}
{% if field.errors %}
<div id="{{ field.auto_id }}-desc-error">
{{ field.errors }}
</div>
{% endif %}
{% with field=field|dsfr_input_class_attr %}
{% for subwidget in field.subwidgets %}
<div class="fr-fieldset__element{% if field.field.widget.inline %} fr-fieldset__element--inline{% endif %}">
{{ subwidget }}
</div>
{% endfor %}
{% endwith %}
{% if field.errors %}
<div id="{{ field.auto_id }}-desc-error">
{{ field.errors }}
</div>
</div>
{% endif %}
</fieldset>
8 changes: 1 addition & 7 deletions dsfr/templates/dsfr/form_field_snippets/input_snippet.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@
{% endif %}
{# djlint:on #}
</label>
{% if field.errors %}
{% with aria_describedby="aria-describedby:"|add:field.auto_id|add:"-desc-error" %}
{{ field|dsfr_input_class_attr|attr:"aria-invalid:true"|attr:aria_describedby }}
{% endwith %}
{% else %}
{{ field|dsfr_input_class_attr }}
{% endif %}
{{ field|dsfr_input_class_attr }}
{% if field.errors %}
<div id="{{ field.auto_id }}-desc-error">
{{ field.errors }}
Expand Down
24 changes: 11 additions & 13 deletions dsfr/templates/dsfr/form_field_snippets/radioselect_snippet.html
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
{% load widget_tweaks dsfr_tags %}
<fieldset class="fr-fieldset{% if field.errors %} fr-fieldset--error{% endif %}"
id="radio-{{ field.auto_id }}"
aria-labelledby="{{ field.auto_id }}-legend{% if field.errors %} {{ field.auto_id }}-messages{% endif %}">
<legend class="fr-fieldset__legend fr-text--regular"
aria-labelledby="{{ field.auto_id }}-legend{% if field.errors %} {{ field.auto_id }}-desc-error{% endif %}">
<legend class="fr-fieldset__legend fr-fieldset__legend--regular"
id="{{ field.auto_id }}-legend">
{{ field.label_tag }}
{{ field.label }}
{% if field.field.required %}
*
{% endif %}
{% if field.help_text %}
<span class="fr-hint-text">{{ field.help_text|safe }}</span>
{% endif %}
</legend>
<div class="fr-fieldset__content">
<div class="fr-radio-group">
{% if field.field.disabled %}
{{ field|dsfr_input_class_attr|attr:"type:radio"|attr:"disabled" }}
{% else %}
{{ field|dsfr_input_class_attr|attr:"type:radio" }}
{% endif %}
</div>
</div>
{% with field=field|dsfr_input_class_attr %}
{% for subwidget in field.subwidgets %}
<div class="fr-fieldset__element{% if field.field.widget.inline %} fr-fieldset__element--inline{% endif %}">
{{ subwidget }}
</div>
{% endfor %}
{% endwith %}
{% if field.errors %}
<div class="fr-messages-group" id="{{ field.auto_id }}-messages">
<div class="fr-messages-group" id="{{ field.auto_id }}-desc-error">
{{ field.errors }}
</div>
{% endif %}
Expand Down
22 changes: 22 additions & 0 deletions dsfr/templatetags/dsfr_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,12 @@ def dsfr_form_field(field) -> dict:
**Usage**:
`{% dsfr_form_field field %}`
You can use this in combination with filter `dsfr_inline` to inline checkboxes,
radio buttons and rich radio buttons.
**Usage**:
`{% dsfr_form_field field|dsfr_inline %}`
"""
return {"field": field}

Expand Down Expand Up @@ -1691,3 +1697,19 @@ def get_context_data(self, **kwargs):
args = (str(args),)

return format_html(format_string, *args, **kwargs)


@register.filter
def dsfr_inline(field):
"""
Sets field.widget.inline to `True` to inline form field in combination with
{% dsfr_form_field %}
**Usage**:
```django
{% dsfr_form_field form.is_aidant|dsfr_inline %}
```
"""
field.field.widget.inline = True
return field
9 changes: 8 additions & 1 deletion dsfr/test/test_enums.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
from enum import auto
from unittest import skipIf

from django import VERSION
from django.db.models import IntegerChoices
from django.test import SimpleTestCase
from django.utils.safestring import mark_safe
from django.utils.version import PY311

from dsfr.enums import ExtendedChoices

if VERSION >= (4, 0):
from django.utils.version import PY311
else:
import sys

PY311 = sys.version_info >= (3, 11)

if PY311:
from enum import property as enum_property, nonmember
else:
Expand Down
10 changes: 9 additions & 1 deletion dsfr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ def generate_summary_items(sections_names: list) -> list:


def dsfr_input_class_attr(bf: BoundField):
if not bf.is_hidden and "class" not in bf.field.widget.attrs:
if bf.is_hidden:
return bf
if "class" not in bf.field.widget.attrs:
bf.field.label_suffix = ""
if isinstance(bf.field.widget, (widgets.Select, widgets.SelectMultiple)):
bf.field.widget.attrs["class"] = "fr-select"
Expand All @@ -130,6 +132,12 @@ def dsfr_input_class_attr(bf: BoundField):
),
):
bf.field.widget.attrs["class"] = "fr-input"

if bf.errors:
bf.field.widget.attrs.update(
{"aria-invalid": "true", "aria-describedby": f"{bf.auto_id}-desc-error"}
)

return bf


Expand Down
21 changes: 21 additions & 0 deletions example_app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Field

from dsfr.templatetags.dsfr_tags import dsfr_inline
from dsfr.utils import lazy_static
from dsfr.widgets import RichRadioSelect
from example_app.models import Author, Book
Expand Down Expand Up @@ -110,6 +111,13 @@ class ExampleForm(DsfrBaseForm):
widget=forms.RadioSelect,
)

sample_radio_inline = forms.ChoiceField(
label="Boutons radio inline",
required=False,
choices=[(1, "Premier choix"), (2, "Second choix"), (3, "Troisième choix")],
widget=forms.RadioSelect,
)

sample_checkbox = forms.MultipleChoiceField(
label="Cases à cocher",
required=False,
Expand All @@ -122,6 +130,17 @@ class ExampleForm(DsfrBaseForm):
widget=forms.CheckboxSelectMultiple,
)

sample_checkbox_inline = forms.MultipleChoiceField(
label="Cases à cocher",
required=False,
choices=[
("1", "Premier choix"),
("2", "Second choix"),
("3", "Troisième choix"),
],
widget=forms.CheckboxSelectMultiple,
)

sample_rich_radio = forms.ChoiceField(
label="Cases à cocher",
required=False,
Expand Down Expand Up @@ -176,6 +195,8 @@ def clean_sample_checkbox(self):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_autofocus_on_first_error()
dsfr_inline(self["sample_radio_inline"])
dsfr_inline(self["sample_checkbox_inline"])


class AuthorCreateForm(ModelForm, DsfrBaseForm):
Expand Down
6 changes: 3 additions & 3 deletions example_app/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ def test_form_sets_autofocus_on_first_error(self):
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(
response,
"""<input type="number" name="sample_number" value="-5" class="fr-input"
autofocus="" aria-describedby="id_sample_number-desc-error" aria-invalid="true"
required id="id_sample_number">""",
"""<input type="number" name="sample_number" value="-5"
aria-invalid="true" aria-describedby="id_sample_number-desc-error" class="fr-input"
autofocus="" required id="id_sample_number">""",
html=True,
)
self.assertContains(response, "Merci d’entrer un nombre positif", html=True)

0 comments on commit 5c7efae

Please sign in to comment.