Skip to content

fix: announce live regions that render inside a shadow root - #252

Open
tlouisse wants to merge 1 commit into
guidepup:mainfrom
tlouisse:fix/announce-live-regions-inside-shadow-roots
Open

tlouisse wants to merge 1 commit into
guidepup:mainfrom
tlouisse:fix/announce-live-regions-inside-shadow-roots

Conversation

@tlouisse

Copy link
Copy Markdown

Announce live regions that render inside a shadow root

Fixes #251.

A live region whose content is rendered inside its own shadow
root (host element with aria-live, message rendered into the shadow root) is
never announced, while the same region in the light DOM is.

Three coupled causes, each verified against 0.33.0:

  1. observeDOM observes the container with subtree: true, and a
    MutationObserver does not cross shadow boundaries, so shadow root changes
    are never seen.
  2. Rendering into a shadow root targets the ShadowRoot itself;
    getElementFromNode resolves a ShadowRoot to undefined because it only
    falls back to parentElement.
  3. getLiveRegionAttributes walks up with parentElement only, so the
    aria-live attribute on the host is unreachable from inside its shadow root.

What this changes

  • observeDOM: observes node plus every shadow root inside it, and wraps
    attachShadow so shadow roots attached later are observed as they are created.
    Without the wrap, the first content of a newly attached shadow root is missed:
    observation starts after that content exists and MutationObserver does not
    replay it. Both are undone on teardown.
  • getElementFromNode: a ShadowRoot target maps to its host.
  • getLiveSpokenPhrase: the ancestor walk crosses the shadow boundary
    (assignedSlot ?? parentElement ?? getRootNode().host ?? null).

Tests

New: test/int/ariaLiveShadowRoot.int.test.ts, covering both mutation shapes:

  • a host with aria-live whose children are replaced through its shadow root
    (mutation target is the ShadowRoot);
  • a live region that lives inside a shadow root and gets a text update
    (the walk has to cross the boundary).

Evidence on this branch:

jest test/int/ariaLiveShadowRoot.int.test.ts test/int/ariaLive.int.test.ts
  Test Suites: 2 passed, 2 total   Tests: 9 passed, 9 total

jest test/int
  Test Suites: 57 passed, 57 total   Tests: 330 passed, 330 total

tsc --noEmit -p tsconfig.json  -> 0 errors
eslint src test/int/...        -> clean

Both new tests fail on main without the source change (2 failed, the log stops
after "button, Trigger shadow live region"), so they are regression tests
rather than tests that pass either way.

Context

Found while adding screen reader tests for @lion/ui (ing-bank/lion), where
<lion-input-iban> renders its invalid message in the shadow root of
lion-validation-feedback with aria-live="assertive". NVDA and VoiceOver
announce that message; the virtual reader did not, so the test could not pin it
down locally. With this fix the virtual run speaks
"assertive: Error,, Please enter a valid IBAN.".

(Separate, cosmetic: the doubled comma comes from joining the additions phrase
and the text phrase of the same live region. Happy to look at that separately if
you want it tidied - it is not needed for this fix.)

A MutationObserver does not cross shadow boundaries, and the live region lookup
walked up with parentElement only, so an aria-live region that renders its
content inside its own shadow root was never announced.

- observeDOM observes shadow roots as well, and wraps attachShadow so shadow
  roots attached later are covered as they are created: a shadow root that is
  already populated when observation starts never replays those changes.
- getElementFromNode maps a ShadowRoot mutation target to its host instead of
  resolving to undefined - rendering into a shadow root targets the ShadowRoot
  itself, which has no parentElement.
- getTargetAncestor crosses the shadow boundary when walking up, so an aria-live
  attribute on the host is reachable from inside its shadow root.
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.

Live regions that render inside a shadow root are not announced

1 participant