Skip to content

Commit 871b5b2

Browse files
committed
feature #60902 [TwigBridgeRessources] add aria-invalid and aria-describedby on form inputs when validation failure exist (jeanfrancois-morin)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [TwigBridgeRessources] add aria-invalid and aria-describedby on form inputs when validation failure exist | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT ## Context and objective This PR aims to add the appropriate ARIA attributes (`aria-invalid` and `aria-describedby`) to form fields when validation errors are present. The proposal is based on the recommendations of [WCAG 2.1](https://www.w3.org/TR/WCAG21/#error-identification) and specifically the technique [ARIA21](https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA21), which describes the combined use of `aria-invalid="true"` and `aria-describedby="<TEXT_ERROR_ID>"` AFTER user input validation. It seems particularly relevant to implement this since SymfonyUX's LiveComponent allows for an auto-validation system, enabling real-time error feedback to users as they fill out the form. Concretely, users of screen readers will be informed of the presence of errors as soon as they focus on the form field, and if an explicit error message is associated, they can access it immediately. Note that the `aria-describedby` attribute can point to one or more HTML elements, allowing multiple messages (for example, a help message and an error message) to be vocalized. This PR takes this possibility into account by associating each field with the text passages `_field_id_help_` and `_field_id_errors_` when applicable. ## Twig Side The proposal is integrated into all form themes through the main template `form_div_layout.html.twig`, except for the files `bootstrap_4_layout.html.twig` and `bootstrap_4_horizontal_layout.html.twig`. In these files, errors were initially placed inside the associated label, so it is not necessary to add the `aria-describedby` attribute. Errors will be vocalized automatically and immediately after the label content. The `aria-invalid="true"` attribute remains necessary to indicate the presence of errors. Let me know if this approach works for you, or if you would like me to modify any aspects. Commits ------- 6957eba [TwigBridgeRessources] add aria-invalid and aria-describedby on form inputs when validation failure exist
2 parents 3505a04 + 6957eba commit 871b5b2

11 files changed

+48
-49
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `access_decision()` and `access_decision_for_user()` Twig functions
88
* Call `form_label_content` inside `button_widget` block to render button label
9+
* Add `aria-invalid` and `aria-describedby` attributes to form fields when validation errors are present
910

1011
7.3
1112
---

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ col-sm-2
2222

