Skip to content

Add an opt-in ariaNotify() transport for screen reader announcements - #5962

Open
Eugene (OEvgeny) wants to merge 11 commits into
mainfrom
OEvgeny/fix-aria-notify-transport
Open

Eugene (OEvgeny) wants to merge 11 commits into
mainfrom
OEvgeny/fix-aria-notify-transport

Conversation

@OEvgeny

@OEvgeny Eugene (OEvgeny) commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Part of ariaNotify()-first screen reader announcements (phase 1), related to #5857.

Changelog Entry

Added

  • 🧪 Added styleOptions.assistiveTechnologyAnnouncementTransport to announce via ariaNotify() instead of the live region, in PR #5962, by @OEvgeny

Description

Web Chat announces only via its live region, silent when the host hides it. With the experimental option set to 'aria-notify' (default 'live-region'), it calls ariaNotify() on its root instead, once per entry, falling back to the live region if unsupported.

Design

  • Text comes from the rendered entry (removed right after), so callers stay unchanged; dom-accessibility-api computes names, not live region text.
  • Next: a cancelable webchat:announce event, then ariaNotify() by default after an NVDA/JAWS pass.

Specific Changes

  • StyleOptions.ts, normalizeStyleOptions.ts (+ spec): option, JSDoc, warns if invalid
  • LiveRegionTwin: cached support check, fallback, one call per new entry, empty live region
  • getAnnouncementText.ts: text walker
  • ariaNotify/*.html: calls on the root, empty live region, host hiding it, alt text, buttons, inputs, no repeats after switching, fallback
  • I have added tests and executed them locally
  • I have added .changelog/<PR#>-<slug>.md
  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

Eugene Olonov added 4 commits October 22, 2026 11:24
In 'aria-notify' mode (and 'auto' when the root element supports ariaNotify()),
LiveRegionTwin computes the text of each queued entry with a small walker and
calls ariaNotify() on the Web Chat root element once per entry; the twin
container has no live semantics then. Default stays 'live-region'.

// Detects per instance, on the root element: Web Chat could be rendered in another document, such as an iframe or a popup window.
// React attaches the ref of the root element after layout effects of its descendants, so only passive effects can read it on mount.
useEffect(() => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be placed out of the render path? Any reason this check can flip mid-render?

@OEvgeny Eugene (OEvgeny) Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't flip: whether ariaNotify() exists depends on the browser engine, and it is the same in iframes and popups of that browser. Moved out of state in 6fea16c: a module-level check (Element.prototype.ariaNotify is a function) runs once on first use and is cached; first use rather than module load, so pages and tests that polyfill or remove ariaNotify() before rendering still work. The transport is decided on the first render, with no extra render; each ariaNotify() call still checks its target. accessibility/ariaNotify/ 6/6 ×3 and accessibility/liveRegion/ 34/34 on the pushed head.

@OEvgeny
Eugene (OEvgeny) marked this pull request as ready for review October 22, 2026 22:03
Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/api/src/StyleOptions.ts
await pageConditions.allImagesLoaded();

// THEN: The live region copy has the image as `<img alt>` and the buttons as `<button>` elements.
expect(pageElements.liveRegion().querySelector('img').getAttribute('alt')).toBe('Bot avatar');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure but it seems the pageElements.liveRegion() should be emptied as we switched to ariaNotify().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right: in 'aria-notify' mode the copy only serves to compute the text, but it stayed for internalLiveRegionFadeAfter (60 s here). 6dd07ab removes it right after ariaNotify(); the test now asserts the empty live region and the exact ariaNotify() calls.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the visual screen reader is correct or not after we switch to ariaNotify.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was misleading: the box is .webchat__live-region un-hidden by test CSS, showing an aria-hidden copy nobody reads. Since 6dd07ab it is empty in 'aria-notify' mode; what is announced is asserted via the ariaNotify() spy.

Comment thread packages/api/src/defaultStyleOptions.ts Outdated
emojiSet: undefined,

// Live region - Accessibility
announcementTransport: 'live-region',

@compulim William Wong (compulim) Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call it accessibilityAnnouncementTransport or assistiveTechnologyAnnouncementTransport (AT means screen reader)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to assistiveTechnologyAnnouncementTransport in ed98b5b.

Comment on lines +34 to +40
if (is(picklist(['aria-notify', 'live-region']), announcementTransport)) {
return announcementTransport;
}

typeof announcementTransport === 'undefined' || announcementTransportInvalidValue();

return defaultStyleOptions.announcementTransport;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am right...

Suggested change
if (is(picklist(['aria-notify', 'live-region']), announcementTransport)) {
return announcementTransport;
}
typeof announcementTransport === 'undefined' || announcementTransportInvalidValue();
return defaultStyleOptions.announcementTransport;
return parse(
fallback(
optional(picklist(['aria-notify', 'live-region']), 'live-region'),
() => {
announcementTransportInvalidValue();
return 'live-region';
}
),
announcementTransport
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — applied in 2a9dca4, with a spec for undefined/valid/invalid.

Eugene Olonov added 3 commits October 23, 2026 00:13
With announcementTransport: 'aria-notify', the copies rendered to compute the
announced text stayed in .webchat__live-region (aria-hidden, not live) for
internalLiveRegionFadeAfter. Remove them right after they are announced, and
assert the empty live region in the ariaNotify tests.
Apply review suggestion: parse with optional() (undefined -> 'live-region',
no warning) and fallback() (invalid -> warn, 'live-region'). Add a spec for
undefined, valid and invalid values.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants