Skip to content

Commit

Permalink
[5.x] Improve form field accessibility (#10993)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
daun and jasonvarga authored Dec 20, 2024
1 parent 0b6e0d2 commit 33ba7cc
Show file tree
Hide file tree
Showing 19 changed files with 178 additions and 86 deletions.
12 changes: 10 additions & 2 deletions resources/views/extend/forms/fields.antlers.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
{{ fields }}
<div class="p-4">
<label>{{ display }}</label>
<label for="{{ id }}">
{{ display }}
{{ if validate | contains:required }}
<sup aria-label="{{ trans:Required }}">*</sup>
{{ /if }}
</label>
<div class="p-2">{{ field }}</div>
{{ if instructions }}
<p class="text-gray-500" id="{{ id }}-instructions">{{ instructions }}</p>
{{ /if }}
{{ if error }}
<p class="text-gray-500">{{ error }}</p>
<p class="text-red-700" id="{{ id }}-error">{{ error }}</p>
{{ /if }}
</div>
{{ /fields }}
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/assets.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<input
id="{{ id }}"
type="file"
name="{{ handle }}{{ if max_files !== 1 }}[]{{ /if }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if max_files !== 1 }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/checkboxes.antlers.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
{{ foreach:options as="option|label" }}
<label>
<input
id="{{ id }}-{{ option | slugify }}-option"
type="checkbox"
name="{{ handle }}[]"
value="{{ option }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value|in_array:option }}checked{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ label !== null ? label : option }}
</label>
Expand Down
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/default.antlers.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<input
id="{{ id }}"
type="{{ input_type ?? 'text' }}"
name="{{ handle }}"
value="{{ value }}"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/dictionary.antlers.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<select
id="{{ id }}"
name="{{ handle }}{{ multiple ?= "[]" }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if multiple }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ unless multiple }}
<option value>
Expand Down
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/files.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<input
id="{{ id }}"
type="file"
name="{{ handle }}{{ if max_files !== 1 }}[]{{ /if }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if max_files !== 1 }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/radio.antlers.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{{ foreach:options as="option|label" }}
<label>
<input
id="{{ id }}-{{ option | slugify }}-option"
type="radio"
name="{{ handle }}"
value="{{ option }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value == option }}checked{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ label !== null ? label : option }}
</label>
Expand Down
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/select.antlers.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<select
id="{{ id }}"
name="{{ handle }}{{ multiple ?= "[]" }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if multiple }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ unless multiple }}
<option value>
Expand Down
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/text.antlers.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<input
id="{{ id }}"
type="{{ input_type ?? 'text' }}"
name="{{ handle }}"
value="{{ value }}"
Expand All @@ -7,4 +8,9 @@
{{ if autocomplete }}autocomplete="{{ autocomplete }}"{{ /if }}
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/textarea.antlers.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<textarea
id="{{ id }}"
name="{{ handle }}"
rows="5"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ value }}
</textarea>
6 changes: 6 additions & 0 deletions resources/views/extend/forms/fields/toggle.antlers.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<label>
<input type="hidden" name="{{ handle }}" value="0">
<input
id="{{ id }}"
type="checkbox"
name="{{ handle }}"
value="1"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value && value !== '0' }}checked{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
{{ if error }}
aria-invalid="true" aria-describedby="{{ id }}-error"
{{ elseif instructions }}
aria-describedby="{{ id }}-instructions"
{{ /if }}
>
{{ if inline_label }}
{{ inline_label }}
Expand Down
10 changes: 10 additions & 0 deletions src/Tags/Concerns/RendersForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate
->map->get('default')
->filter()->all();

$formHandle = $field->form()?->handle() ?? Str::slug($errorBag);
$data = array_merge($configDefaults, $field->toArray(), [
'id' => $this->generateFieldId($field->handle(), $formHandle),
'instructions' => $field->instructions(),
'error' => $errors->first($field->handle()) ?: null,
'default' => $field->value() ?? $field->defaultValue(),
Expand Down Expand Up @@ -174,4 +176,12 @@ protected function minifyFieldHtml($html)

return $html;
}

/**
* Generate a field id to associate input with label.
*/
private function generateFieldId(string $fieldHandle, ?string $formName = null): string
{
return ($formName ?? 'default').'-form-'.$fieldHandle.'-field';
}
}
44 changes: 22 additions & 22 deletions tests/Tags/Form/FormCreateAlpineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ public function it_dynamically_renders_text_field_x_model()
],
];

$this->assertFieldRendersHtml(['<input type="text" name="name" value="" x-model="name">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input type="text" name="name" value="" x-model="my_form.name">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-name-field" type="text" name="name" value="" x-model="name">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-name-field" type="text" name="name" value="" x-model="my_form.name">'], $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand All @@ -311,8 +311,8 @@ public function it_dynamically_renders_textarea_field_x_model()
],
];