2323
{# Rows #}
2424

25-
{% block form_row -%}
26-
{%- set widget_attr = {} -%}
27-
{%- if help -%}
28-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
29-
{%- endif -%}
25+
{% block form_row_render -%}
3026
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ ((not compound or force_error|default(false)) and not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
3127
{{- form_label(form) -}}
3228
<div class="{{ block('form_group_class') }}">
@@ -35,7 +31,7 @@ col-sm-2
3531
{{- form_errors(form) -}}
3632
</div>
3733
{##}</div>
38-
{%- endblock form_row %}
34+
{%- endblock form_row_render -%}
3935

4036
{% block submit_row -%}
4137
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,14 @@
127127

128128
{# Rows #}
129129

130-
{% block form_row -%}
131-
{%- set widget_attr = {} -%}
132-
{%- if help -%}
133-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
134-
{%- endif -%}
130+
{% block form_row_render -%}
135131
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ ((not compound or force_error|default(false)) and not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
136132
{{- form_label(form) }} {# -#}
137133
{{ form_widget(form, widget_attr) }} {# -#}
138134
{{- form_help(form) -}}
139135
{{ form_errors(form) }} {# -#}
140136
</div> {# -#}
141-
{%- endblock form_row %}
137+
{%- endblock form_row_render -%}
142138

143139
{% block button_row -%}
144140
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
@@ -189,7 +185,7 @@
189185
{% if form is not rootform %}<span class="help-block">{% else %}<div class="alert alert-danger">{% endif %}
190186
<ul class="list-unstyled">
191187
{%- for error in errors -%}
192-
<li><span class="glyphicon glyphicon-exclamation-sign"></span> {{ error.message }}</li>
188+
<li id="{{ id ~ '_error' ~ loop.index }}"><span class="glyphicon glyphicon-exclamation-sign"></span> {{ error.message }}</li>
193189
{%- endfor -%}
194190
</ul>
195191
{% if form is not rootform %}</span>{% else %}</div>{% endif %}

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ col-sm-2
2424
{%- if expanded is defined and expanded -%}
2525
{{ block('fieldset_form_row') }}
2626
{%- else -%}
27-
{%- set widget_attr = {} -%}
27+
{%- set attr = {} -%}
2828
{%- if help -%}
29-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
29+
{%- set attr = attr|merge({'aria-describedby': id ~ '_help'}) -%}
30+
{%- endif -%}
31+
{%- if errors|length > 0 -%}
32+
{%- set attr = attr|merge({'aria-invalid': 'true'}) -%}
3033
{%- endif -%}
34+
{%- set widget_attr = {attr: attr} -%}
3135
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group row' ~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
3236
{{- form_label(form) -}}
3337
<div class="{{ block('form_group_class') }}">

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,14 @@
282282
{%- if compound is defined and compound -%}
283283
{%- set element = 'fieldset' -%}
284284
{%- endif -%}
285-
{%- set widget_attr = {} -%}
285+
{%- set attr = {} -%}
286286
{%- if help -%}
287-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
287+
{%- set attr = attr|merge({'aria-describedby': id ~ '_help'}) -%}
288288
{%- endif -%}
289+
{%- if errors|length > 0 -%}
290+
{%- set attr = attr|merge({'aria-invalid': 'true'}) -%}
291+
{%- endif -%}
292+
{%- set widget_attr = {attr: attr} -%}
289293
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
290294
{{- form_label(form) -}}
291295
{{- form_widget(form, widget_attr) -}}

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_horizontal_layout.html.twig

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,10 @@
2323

2424
{# Rows #}
2525

26-
{% block form_row -%}
26+
{% block form_row_render -%}
2727
{%- if expanded is defined and expanded -%}
2828
{{ block('fieldset_form_row') }}
2929
{%- else -%}
30-
{%- set widget_attr = {} -%}
31-
{%- if help -%}
32-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
33-
{%- endif -%}
3430
{%- set row_class = row_class|default(row_attr.class|default('mb-3')) -%}
3531
{%- set is_form_floating = is_form_floating|default('form-floating' in row_class) -%}
3632
{%- set is_input_group = is_input_group|default('input-group' in row_class) -%}
@@ -68,7 +64,7 @@
6864
{%- endif -%}
6965
{##}</div>
7066
{%- endif -%}
71-
{%- endblock form_row %}
67+
{%- endblock form_row_render %}
7268

7369
{% block fieldset_form_row -%}
7470
{%- set widget_attr = {} -%}

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,10 @@
320320

321321
{# Rows #}
322322

323-
{%- block form_row -%}
323+
{%- block form_row_render -%}
324324
{%- if compound is defined and compound -%}
325325
{%- set element = 'fieldset' -%}
326326
{%- endif -%}
327-
{%- set widget_attr = {} -%}
328-
{%- if help -%}
329-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
330-
{%- endif -%}
331327
{%- set row_class = row_class|default(row_attr.class|default('mb-3')|trim) -%}
332328
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: row_class})} %}{{ block('attributes') }}{% endwith %}>
333329
{%- if 'form-floating' in row_class -%}
@@ -340,7 +336,7 @@
340336
{{- form_help(form) -}}
341337
{{- form_errors(form) -}}
342338
</{{ element|default('div') }}>
343-
{%- endblock form_row %}
339+
{%- endblock form_row_render %}
344340

345341
{%- block button_row -%}
346342
<div{% with {attr: row_attr|merge({class: row_attr.class|default('mb-3')|trim})} %}{{ block('attributes') }}{% endwith %}>
@@ -353,7 +349,7 @@
353349
{%- block form_errors -%}
354350
{%- if errors|length > 0 -%}
355351
{%- for error in errors -%}
356-
<div class="{% if form is not rootform %}invalid-feedback{% else %}alert alert-danger{% endif %} d-block">{{ error.message }}</div>
352+
<div class="{% if form is not rootform %}invalid-feedback{% else %}alert alert-danger{% endif %} d-block" id="{{ id ~ '_error' ~ loop.index }}">{{ error.message }}</div>
357353
{%- endfor -%}
358354
{%- endif %}
359355
{%- endblock form_errors %}

src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,17 +344,32 @@
344344
{%- endblock repeated_row -%}
345345

346346
{%- block form_row -%}
347-
{%- set widget_attr = {} -%}
347+
{%- set attr = {} -%}
348+
{%- set aria_describedby = [] -%}
348349
{%- if help -%}
349-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
350+
{%- set aria_describedby = aria_describedby|merge([id ~ '_help']) -%}
350351
{%- endif -%}
352+
{%- if errors|length > 0 -%}
353+
{%- set aria_describedby = aria_describedby|merge(errors|map((_, index) => id ~ '_error' ~ (index + 1))) -%}
354+
{%- endif -%}
355+
{%- if aria_describedby|length > 0 -%}
356+
{%- set attr = attr|merge({'aria-describedby': aria_describedby|join(' ')}) -%}
357+
{%- endif -%}
358+
{%- if errors|length > 0 -%}
359+
{%- set attr = attr|merge({'aria-invalid': 'true'}) -%}
360+
{%- endif -%}
361+
{%- set widget_attr = {attr: attr} -%}
362+
{{- block('form_row_render') -}}
363+
{%- endblock form_row -%}
364+
365+
{%- block form_row_render -%}
351366
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
352367
{{- form_label(form) -}}
353368
{{- form_errors(form) -}}
354369
{{- form_widget(form, widget_attr) -}}
355370
{{- form_help(form) -}}
356371
</div>
357-
{%- endblock form_row -%}
372+
{%- endblock form_row_render -%}
358373

359374
{%- block button_row -%}
360375
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
@@ -399,7 +414,7 @@
399414
{%- if errors|length > 0 -%}
400415
<ul>
401416
{%- for error in errors -%}
402-
<li>{{ error.message }}</li>
417+
<li id="{{ id ~ '_error' ~ loop.index }}">{{ error.message }}</li>
403418
{%- endfor -%}
404419
</ul>
405420
{%- endif -%}

src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
{% use 'form_div_layout.html.twig' %}
22

3-
{%- block form_row -%}
4-
{%- set widget_attr = {} -%}
5-
{%- if help -%}
6-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
7-
{%- endif -%}
3+
{%- block form_row_render -%}
84
<tr{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
95
<td>
106
{{- form_label(form) -}}
@@ -15,7 +11,7 @@
1511
{{- form_help(form) -}}
1612
</td>
1713
</tr>
18-
{%- endblock form_row -%}
14+
{%- endblock form_row_render -%}
1915

2016
{%- block button_row -%}
2117
<tr{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>

src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,7 @@
281281

282282
{# Rows #}
283283

284-
{% block form_row -%}
285-
{%- set widget_attr = {} -%}
286-
{%- if help -%}
287-
{%- set widget_attr = {attr: {'aria-describedby': id ~ '_help'}} -%}
288-
{%- endif -%}
284+
{% block form_row_render -%}
289285
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>
290286
<div class="large-12 columns{% if (not compound or force_error|default(false)) and not valid %} error{% endif %}">
291287
{{- form_label(form) -}}
@@ -294,7 +290,7 @@
294290
{{- form_errors(form) -}}
295291
</div>
296292
</div>
297-
{%- endblock form_row %}
293+
{%- endblock form_row_render %}
298294

299295
{% block choice_row -%}
300296
{% set force_error = true %}
@@ -342,8 +338,7 @@
342338
{% if errors|length > 0 -%}
343339
{% if form is not rootform %}<small class="error">{% else %}<div data-alert class="alert-box alert">{% endif %}
344340
{%- for error in errors -%}
345-
{{ error.message }}
346-
{% if not loop.last %}, {% endif %}
341+
<span id="{{ id ~ '_error' ~ loop.index }}">{{ error.message }}</span>{% if not loop.last %}, {% endif %}
347342
{%- endfor -%}
348343
{% if form is not rootform %}</small>{% else %}</div>{% endif %}
349344
{%- endif %}

0 commit comments

Comments
 (0)