bug #24435 [Form] Make sure errors are a part of the label on bootstrap 4 - this is a requirement for WCAG2 (Nyholm)
This PR was merged into the 3.4 branch.
Discussion
----------
[Form] Make sure errors are a part of the label on bootstrap 4 - this is a requirement for WCAG2
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets |
| License | MIT
| Doc PR | symfony/symfony-docs#... <!--highly recommended for new features-->
I recently let Europe's leading accessibility experts (Funkanu.se) review a site of mine, they gave me (among other) the feedback that errors should be a part of the label.
They said that it makes no sense for blind users to read label, read input and then read errors.
I know the implementation might look strange. But I wish something like this would be merged. That would be great for accessibility for all apps using Symfony.
We *could* also make sure it prints something like:
```
<label for=”name”>Name: <span class=”hidden”>Error message</span></label>
<input id=”name” type=”text”>
<span aria-hidden=”true”>Error message</span>
```
Commits
-------
a0b40f5
[Form] Make sure errors are a part of the label on bootstrap 4 - this is a requirement for WCAG2
This commit is contained in:
commit
cd56299c14
@ -28,7 +28,6 @@ col-sm-2
|
|||||||
{{- form_label(form) -}}
|
{{- form_label(form) -}}
|
||||||
<div class="{{ block('form_group_class') }}">
|
<div class="{{ block('form_group_class') }}">
|
||||||
{{- form_widget(form) -}}
|
{{- form_widget(form) -}}
|
||||||
{{- form_errors(form) -}}
|
|
||||||
</div>
|
</div>
|
||||||
{##}</div>
|
{##}</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@ -40,7 +39,6 @@ col-sm-2
|
|||||||
{{- form_label(form) -}}
|
{{- form_label(form) -}}
|
||||||
<div class="{{ block('form_group_class') }}">
|
<div class="{{ block('form_group_class') }}">
|
||||||
{{- form_widget(form) -}}
|
{{- form_widget(form) -}}
|
||||||
{{- form_errors(form) -}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{##}</fieldset>
|
{##}</fieldset>
|
||||||
|
@ -39,7 +39,41 @@
|
|||||||
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
|
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
|
||||||
{% set valid = true %}
|
{% set valid = true %}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{- parent() -}}
|
|
||||||
|
{%- if widget == 'single_text' -%}
|
||||||
|
{{- block('form_widget_simple') -}}
|
||||||
|
{%- else -%}
|
||||||
|
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
|
||||||
|
<div {{ block('widget_container_attributes') }}>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table {{ table_class|default('table-bordered table-condensed table-striped') }}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{%- if with_years %}<th>{{ form_label(form.years) }}</th>{% endif -%}
|
||||||
|
{%- if with_months %}<th>{{ form_label(form.months) }}</th>{% endif -%}
|
||||||
|
{%- if with_weeks %}<th>{{ form_label(form.weeks) }}</th>{% endif -%}
|
||||||
|
{%- if with_days %}<th>{{ form_label(form.days) }}</th>{% endif -%}
|
||||||
|
{%- if with_hours %}<th>{{ form_label(form.hours) }}</th>{% endif -%}
|
||||||
|
{%- if with_minutes %}<th>{{ form_label(form.minutes) }}</th>{% endif -%}
|
||||||
|
{%- if with_seconds %}<th>{{ form_label(form.seconds) }}</th>{% endif -%}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{%- if with_years %}<td>{{ form_widget(form.years) }}</td>{% endif -%}
|
||||||
|
{%- if with_months %}<td>{{ form_widget(form.months) }}</td>{% endif -%}
|
||||||
|
{%- if with_weeks %}<td>{{ form_widget(form.weeks) }}</td>{% endif -%}
|
||||||
|
{%- if with_days %}<td>{{ form_widget(form.days) }}</td>{% endif -%}
|
||||||
|
{%- if with_hours %}<td>{{ form_widget(form.hours) }}</td>{% endif -%}
|
||||||
|
{%- if with_minutes %}<td>{{ form_widget(form.minutes) }}</td>{% endif -%}
|
||||||
|
{%- if with_seconds %}<td>{{ form_widget(form.seconds) }}</td>{% endif -%}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
{%- endblock dateinterval_widget %}
|
{%- endblock dateinterval_widget %}
|
||||||
|
|
||||||
{% block percent_widget -%}
|
{% block percent_widget -%}
|
||||||
@ -125,13 +159,28 @@
|
|||||||
{# Labels #}
|
{# Labels #}
|
||||||
|
|
||||||
{% block form_label -%}
|
{% block form_label -%}
|
||||||
|
{% if label is not same as(false) -%}
|
||||||
{%- if compound is defined and compound -%}
|
{%- if compound is defined and compound -%}
|
||||||
{%- set element = 'legend' -%}
|
{%- set element = 'legend' -%}
|
||||||
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%}
|
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-control-label')|trim}) -%}
|
{%- set label_attr = label_attr|merge({for: id, class: (label_attr.class|default('') ~ ' form-control-label')|trim}) -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{% if required -%}
|
||||||
|
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) %}
|
||||||
|
{%- endif -%}
|
||||||
|
{% if label is empty -%}
|
||||||
|
{%- if label_format is not empty -%}
|
||||||
|
{% set label = label_format|replace({
|
||||||
|
'%name%': name,
|
||||||
|
'%id%': id,
|
||||||
|
}) %}
|
||||||
|
{%- else -%}
|
||||||
|
{% set label = name|humanize %}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
<{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }}{% block form_label_errors %}{{- form_errors(form) -}}{% endblock form_label_errors %}</{{ element|default('label') }}>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{- parent() -}}
|
|
||||||
{%- endblock form_label %}
|
{%- endblock form_label %}
|
||||||
|
|
||||||
{% block checkbox_radio_label -%}
|
{% block checkbox_radio_label -%}
|
||||||
@ -169,7 +218,6 @@
|
|||||||
<{{ element|default('div') }} class="form-group">
|
<{{ element|default('div') }} class="form-group">
|
||||||
{{- form_label(form) -}}
|
{{- form_label(form) -}}
|
||||||
{{- form_widget(form) -}}
|
{{- form_widget(form) -}}
|
||||||
{{- form_errors(form) -}}
|
|
||||||
</{{ element|default('div') }}>
|
</{{ element|default('div') }}>
|
||||||
{%- endblock form_row %}
|
{%- endblock form_row %}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Form\Tests;
|
namespace Symfony\Component\Form\Tests;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class providing test cases for the Bootstrap 4 horizontal Twig form theme.
|
* Abstract class providing test cases for the Bootstrap 4 horizontal Twig form theme.
|
||||||
*
|
*
|
||||||
@ -18,6 +20,30 @@ namespace Symfony\Component\Form\Tests;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractBootstrap4HorizontalLayoutTest extends AbstractBootstrap4LayoutTest
|
abstract class AbstractBootstrap4HorizontalLayoutTest extends AbstractBootstrap4LayoutTest
|
||||||
{
|
{
|
||||||
|
public function testRow()
|
||||||
|
{
|
||||||
|
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType');
|
||||||
|
$form->addError(new FormError('[trans]Error![/trans]'));
|
||||||
|
$view = $form->createView();
|
||||||
|
$html = $this->renderRow($view);
|
||||||
|
|
||||||
|
$this->assertMatchesXpath($html,
|
||||||
|
'/div
|
||||||
|
[
|
||||||
|
./label[@for="name"]
|
||||||
|
[
|
||||||
|
./div[
|
||||||
|
./ul
|
||||||
|
[./li[.="[trans]Error![/trans]"]]
|
||||||
|
[count(./li)=1]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
/following-sibling::div[./input[@id="name"]]
|
||||||
|
]
|
||||||
|
'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testLabelOnForm()
|
public function testLabelOnForm()
|
||||||
{
|
{
|
||||||
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType');
|
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType');
|
||||||
|
@ -20,6 +20,30 @@ use Symfony\Component\Form\FormError;
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractBootstrap4LayoutTest extends AbstractBootstrap3LayoutTest
|
abstract class AbstractBootstrap4LayoutTest extends AbstractBootstrap3LayoutTest
|
||||||
{
|
{
|
||||||
|
public function testRow()
|
||||||
|
{
|
||||||
|
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType');
|
||||||
|
$form->addError(new FormError('[trans]Error![/trans]'));
|
||||||
|
$view = $form->createView();
|
||||||
|
$html = $this->renderRow($view);
|
||||||
|
|
||||||
|
$this->assertMatchesXpath($html,
|
||||||
|
'/div
|
||||||
|
[
|
||||||
|
./label[@for="name"]
|
||||||
|
[
|
||||||
|
./div[
|
||||||
|
./ul
|
||||||
|
[./li[.="[trans]Error![/trans]"]]
|
||||||
|
[count(./li)=1]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
/following-sibling::input[@id="name"]
|
||||||
|
]
|
||||||
|
'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testLabelOnForm()
|
public function testLabelOnForm()
|
||||||
{
|
{
|
||||||
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType');
|
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType');
|
||||||
|
Reference in New Issue
Block a user