From 8b78c6cc9d6a3a9473ad6fff1957669ee31c9089 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 27 May 2026 15:28:57 +1000 Subject: [PATCH 1/2] fix(a11y): vimeo keyboard accessibility and video placeholder alt text Applies the same keyboard-accessibility pattern from #798 to ScriptVimeoPlayer so users navigating with a keyboard can activate the placeholder via Enter or Space. Also sets the placeholder image alt to "Play video" on both the Vimeo and YouTube components so screen readers describe the action rather than treating the thumbnail as decorative. --- .../runtime/components/ScriptVimeoPlayer.vue | 33 ++++++++++++++++--- .../components/ScriptYouTubePlayer.vue | 2 +- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/script/src/runtime/components/ScriptVimeoPlayer.vue b/packages/script/src/runtime/components/ScriptVimeoPlayer.vue index 4646672e6..4b5c7a236 100644 --- a/packages/script/src/runtime/components/ScriptVimeoPlayer.vue +++ b/packages/script/src/runtime/components/ScriptVimeoPlayer.vue @@ -253,16 +253,41 @@ watch(status, (status) => { } }) +// Keyboard accessibility: dispatch the configured trigger event on Enter/Space +// so users navigating via keyboard can activate the placeholder. +const keyboardTriggerable = computed(() => { + if (ready.value) + return false + const triggers = Array.isArray(props.trigger) ? props.trigger : [props.trigger] + return triggers.some(t => typeof t === 'string' && !['immediate', 'onNuxtReady', 'visibility', 'visible'].includes(t)) +}) + +function onPlaceholderKeydown(e: KeyboardEvent) { + if (e.key !== 'Enter' && e.key !== ' ') + return + e.preventDefault() + if (!rootEl.value) + return + const triggers = (Array.isArray(props.trigger) ? props.trigger : [props.trigger]).filter(Boolean) as string[] + for (const t of triggers) { + if (['immediate', 'onNuxtReady', 'visibility', 'visible'].includes(t)) + continue + rootEl.value.dispatchEvent(new Event(t, { bubbles: false })) + } +} + const rootAttrs = computed(() => { + const interactive = keyboardTriggerable.value return defu(props.rootAttrs, { 'aria-busy': status.value === 'loading', 'aria-label': status.value === 'awaitingLoad' - ? 'Vimeo Player - Placeholder' + ? (interactive ? 'Play video' : 'Vimeo Player - Placeholder') : status.value === 'loading' ? 'Vimeo Player - Loading' : 'Vimeo Player - Loaded', 'aria-live': 'polite', - 'role': 'application', + 'role': interactive ? 'button' : 'application', + 'tabindex': interactive ? 0 : undefined, 'style': { '--vimeo-ratio': props.ratio, 'maxWidth': '100%', @@ -279,7 +304,7 @@ const rootAttrs = computed(() => { const placeholderAttrs = computed(() => { return defu(props.placeholderAttrs, { src: placeholder.value, - alt: '', + alt: 'Play video', loading: props.aboveTheFold ? 'eager' : 'lazy', // @ts-expect-error untyped fetchpriority: props.aboveTheFold ? 'high' : undefined, @@ -296,7 +321,7 @@ onBeforeUnmount(() => player?.unload())