Skip to content

♿ 🐛 [Story system-layer] Restore focus after paired button toggles#1

Closed
michaeldegori wants to merge 1 commit into
mainfrom
fix/story-system-layer-button-focus
Closed

♿ 🐛 [Story system-layer] Restore focus after paired button toggles#1
michaeldegori wants to merge 1 commit into
mainfrom
fix/story-system-layer-button-focus

Conversation

@michaeldegori
Copy link
Copy Markdown
Owner

@michaeldegori michaeldegori commented May 12, 2026

Summary

Restores keyboard and screen-reader focus after the AMP Story system layer toggles between paired sibling buttons (captions on/off, mute/unmute, pause/play). Without this fix, activating one of those buttons via keyboard loses focus on desktop, and iOS VoiceOver lands on a hidden sibling.

Why

Each pair renders as two sibling <button> elements; CSS swaps which one is display: block !important based on a host attribute. When the user activates the visible button, the attribute flips and the just-clicked button becomes display: none, so the browser blurs it. Assistive tech treats this as confusing focus loss.

The fix moves focus to the now-visible sibling, but only when the just-hidden sibling held focus before the state change. Programmatic state changes (autoplay, viewer messaging) do not steal focus.

Changes

  • extensions/amp-story/1.0/amp-story-system-layer.js:
    • Adds activeElementHasClass_ (predicate) and focusButton_ (uses tryFocus from #core/dom).
    • In onCaptionsStateUpdate_, onMutedStateUpdate_, and onPausedStateUpdate_: captures whether the just-hidden sibling holds focus before the attribute mutation, then calls focusButton_ on the now-visible sibling.
  • extensions/amp-story/1.0/test/test-amp-story-system-layer.js:
    • 9 new tests covering focus restoration and the do-not-steal-focus behavior for each paired toggle.

Repro (without this fix)

  1. Open any AMP Story with the relevant control visible.
  2. Tab to the captions, pause, or mute button.
  3. Activate via Enter or Space.
  4. Observe that focus is lost (desktop) or jumps to a hidden element (iOS VoiceOver).

Tests

  • npx amp lint on the changed files: clean.
  • npx amp unit --files=extensions/amp-story/1.0/test/test-amp-story-system-layer.js: 29 passing, 1 pre-existing skip.

The captions, mute/unmute, and pause/play controls in the story system
layer are paired sibling buttons toggled by host attributes that drive
display:block via CSS. Clicking one hides it and reveals its sibling,
which drops keyboard focus on desktop and lands iOS VoiceOver on a
hidden element.

Capture whether the just-hidden sibling holds focus before the
attribute mutation, then move focus to the now-visible sibling.
Programmatic state changes do not move focus.
@michaeldegori
Copy link
Copy Markdown
Owner Author

Superseded by upstream PR ampproject#40504.

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.

1 participant