$this->assertFieldRendersHtml(['<textarea name="comment" rows="5" x-model="comment"></textarea>'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<textarea name="comment" rows="5" x-model="my_form.comment"></textarea>'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<textarea id="[[form-handle]]-form-comment-field" name="comment" rows="5" x-model="comment"></textarea>'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<textarea id="[[form-handle]]-form-comment-field" name="comment" rows="5" x-model="my_form.comment"></textarea>'], $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand All @@ -330,13 +330,13 @@ public function it_dynamically_renders_checkboxes_field_x_model()
],
];

$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="cat" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="rat" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="armadillo" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-cat-option" type="checkbox" name="fav_animals[]" value="cat" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-rat-option" type="checkbox" name="fav_animals[]" value="rat" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-armadillo-option" type="checkbox" name="fav_animals[]" value="armadillo" x-model="fav_animals">'], $config, [], ['js' => 'alpine']);

$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="cat" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="rat" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input type="checkbox" name="fav_animals[]" value="armadillo" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-cat-option" type="checkbox" name="fav_animals[]" value="cat" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-rat-option" type="checkbox" name="fav_animals[]" value="rat" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animals-field-armadillo-option" type="checkbox" name="fav_animals[]" value="armadillo" x-model="my_form.fav_animals">'], $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand All @@ -354,13 +354,13 @@ public function it_dynamically_renders_radio_field_x_model()
],
];

$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="cat" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="rat" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="armadillo" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-cat-option" type="radio" name="fav_animal" value="cat" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-rat-option" type="radio" name="fav_animal" value="rat" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-armadillo-option" type="radio" name="fav_animal" value="armadillo" x-model="fav_animal">'], $config, [], ['js' => 'alpine']);

$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="cat" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="rat" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input type="radio" name="fav_animal" value="armadillo" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-cat-option" type="radio" name="fav_animal" value="cat" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-rat-option" type="radio" name="fav_animal" value="rat" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml(['<input id="[[form-handle]]-form-fav_animal-field-armadillo-option" type="radio" name="fav_animal" value="armadillo" x-model="my_form.fav_animal">'], $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand All @@ -379,7 +379,7 @@ public function it_dynamically_renders_select_field_x_model()
];

$expected = [
'<select name="favourite_animal" x-model="favourite_animal">',
'<select id="[[form-handle]]-form-favourite_animal-field" name="favourite_animal" x-model="favourite_animal">',
'<option value>Please select...</option>',
'<option value="cat">Cat</option>',
'<option value="armadillo">Armadillo</option>',
Expand All @@ -390,7 +390,7 @@ public function it_dynamically_renders_select_field_x_model()
$this->assertFieldRendersHtml($expected, $config, [], ['js' => 'alpine']);

$expectedScoped = [
'<select name="favourite_animal" x-model="my_form.favourite_animal">',
'<select id="[[form-handle]]-form-favourite_animal-field" name="favourite_animal" x-model="my_form.favourite_animal">',
'<option value>Please select...</option>',
'<option value="cat">Cat</option>',
'<option value="armadillo">Armadillo</option>',
Expand All @@ -413,8 +413,8 @@ public function it_dynamically_renders_asset_field_x_model()
],
];

$this->assertFieldRendersHtml('<input type="file" name="cat_selfie" x-model="cat_selfie">', $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml('<input type="file" name="cat_selfie" x-model="my_form.cat_selfie">', $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml('<input id="[[form-handle]]-form-cat_selfie-field" type="file" name="cat_selfie" x-model="cat_selfie">', $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml('<input id="[[form-handle]]-form-cat_selfie-field" type="file" name="cat_selfie" x-model="my_form.cat_selfie">', $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand All @@ -427,8 +427,8 @@ public function it_dynamically_renders_field_with_fallback_to_default_partial_x_
],
];

$this->assertFieldRendersHtml('<input type="text" name="custom" value="" x-model="custom">', $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml('<input type="text" name="custom" value="" x-model="my_form.custom">', $config, [], ['js' => 'alpine:my_form']);
$this->assertFieldRendersHtml('<input id="[[form-handle]]-form-custom-field" type="text" name="custom" value="" x-model="custom">', $config, [], ['js' => 'alpine']);
$this->assertFieldRendersHtml('<input id="[[form-handle]]-form-custom-field" type="text" name="custom" value="" x-model="my_form.custom">', $config, [], ['js' => 'alpine:my_form']);
}

#[Test]
Expand Down
2 changes: 1 addition & 1 deletion tests/Tags/Form/FormCreateCustomDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function custom_driver_can_add_to_renderable_field_attributes()
EOT
));

$expected = '<input type="email" name="email" value="" z-unless="Statamic.$conditions.showField(\'email\', __zData)" z-gnarley="true" required>';
$expected = '<input id="contact-form-email-field" type="email" name="email" value="" z-unless="Statamic.$conditions.showField(\'email\', __zData)" z-gnarley="true" required>';
$this->assertStringContainsString($expected, $output);
}

Expand Down
Loading

0 comments on commit 33ba7cc

Please sign in to comment.