Skip to content

Commit 09d63a3

Browse files
authored
fix: More a11y issues (#20168)
* fix: Name CheckboxList search input and announce no-results to screen readers * fix: Announce ColorPicker popover and dialog to screen readers * fix: Expose RichEditor toolbar and toggle-button state to screen readers * fix: Give KeyValue inputs and action columns accessible names * fix: Toggle reports correct and always-valid `aria-checked` state * fix: Convey unread database notification state to screen readers * fix: Stop toast live region re-announcing the whole stack * fix: Make database notification header actions keyboard-reachable * fix: Announce danger notifications assertively to screen readers * fix: Mark active panel navigation items with `aria-current="page"` * fix: Add a skip-to-content link to the panel layout * fix: Name collapsed-sidebar icon-only controls for assistive tech * fix: Group the theme switcher and announce the active theme * fix: Include user name in sidebar user-menu trigger accessible name * fix: Announce unread notification count in the notifications trigger name * fix: Label panel navigation landmarks for assistive tech * fix: Mark active item with `aria-current` in dropdown navigation menus * fix: Expose Icon tooltip text as an accessible name * fix: Expose FusedGroup as a labelled group and associate its validation error * fix: Make copyable Text keyboard-operable * fix: Honor prefers-reduced-motion for framework animations * fix: Always render an `alt` attribute on the base avatar image * fix: Expose current page in breadcrumbs and pagination for assistive tech * fix: Keep modal close button focusable when Escape close is disabled * fix: Add `aria-current` to active page sub-navigation tab links * fix: Add visible focus rings to table controls and a boundary to color swatches * fix: Give inline-editable table column controls an accessible name * fix: Make ToggleColumn keyboard-operable and focusable * fix: Add a boundary to infolist color swatches and focus rings to copyable entries * fix: Render hidden infolist entry labels as screen-reader-only, not display:none * fix: Give infolist ColorEntry swatches an accessible name * fix: Mark KeyValueEntry key cells as row headers * fix: Add `scope="col"` to RepeatableEntry table headers * fix: Make TextEntry expand/collapse list controls keyboard-operable buttons * fix: Add a visible focus ring to the clickable stats-overview stat card * fix: Add an accessible name to the chart widget inline filter select * refactor: Bind Blade component attributes with a colon instead of interpolation * fix: Honor prefers-reduced-motion for the notification reposition animation * fix: Bind aria-current on dropdown nav items so the Blade component compiles * fix: Dismiss tooltips when Escape is pressed * fix: Use navigation semantics for page sub-navigation tabs * fix: Name column-manager reorder handles and associate visibility checkbox labels * fix: expose heading level on static section and modal heading components * fix: default an accessible name on the badge delete button * fix: give icon-only infolist entries a screen-reader text alternative * fix: announce callout status severity to screen readers * fix: add `alt()` support to the image entry for accessible names * fix: give non-text table columns a text alternative * fix: make the collapsible section disclosure button accessible * fix: name the collapsible section toggle and correct its disclosure ARIA * fix: expose the schema section as a named landmark region * fix: expose disclosure semantics on dropdown triggers * fix: reword `ColorColumn` swatch comment to drop internal todo reference * fix: expose query builder rules and groups as named groups * fix: swap the Blade section disclosure button `aria-label` between expand and collapse states * fix: sort the `HasLabel` import alphabetically in `IconEntry` * fix: associate wizard step controls with their panels * fix: concatenate the section disclosure `x-bind:aria-label` expression instead of echoing inside the attribute * fix: announce wizard step completion state to screen readers * fix: harden loading-state accessibility * fix: use the sanitized color for swatch accessible names * fix: drop unsupported ARIA attributes from the color picker input * fix: give table summary footer proper header cell scope semantics * fix: emit the schema icon tooltip as visually-hidden text instead of an inert unescaped `aria-label` * fix: expose indeterminate state on select-all checkboxes * fix: provide text alternatives for chart canvas elements * fix: expose disclosure state on sidebar toggle buttons * Update theme.css * fix: improve accessibility of the link component * fix: guard icon-button aria-label and disabled-tooltip keyboard focus * fix: announce dynamic table status changes to screen readers * fix: announce FileUpload errors and name its download and open links * fix: announce TagsInput tag additions and removals to screen readers * fix: associate sidebar group disclosure buttons with their items via aria-controls * fix: emit a valid aria-pressed value on rich editor toggle tools * fix: don't apply an assertive role to inline danger notifications * fix: use the collapsed state for a section's initial collapse-button label * fix: name wizard step panels when the header is hidden * fix: expose a user-supplied icon aria-label as a visually-hidden alternative * fix: make the toggle column switch focusable before Alpine initializes * fix: give the FileUpload image editor image an empty alt * fix: rename `ImageColumn::getImageAlt()` to `getAlt()` for sibling parity * fix: align icon text-alternative type handling between entry and column * fix: make the text column limited-list expand controls real buttons * fix: add header scopes and reorder/actions labels to the table repeater * fix: dismiss only the tooltip on Escape when a dropdown is also open * fix: add a skip-to-content link to the simple layout * fix: give the tabs navigation landmark a default accessible name * fix: mark custom image-path icons as decorative by default * fix: clamp the heading level components to a valid heading tag * fix: don't announce the rich editor toolbar until it is keyboard-navigable * fix: keep text list expand controls as role=button to stay valid inside record links * fix: bind the toggle column tabindex client-side to stay valid inside record links * fix: escape sr-only `aria-label` values rendered without attribute escaping * fix: place plural condition markers at the start of each translation segment * fix: announce the total result count instead of the current page count * fix: withhold `role="img"` from copyable color entry swatches to match the column * fix: key table cell start/end padding off `:first-child`/`:last-child` so summary `<th>` cells do not break `*-of-type` matching * fix: announce the active tab on button-tag tabs with `aria-current="true"` * fix: keep sidebar group item ids unique when labels share or lack a slug * fix: clear stale `TagsInput` live region announcements after a delay * fix: keep dropdown trigger ARIA wiring in sync across Livewire morphs * fix: render the tab-reachable modal close button after the content so the focus trap does not autofocus it * fix: focus the notifications slide-over window on open instead of the `Mark all as read` action * Update theme.css * fix: escape the sr-only `aria-label` in the link Blade component * fix: escape the sr-only `aria-label` in the button Blade component * fix: omit the section collapse button `aria-controls` when there is no content to control * test: cover Blade component sr-only `aria-label` escaping and section `aria-controls` * fix: scope column manager checkbox ids to their table * fix: insert tags literally into `TagsInput` live region announcements * fix: keep the tab-reachable modal close button in the sticky header and autofocus the window instead * test: cover column manager checkbox id scoping across two tables in the browser * fix: escape aria-labels in the embedded HTML generators and reflect toggle state in aria-checked The sr-only aria-label escaping fix applied to the link/button Blade views was not mirrored into the CanGenerate{Button,IconButton,Link}Html traits that Action renders through, so an Htmlable label could break out of the attribute (the merges use escape: false). Escape them the same way the Blade views do, with doubleEncode: false so string labels are not double-encoded, and give the icon-button Blade view the matching treatment. Also render the toggle Blade component's initial aria-checked from its state instead of a hardcoded false, matching the embedded PHP renderers before Alpine hydrates. * fix: keep editable column aria-labels safe for Htmlable labels The editable columns built their aria-label with (string) $this->getLabel(), which crashes on an Htmlable label that has no __toString() and double-encodes a HtmlString's entities. Use the Htmlable-aware conversion the rest of the branch uses, with doubleEncode: false. * fix: guard tags input live region announcements for published view overrides announce() wrote to the liveRegion ref and the code called replace() on the message strings unconditionally, both of which are absent when a user serves a published pre-change tags-input view, throwing on every tag entry. Bail out of announce() when the ref is missing and call replace() optionally. * fix: guard file upload action labels for published view overrides The download/open anchors set aria-label and title from parameters that are undefined when a published pre-change file-upload view is served, rendering a literal "undefined". Only set the attributes when the label is present. * summary bug * remove inconsistent labelling
1 parent 239cb12 commit 09d63a3

139 files changed

Lines changed: 1609 additions & 153 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/forms/dist/components/file-upload.js

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/forms/dist/components/tags-input.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/forms/resources/js/components/file-upload.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default function fileUploadFormComponent({
3535
confirmSvgEditingMessage,
3636
deleteUploadedFileUsing,
3737
disabledSvgEditingMessage,
38+
downloadActionLabel,
3839
getUploadedFilesUsing,
3940
hasCircleCropper,
4041
hasImageEditor,
@@ -63,6 +64,7 @@ export default function fileUploadFormComponent({
6364
maxSize,
6465
mimeTypeMap,
6566
minSize,
67+
openActionLabel,
6668
panelAspectRatio,
6769
panelLayout,
6870
placeholder,
@@ -605,6 +607,13 @@ export default function fileUploadFormComponent({
605607
anchor.href = downloadableUrl
606608
anchor.download = file.file.name
607609

610+
// A published pre-change view override passes no label, so skip the attributes
611+
// instead of rendering a literal "undefined".
612+
if (downloadActionLabel) {
613+
anchor.setAttribute('aria-label', downloadActionLabel)
614+
anchor.setAttribute('title', downloadActionLabel)
615+
}
616+
608617
return anchor
609618
},
610619

@@ -620,6 +629,13 @@ export default function fileUploadFormComponent({
620629
anchor.href = openableUrl
621630
anchor.target = '_blank'
622631

632+
// A published pre-change view override passes no label, so skip the attributes
633+
// instead of rendering a literal "undefined".
634+
if (openActionLabel) {
635+
anchor.setAttribute('aria-label', openActionLabel)
636+
anchor.setAttribute('title', openActionLabel)
637+
}
638+
623639
return anchor
624640
},
625641

packages/forms/resources/js/components/tags-input.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
1-
export default function tagsInputFormComponent({ state, splitKeys }) {
1+
export default function tagsInputFormComponent({
2+
state,
3+
splitKeys,
4+
tagAddedMessage,
5+
tagRemovedMessage,
6+
}) {
27
return {
38
newTag: '',
49

510
state,
611

12+
liveRegionClearTimeout: null,
13+
14+
announce(message) {
15+
// A published pre-change view override has no `liveRegion` ref, so bail out
16+
// instead of throwing and breaking tag entry for those users.
17+
const liveRegion = this.$refs.liveRegion
18+
19+
if (!liveRegion) {
20+
return
21+
}
22+
23+
if (this.liveRegionClearTimeout !== null) {
24+
clearTimeout(this.liveRegionClearTimeout)
25+
}
26+
27+
liveRegion.textContent = message
28+
29+
// Clear the announcement once it has been read, so stale messages do not
30+
// remain reachable by the screen reader virtual cursor.
31+
this.liveRegionClearTimeout = setTimeout(() => {
32+
liveRegion.textContent = ''
33+
34+
this.liveRegionClearTimeout = null
35+
}, 3000)
36+
},
37+
738
createTag() {
839
this.newTag = this.newTag.trim()
940

@@ -19,11 +50,16 @@ export default function tagsInputFormComponent({ state, splitKeys }) {
1950

2051
this.state.push(this.newTag)
2152

53+
// A function replacement inserts the tag literally, so `$` sequences in it are not treated as substitution patterns by `String.replace()`. The message is optional so a published pre-change view override does not throw.
54+
this.announce(tagAddedMessage?.replace(':tag', () => this.newTag))
55+
2256
this.newTag = ''
2357
},
2458

2559
deleteTag(tagToDelete) {
2660
this.state = this.state.filter((tag) => tag !== tagToDelete)
61+
62+
this.announce(tagRemovedMessage?.replace(':tag', () => tagToDelete))
2763
},
2864

2965
reorderTags(event) {

packages/forms/resources/lang/en/components.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,26 @@
122122

123123
],
124124

125+
'color_picker' => [
126+
127+
'panel_label' => 'Color picker',
128+
129+
],
130+
125131
'file_upload' => [
126132

133+
'actions' => [
134+
135+
'download' => [
136+
'label' => 'Download',
137+
],
138+
139+
'open' => [
140+
'label' => 'Open in new tab',
141+
],
142+
143+
],
144+
127145
'editor' => [
128146

129147
'label' => 'Image editor',
@@ -270,6 +288,18 @@
270288

271289
],
272290

291+
'columns' => [
292+
293+
'actions' => [
294+
'label' => 'Actions',
295+
],
296+
297+
'reorder' => [
298+
'label' => 'Reorder',
299+
],
300+
301+
],
302+
273303
'fields' => [
274304

275305
'key' => [
@@ -341,6 +371,18 @@
341371

342372
'repeater' => [
343373

374+
'columns' => [
375+
376+
'actions' => [
377+
'label' => 'Actions',
378+
],
379+
380+
'reorder' => [
381+
'label' => 'Reorder',
382+
],
383+
384+
],
385+
344386
'actions' => [
345387

346388
'add' => [
@@ -606,6 +648,10 @@
606648
'searching_message' => 'Searching...',
607649
],
608650

651+
'toolbar' => [
652+
'aria_label' => 'Editor toolbar',
653+
],
654+
609655
'tools' => [
610656
'align_center' => 'Align center',
611657
'align_end' => 'Align end',
@@ -745,6 +791,10 @@
745791

746792
'placeholder' => 'New tag',
747793

794+
'tag_added' => 'Added: :tag',
795+
796+
'tag_removed' => 'Removed: :tag',
797+
748798
],
749799

750800
'text_input' => [

packages/forms/src/Components/CheckboxList.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ public function toEmbeddedHtml(): string
528528

529529
<div class="fi-input-wrp-content-ctn">
530530
<input
531+
aria-label="<?= e($this->getSearchPrompt()) ?>"
531532
placeholder="<?= e($this->getSearchPrompt()) ?>"
532533
type="search"
533534
x-model.debounce.<?= $this->getSearchDebounce() ?>="search"
@@ -627,6 +628,8 @@ class="fi-fo-checkbox-list-option-ctn"
627628
<div
628629
x-cloak
629630
x-show="search && ! visibleCheckboxListOptions.length"
631+
role="status"
632+
aria-live="polite"
630633
class="fi-fo-checkbox-list-no-search-results-message"
631634
>
632635
<?= e($this->getNoSearchResultsMessage()) ?>

packages/forms/src/Components/ColorPicker.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ public function toEmbeddedHtml(): string
109109

110110
$inputAttributes = $this->getExtraInputAttributeBag()
111111
->merge([
112+
// `aria-expanded` / `aria-haspopup` are not supported on a plain textbox, so only
113+
// `aria-controls` (a global ARIA attribute) associates the input with its panel.
114+
'aria-controls' => "{$id}-panel",
112115
'autocomplete' => 'off',
113116
'disabled' => $isDisabled,
114117
'id' => $id,
@@ -158,6 +161,7 @@ public function toEmbeddedHtml(): string
158161
/>
159162

160163
<div
164+
aria-hidden="true"
161165
class="fi-fo-color-picker-preview my-auto me-3 size-5 shrink-0 rounded-full select-none"
162166
x-on:click="togglePanelVisibility()"
163167
x-bind:class="{
@@ -172,6 +176,9 @@ class="fi-fo-color-picker-preview my-auto me-3 size-5 shrink-0 rounded-full sele
172176
x-cloak
173177
x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }"
174178
x-ref="panel"
179+
id="<?= e($id) ?>-panel"
180+
role="dialog"
181+
aria-label="<?= e(__('filament-forms::components.color_picker.panel_label')) ?>"
175182
class="fi-fo-color-picker-panel"
176183
>
177184
<<?= $tag ?> x-ref="picker" color="<?= e($this->getState()) ?>" />

packages/forms/src/Components/FileUpload.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,8 @@ public function toEmbeddedHtml(): string
960960
state: $wire.<?= $this->applyStateBindingModifiers("\$entangle('{$statePath}')") ?>,
961961
uploadButtonPosition: <?= Js::from($this->getUploadButtonPosition()) ?>,
962962
uploadingMessage: <?= Js::from($this->getUploadingMessage()) ?>,
963+
downloadActionLabel: <?= Js::from(__('filament-forms::components.file_upload.actions.download.label')) ?>,
964+
openActionLabel: <?= Js::from(__('filament-forms::components.file_upload.actions.open.label')) ?>,
963965
uploadProgressIndicatorPosition: <?= Js::from($this->getUploadProgressIndicatorPosition()) ?>,
964966
uploadUsing: (fileKey, file, success, error, progress) => {
965967
$wire.upload(
@@ -990,6 +992,7 @@ public function toEmbeddedHtml(): string
990992
x-show="error"
991993
x-text="error"
992994
x-cloak
995+
role="alert"
993996
class="fi-fo-file-upload-error-message"
994997
></div>
995998

@@ -1016,7 +1019,9 @@ class="fi-fo-file-upload-editor-overlay"
10161019

10171020
<div class="fi-fo-file-upload-editor-window">
10181021
<div class="fi-fo-file-upload-editor-image-ctn">
1022+
<?php // Decorative: Cropper.js drives this image and the editor dialog is labelled elsewhere. ?>
10191023
<img
1024+
alt=""
10201025
x-ref="editor"
10211026
class="fi-fo-file-upload-editor-image"
10221027
/>

packages/forms/src/Components/KeyValue.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ public function toEmbeddedHtml(): string
413413
scope="col"
414414
x-show="rows.length"
415415
class="fi-has-action"
416-
></th>
416+
>
417+
<span class="fi-sr-only"><?= e(__('filament-forms::components.key_value.columns.reorder.label')) ?></span>
418+
</th>
417419
<?php } ?>
418420

419421
<th scope="col">
@@ -429,7 +431,9 @@ class="fi-has-action"
429431
scope="col"
430432
x-show="rows.length"
431433
class="fi-has-action"
432-
></th>
434+
>
435+
<span class="fi-sr-only"><?= e(__('filament-forms::components.key_value.columns.actions.label')) ?></span>
436+
</th>
433437
<?php } ?>
434438
</tr>
435439
</thead>
@@ -463,6 +467,7 @@ class="fi-fo-key-value-table-row-sortable-handle"
463467

464468
<td>
465469
<input
470+
aria-label="<?= e($this->getKeyLabel()) ?>"
466471
<?= ((! $canEditKeys) || $isDisabled) ? 'disabled' : '' ?>
467472
placeholder="<?= e($keyPlaceholder) ?>"
468473
type="text"
@@ -474,6 +479,7 @@ class="fi-input"
474479

475480
<td>
476481
<input
482+
aria-label="<?= e($this->getValueLabel()) ?>"
477483
<?= ((! $canEditValues) || $isDisabled) ? 'disabled' : '' ?>
478484
placeholder="<?= e($valuePlaceholder) ?>"
479485
type="text"

packages/forms/src/Components/Repeater.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,7 +1951,9 @@ protected function toTableEmbeddedHtml(): string
19511951
<thead>
19521952
<tr>
19531953
<?php if ($hasReorderColumn) { ?>
1954-
<th class="fi-fo-table-repeater-empty-header-cell"></th>
1954+
<th scope="col" class="fi-fo-table-repeater-empty-header-cell">
1955+
<span class="fi-sr-only"><?= e(__('filament-forms::components.repeater.columns.reorder.label')) ?></span>
1956+
</th>
19551957
<?php } ?>
19561958

19571959
<?php foreach ($tableColumns as $column) { ?>
@@ -1968,7 +1970,7 @@ protected function toTableEmbeddedHtml(): string
19681970
$thAttributes = $thAttributes->style(['width: ' . e($columnWidth)]);
19691971
}
19701972
?>
1971-
<th <?= $thAttributes->toHtml() ?>>
1973+
<th scope="col" <?= $thAttributes->toHtml() ?>>
19721974
<?php if (! $column->isHeaderLabelHidden()) { ?>
19731975
<?= e($column->getLabel()) ?><?php if ($column->isMarkedAsRequired()) { ?><sup class="fi-fo-table-repeater-header-required-mark">*</sup><?php } ?>
19741976
<?php } else { ?>
@@ -1980,7 +1982,9 @@ protected function toTableEmbeddedHtml(): string
19801982
<?php } ?>
19811983

19821984
<?php if ($hasActionsColumn) { ?>
1983-
<th class="fi-fo-table-repeater-empty-header-cell"></th>
1985+
<th scope="col" class="fi-fo-table-repeater-empty-header-cell">
1986+
<span class="fi-sr-only"><?= e(__('filament-forms::components.repeater.columns.actions.label')) ?></span>
1987+
</th>
19841988
<?php } ?>
19851989
</tr>
19861990
</thead>

0 commit comments

Comments
 (0)