a11y: video play/pause button — aria-pressed, event-driven state, fix aria-hidden (closes #5515)#5516
Conversation
…dden Closes #5515 - Remove aria-hidden="" from button (was hiding it from AT) - Remove redundant title attribute (visible text content is sufficient) - Set aria-pressed="true" as initial default (conservative: video not confirmed playing until player fires its play event) - Change button initial label to "Play Video" (matches aria-pressed state) Vimeo: - Replace textContent comparison in click handler with aria-pressed check - Replace bufferend class management with dedicated play/pause events - play event: sets "Pause Video" + aria-pressed="false" + classes - pause event: sets "Play Video" + aria-pressed="true" + classes - Click handler: only calls play()/pause(); events drive all state YouTube: - Replace textContent comparison in click handler with aria-pressed check - onPlayerStateChange state 1: syncs button to "Pause Video" + aria-pressed="false" - onPlayerStateChange state 2: syncs button to "Play Video" + aria-pressed="true" - Both cases also manage az-video-playing/az-video-paused classes
…ure) Revert the split template literal at L48 back to a single line as required by prettier. The line is within the 80-char print-width limit so no wrap is needed.
- Revert split `if (window.screen && ...)` back to one line (L5)
- Revert split `const firstScriptTag` back to one line (L20)
- Remove unused `parentParagraph` variable (L31): class state updates
were moved to onPlayerStateChange; the click handler no longer needs
to reference the parent container directly
- Wrap long `getAttribute('aria-pressed')` condition per prettier
print-width rules (L65)
There was a problem hiding this comment.
Pull request overview
Improves accessibility and correctness of the background video play/pause control for YouTube/Vimeo by moving to event-driven UI state updates and removing problematic ARIA attributes.
Changes:
- Updates the Twig template to remove
aria-hidden=""/title, addaria-pressed, and set an initial “Play Video” label. - Refactors YouTube logic so
onPlayerStateChangedrives the button state, and click only triggers play/pause. - Refactors Vimeo logic so
play/pauseevents drive the button state, and click only triggers play/pause.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| modules/custom/az_paragraphs/az_paragraphs_text_media/templates/media--az-remote-video--az-background.html.twig | Updates initial button markup (label + ARIA attributes). |
| modules/custom/az_paragraphs/az_paragraphs_text_media/js/az_paragraphs_az_text_media_youtube.js | Makes button state event-driven via YouTube player state changes. |
| modules/custom/az_paragraphs/az_paragraphs_text_media/js/az_paragraphs_az_text_media_vimeo.js | Makes button state event-driven via Vimeo play/pause events. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -156,6 +148,22 @@ | |||
| resize(); | |||
| parentContainer.classList.add('az-video-playing'); | |||
There was a problem hiding this comment.
When the player enters the playing state (event.data === 1), the code adds az-video-playing but never removes az-video-paused. After a pause/resume cycle the container can end up with both classes, which can cause conflicting styling/behavior. Remove az-video-paused when transitioning to playing.
| parentContainer.classList.add('az-video-playing'); | |
| parentContainer.classList.add('az-video-playing'); | |
| parentContainer.classList.remove('az-video-paused'); |
| // Sync button state: video is confirmed playing. | ||
| const btn = parentContainer.querySelector('.az-video-playpause'); | ||
| if (btn) { | ||
| btn.textContent = 'Pause Video'; | ||
| btn.setAttribute('aria-pressed', 'false'); | ||
| } |
There was a problem hiding this comment.
In the confirmed playing state (event.data === 1), the button is set to "Pause Video" but aria-pressed is set to "false". For toggle buttons, aria-pressed should represent the current on/off state; this should be consistent with playback (playing => aria-pressed="true", paused => aria-pressed="false").
| const btn = parentContainer.querySelector('.az-video-playpause'); | ||
| if (btn) { | ||
| btn.textContent = 'Play Video'; | ||
| btn.setAttribute('aria-pressed', 'true'); |
There was a problem hiding this comment.
In the paused state (event.data === 2), the button is set to "Play Video" but aria-pressed is set to "true", which reports the toggle as pressed while the video is paused. Align aria-pressed with the playback state (paused => aria-pressed="false") to keep the programmatic state accurate.
| btn.setAttribute('aria-pressed', 'true'); | |
| btn.setAttribute('aria-pressed', 'false'); |
| // Sync button and class state when video plays. | ||
| element.player.on('play', () => { | ||
| parentParagraph.classList.remove('az-video-paused'); | ||
| parentParagraph.classList.add('az-video-playing'); | ||
| playPauseButton.textContent = 'Pause Video'; | ||
| playPauseButton.setAttribute('aria-pressed', 'false'); | ||
| }); | ||
|
|
||
| // Sync button and class state when video pauses. | ||
| element.player.on('pause', () => { | ||
| parentParagraph.classList.remove('az-video-playing'); | ||
| parentParagraph.classList.add('az-video-paused'); | ||
| playPauseButton.textContent = 'Play Video'; | ||
| playPauseButton.setAttribute('aria-pressed', 'true'); | ||
| }); |
There was a problem hiding this comment.
The Vimeo play/pause handlers update the label, but the aria-pressed values are inverted relative to playback (playing sets aria-pressed to "false" and paused sets it to "true"). For a toggle button, aria-pressed should reflect the current state (playing => true, paused => false) so assistive tech gets an accurate programmatic value.
| // Play/Pause button: delegate state changes to player events. | ||
| playPauseButton.addEventListener('click', (event) => { | ||
| event.preventDefault(); | ||
| if (event.currentTarget.textContent === 'Play Video') { | ||
| if (event.currentTarget.getAttribute('aria-pressed') === 'true') { | ||
| element.player.play().catch((error) => vimeoError(error)); | ||
| parentParagraph.classList.remove('az-video-paused'); | ||
| parentParagraph.classList.add('az-video-playing'); | ||
| event.currentTarget.textContent = 'Pause Video'; | ||
| event.currentTarget.setAttribute('title', 'Pause the video'); | ||
| } else { | ||
| element.player.pause().catch((error) => vimeoError(error)); | ||
| parentParagraph.classList.remove('az-video-playing'); | ||
| parentParagraph.classList.add('az-video-paused'); | ||
| event.currentTarget.textContent = 'Play Video'; | ||
| event.currentTarget.setAttribute('title', 'Play the video'); | ||
| } |
There was a problem hiding this comment.
The click handler treats aria-pressed="true" as "paused" (it calls player.play()), which inverts toggle semantics. After aligning aria-pressed with playback state, update this conditional so the action is derived from the current playback state consistently.
| <div class="az-video-player"></div> | ||
| </div> | ||
| <button type="button" class="bg-video-player-control btn btn-light az-video-playpause video-playpause" title="Pause the Video" aria-hidden="">Pause Video</button> | ||
| <button type="button" class="bg-video-player-control btn btn-light az-video-playpause video-playpause" aria-pressed="true">Play Video</button> |
There was a problem hiding this comment.
The initial state sets aria-pressed="true" while the label is "Play Video" (and playback is not yet confirmed). For a toggle button, aria-pressed should reflect the current state; starting from an unknown/not-playing state should be aria-pressed="false", then updated to true only once playback is confirmed.
| <button type="button" class="bg-video-player-control btn btn-light az-video-playpause video-playpause" aria-pressed="true">Play Video</button> | |
| <button type="button" class="bg-video-player-control btn btn-light az-video-playpause video-playpause" aria-pressed="false">Play Video</button> |
| // Play/Pause button: delegate state changes to player events. | ||
| const playPauseButton = | ||
| element.getElementsByClassName('az-video-playpause')[0]; | ||
| playPauseButton.addEventListener('click', (event) => { | ||
| event.preventDefault(); | ||
| if (event.currentTarget.textContent === 'Play Video') { | ||
| if ( | ||
| event.currentTarget.getAttribute('aria-pressed') === 'true' | ||
| ) { | ||
| element.player.playVideo(); | ||
| parentParagraph.classList.remove('az-video-paused'); | ||
| parentParagraph.classList.add('az-video-playing'); | ||
| event.currentTarget.textContent = 'Pause Video'; | ||
| event.currentTarget.setAttribute('title', 'Pause the video'); | ||
| } else { | ||
| element.player.pauseVideo(); | ||
| parentParagraph.classList.remove('az-video-playing'); | ||
| parentParagraph.classList.add('az-video-paused'); | ||
| event.currentTarget.textContent = 'Play Video'; | ||
| event.currentTarget.setAttribute('title', 'Play the video'); | ||
| } |
There was a problem hiding this comment.
The click handler treats aria-pressed="true" as the paused state (it calls playVideo()), which inverts typical toggle semantics and makes aria-pressed represent the opposite of the actual playback state. Consider making aria-pressed="true" mean "video is playing" and adjusting the click logic accordingly so the ARIA state matches reality.
- template: fix initial aria-pressed from 'true' to 'false' (video not yet playing on load)
- youtube.js: invert click handler so aria-pressed='true' (playing) calls pauseVideo()
- youtube.js: add classList.remove('az-video-paused') when entering playing state
- youtube.js: set aria-pressed='true' in playing state, 'false' in paused state
- vimeo.js: set aria-pressed='true' in play event, 'false' in pause event
- vimeo.js: invert click handler so aria-pressed='true' (playing) calls pause()
All changes align aria-pressed semantics: true = video is playing,
false = video is not playing, matching the ARIA toggle button pattern.
Addresses Copilot review on #5516.
|
All 7 Copilot review comments addressed in commit 7c2d422:
\�ria-pressed\ semantics are now consistent across both players: \ rue\ = video is playing, \alse\ = video is not playing. Ready for re-review. |
Description
Follow-up to #5505 (closes #5515).
#5505 replaced the two-button pattern with a single
<button>and fixedtabindexon iframes — all the right moves. This PR addresses two issues left open as follow-up:1. Autoplay state mismatch (bug)
The original button started as "Pause Video" under the assumption that autoplay always succeeds. When autoplay is blocked — by browser policy,
az_gdpr_consent, or user preference (as noted by @kevdevlu) — the video is not playing but the button says "Pause". A screen reader user hears "Pause Video, button" and presses it expecting to pause, but it starts playback instead.Fix: The button now defaults to "Play Video" /
aria-pressed="true"(conservative: not confirmed playing). The actual player events (play/pausefor Vimeo;onPlayerStateChangestates 1 and 2 for YouTube) drive all state updates. The button is always accurate regardless of autoplay outcome.2.
aria-hidden=""removed andaria-pressedadded (accessibility)aria-hidden=""on the button had an ambiguous empty-string value — some AT treat it asaria-hidden="true", hiding the only interactive control from the accessibility tree.aria-pressedis now used for toggle semantics. Screen readers announce: "Play Video, toggle button, pressed" (paused state) and "Pause Video, toggle button, not pressed" (playing state). State is programmatically determinable independent of the label.titleattribute is removed — visible text content is sufficient for the accessible name.Changes
media--az-remote-video--az-background.html.twigaria-hidden="", removetitle, addaria-pressed="true", set initial label to "Play Video"az_paragraphs_az_text_media_vimeo.jsplay/pauseevents sync button; click handler only calls play/pauseaz_paragraphs_az_text_media_youtube.jsonPlayerStateChangestates 1 and 2 sync button; click handler usesaria-pressedHow to test
az_gdpr_consentor browser devtools throttling): button should remain "Play Video" — pressing it starts playback; button then switches to "Pause Video"aria-pressed="true"aria-pressed="false"Related
Types of changes
Arizona Quickstart