-
Notifications
You must be signed in to change notification settings - Fork 0
LYT-1120: give widget dialogs resolving accessible names and announce form states #676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ashyablok-cs
merged 3 commits into
develop
from
LYT-1120-widget-dialog-accessible-names
Sep 17, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /** @module pathfora/form/announce-form-state */ | ||
|
|
||
| // dom | ||
| import document from '../dom/document'; | ||
|
|
||
| // widgets | ||
| import describeWidgetContainer from '../widgets/describe-widget-container'; | ||
|
|
||
| /** | ||
| * Read the text out of a state element's headline or message. | ||
| * | ||
| * @params {object} state | ||
| * @params {string} selector | ||
| * @returns {string} | ||
| */ | ||
| function stateText(state, selector) { | ||
| var el = state.querySelector(selector); | ||
|
|
||
| return el ? el.textContent || el.innerText || '' : ''; | ||
| } | ||
|
|
||
| /** | ||
| * Make a revealed form success or error state perceivable to assistive | ||
| * technology. | ||
| * | ||
| * The state is revealed by CSS alone, which is silent, and the same rules hide | ||
| * the button the user just activated - so a dialog is renamed after its new | ||
| * contents and handed focus, which is what gets it read out and keeps a | ||
| * keyboard user from being dropped back to the top of the page. An inline | ||
| * widget sits in the page's own flow and should not steal focus, so its text | ||
| * is copied into the live region built alongside the states instead. | ||
| * | ||
| * @exports announceFormState | ||
| * @params {object} widget | ||
| * @params {string} name | ||
| */ | ||
| export default function announceFormState(widget, name) { | ||
| var state = widget.querySelector('.' + name + '-state'), | ||
| container = widget.querySelector('.pf-widget-container'); | ||
|
|
||
| if (!state || !container) { | ||
| return; | ||
| } | ||
|
|
||
| if (container.getAttribute('role') !== 'dialog') { | ||
| var region = widget.querySelector('.pf-widget-announcement'); | ||
|
|
||
| if (!region) { | ||
| return; | ||
| } | ||
|
|
||
| // NOTE the headline and message only, never the state's own buttons: the | ||
| // implicit aria-atomic on role=status means everything in here is read as | ||
| // one message, and "Thank You. We have received your submission." should | ||
| // not end in "Confirm Cancel" | ||
| var announcement = [ | ||
| stateText(state, '.pf-widget-headline'), | ||
| stateText(state, '.pf-widget-message'), | ||
| ] | ||
| .filter(function (text) { | ||
| return text.length > 0; | ||
| }) | ||
| .join('. '); | ||
|
|
||
| if (!announcement.length) { | ||
| return; | ||
| } | ||
|
|
||
| // NOTE written a tick late, after the class that reveals the state has | ||
| // been applied and styles have settled: Safari and VoiceOver are the | ||
| // least forgiving about text that arrives in the same tick as the change | ||
| // around it | ||
| setTimeout(function () { | ||
| while (region.firstChild) { | ||
| region.removeChild(region.firstChild); | ||
| } | ||
|
|
||
| region.appendChild(document.createTextNode(announcement)); | ||
| }, 0); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| describeWidgetContainer( | ||
| container, | ||
| state.querySelector('.pf-widget-headline'), | ||
| state.querySelector('.pf-widget-message'), | ||
| widget.id + '-' + name, | ||
| ); | ||
|
|
||
| container.setAttribute('tabindex', '-1'); | ||
| container.focus(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /** @module pathfora/form/construct-state-live-region */ | ||
|
|
||
| // dom | ||
| import document from '../dom/document'; | ||
|
|
||
| /** | ||
| * Build the empty live region an inline widget announces its form state | ||
| * through. | ||
| * | ||
| * A live region is only reliably announced when it is already rendered and | ||
| * empty at the moment its text arrives - a region that enters the | ||
| * accessibility tree with its text already inside is the classic case screen | ||
| * readers skip. So the region is built with the state elements, well before | ||
| * either state is revealed, and announceFormState writes into it. | ||
| * | ||
| * It is visually hidden rather than display: none, which would take it out of | ||
| * the accessibility tree along with its announcement, and it holds only the | ||
| * state's headline and message - role=status carries an implicit aria-atomic, | ||
| * so anything else in here would be read out with them. | ||
| * | ||
| * @exports constructStateLiveRegion | ||
| */ | ||
| export default function constructStateLiveRegion() { | ||
| var region = document.createElement('div'); | ||
|
|
||
| region.className = 'pf-widget-announcement'; | ||
| region.setAttribute('role', 'status'); | ||
|
|
||
| return region; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /** @module pathfora/widgets/describe-widget-container */ | ||
|
|
||
| /** | ||
| * Point a widget container's aria-labelledby/aria-describedby at the headline | ||
| * and message elements it should be named and described by, giving each one an | ||
| * id to be referenced by. | ||
| * | ||
| * Ids are namespaced under the widget id, which pathfora already guarantees to | ||
| * be unique, so several widgets open at once cannot collide. A widget holding | ||
| * more than one headline and message - a form and the success or error state | ||
| * that replaces it - passes a distinct namespace per set. | ||
| * | ||
| * Callers pass null for an element that holds no text: a reference to an empty | ||
| * or absent element leaves the container unnamed just as surely as no reference | ||
| * at all, so the reference is cleared instead. | ||
| * | ||
| * @exports describeWidgetContainer | ||
| * @params {object} container | ||
| * @params {object} headline | ||
| * @params {object} message | ||
| * @params {string} namespace | ||
| */ | ||
| export default function describeWidgetContainer( | ||
| container, | ||
| headline, | ||
| message, | ||
| namespace, | ||
| ) { | ||
| if (headline) { | ||
| headline.id = namespace + '-pf-widget-headline'; | ||
|
ashyablok-cs marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (message) { | ||
| message.id = namespace + '-pf-widget-message'; | ||
| } | ||
|
|
||
| // NOTE bar layouts have no headline element at all, so the message is the | ||
| // only text available to name the container with | ||
| var name = headline || message, | ||
| description = headline ? message : null; | ||
|
|
||
| if (name) { | ||
| container.setAttribute('aria-labelledby', name.id); | ||
| } else { | ||
| container.removeAttribute('aria-labelledby'); | ||
| } | ||
|
|
||
| if (description) { | ||
| container.setAttribute('aria-describedby', description.id); | ||
| } else { | ||
| container.removeAttribute('aria-describedby'); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /** @module pathfora/widgets/setup-widget-aria */ | ||
|
|
||
| // widgets | ||
| import describeWidgetContainer from './describe-widget-container'; | ||
|
|
||
| /** | ||
| * Give the widget container an accessible name and description by pointing | ||
| * aria-labelledby/aria-describedby at the widget's own headline and message. | ||
| * | ||
| * @exports setupWidgetAria | ||
| * @params {object} widget | ||
| * @params {object} config | ||
| */ | ||
| export default function setupWidgetAria(widget, config) { | ||
| var container = widget.querySelector('.pf-widget-container'); | ||
|
|
||
| if (!container) { | ||
| return; | ||
| } | ||
|
|
||
| describeWidgetContainer( | ||
| container, | ||
| config.headline ? widget.querySelector('.pf-widget-headline') : null, | ||
| config.msg ? widget.querySelector('.pf-widget-message') : null, | ||
| config.id, | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.