Skip to content

Expose the waveform as a keyboard-operable, ARIA slider - #9

Closed
scruffian wants to merge 2 commits into
arraypress:mainfrom
scruffian:a11y/seek-slider
Closed

Expose the waveform as a keyboard-operable, ARIA slider#9
scruffian wants to merge 2 commits into
arraypress:mainfrom
scruffian:a11y/seek-slider

Conversation

@scruffian

Copy link
Copy Markdown
Contributor

Summary

Makes the waveform seek surface accessible to keyboard and assistive-technology users. Closes #8.

Today the seek area is mouse-only: the container is tabindex="-1" until clicked, and the waveform has no role or ARIA value attributes, so keyboard-only and screen-reader users can't discover or operate seeking (WCAG 2.1.1 Keyboard, 4.1.2 Name/Role/Value).

What this does

  • Marks .waveform-container as role="slider", focusable in the tab order (tabindex="0").
  • Keeps aria-valuemin/aria-valuemax/aria-valuenow and a readable aria-valuetext (e.g. "0:30 of 2:00") in sync on metadata load, timeupdate, and external setProgress().
  • Handles the standard slider keys when focused — / and / (±5s), Page Up/Page Down (±10s), Home/End — calling preventDefault() (no page scroll) and stopPropagation() so the existing container-level handler doesn't also fire.
  • Works in both audio modes: in self mode it calls seekTo(); in external mode it dispatches waveformplayer:request-seek with a percent, exactly like click-to-seek.
  • Adds a :focus-visible outline for the slider, matching the existing button/marker focus styling.

Options

Option Type Default Description
accessibleSeek boolean true Expose the waveform as a keyboard-operable ARIA slider. Set false to opt out and keep the prior behavior.
seekLabel string null Accessible name for the slider. Falls back to the track title, then 'Seek'. (Lets consumers localize the label.)

Backwards compatibility

accessibleSeek defaults to true, so the waveform becomes a tab stop with slider semantics by default — this is the point of the fix. The existing click-to-focus container shortcuts are untouched. Anyone who needs the old markup can set accessibleSeek: false.

Testing

  • npm run build succeeds; changes are reflected in dist/.
  • Verified with a jsdom harness (external mode, so no real audio/canvas decoding needed): slider role/tabindex/aria-label/aria-valuemin are set; aria-valuemax/valuenow/valuetext track setProgress(30, 120)"0:30 of 2:00"; dispatches request-seek with percent = 35/120; Endpercent = 1; and accessibleSeek: false leaves the waveform with no role. All assertions pass.

The repo has no existing test suite (and instantiating the player under test requires a canvas stub), so I verified via the harness above rather than adding a framework. Happy to adjust the approach, key bindings, step sizes, or option names to match your preferences.

Context: we (WordPress/Gutenberg) currently layer this behavior on top of the library from the outside by reaching into .waveform-container and suppressing the internal keydown handler. We'd much rather rely on it natively — hence this PR.

scruffian added 2 commits June 5, 2026 14:53
The waveform seek surface was only operable by mouse: the container is
tabindex="-1" until clicked, and the waveform had no role or ARIA value
attributes, so keyboard-only and screen-reader users couldn't discover
or operate seeking (WCAG 2.1.1, 4.1.2).

Mark .waveform-container as role="slider", make it focusable, keep
aria-valuemin/max/now/valuetext in sync with playback, and handle the
standard slider keys (Arrow/Page/Home/End) to seek. Works in both self
and external audio modes. New options: accessibleSeek (default true,
opt-out) and seekLabel (accessible name, falls back to the track title).

Closes arraypress#8
arraypress added a commit that referenced this pull request Jun 27, 2026
Expose the waveform surface as an ARIA slider: role="slider", focusable
in the tab order, with aria-valuemin/max/now and a readable
aria-valuetext ("0:30 of 2:00") kept in sync on metadata load,
timeupdate, and external setProgress(). Standard slider keys when
focused — arrows (+/-5s), Page Up/Down (+/-10s), Home/End — calling
preventDefault() to avoid page scroll. Works in self mode (seekTo) and
external mode (dispatches waveformplayer:request-seek, like
click-to-seek).

New options: accessibleSeek (default true; opt out to keep prior markup)
and seekLabel (accessible name, falls back to title then 'Seek').

Addresses WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value).
Upstreams the accessibility layer the WordPress/Gutenberg team had been
applying externally. Ported onto the current codebase from #9 (authored
against 1.2.1). Resolves #8.

Co-Authored-By: Ben Dwyer <275961+scruffian@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arraypress

Copy link
Copy Markdown
Owner

Thanks for this, @scruffian — really thorough writeup, and exactly the right call to upstream it rather than keep patching .waveform-container from the outside.

I've landed the functionality natively in v1.7.2 (just tagged). The codebase had moved on a fair bit since you opened this (you were against 1.2.1; main is now 1.7.x with the external audio-mode controller path), so rather than merge the diff directly I ported your approach onto the current source — but it's effectively your design:

  • .waveform-container gets role="slider", tabindex="0", and aria-valuemin/max/now + a readable aria-valuetext ("0:30 of 2:00"), synced on metadata load, timeupdate, and external setProgress().
  • Slider keys when focused: arrows (±5s), Page Up/Down (±10s), Home/End, with preventDefault().
  • Self mode → seekTo(); external mode → dispatches waveformplayer:request-seek with percent, identical to click-to-seek, so the waveform-bar controller picks it up unchanged.
  • New options accessibleSeek (default true) and seekLabel, exactly as you proposed.

Credited you as co-author on the commit (1a... feat: accessible keyboard-operable seek slider). You should be able to drop the external Gutenberg layer once you bump to 1.7.2 on npm — would appreciate a sanity check from your side once it's live.

Closing this in favour of the released version. Thanks again. 🙏

@arraypress

Copy link
Copy Markdown
Owner

One practical follow-up, @scruffian — when you get a chance, it's worth bumping the pin on your side. It looks like @wordpress/block-library currently depends on @arraypress/waveform-player@1.2.1, which predates both the external audio-mode controller path and this accessibility work, so the native slider won't reach Gutenberg until that's updated.

Moving block-library to ^1.7.2 should let you drop the external layer entirely — the keyboard/ARIA slider is on by default (role="slider", aria-valuetext, arrow/Page/Home/End seeking), and in external mode it dispatches the same waveformplayer:request-seek event your controller already handles, so seeking keeps working without the .waveform-container reach-in or the stopImmediatePropagation() suppression.

Heads up on what changed between 1.2.1 → 1.7.x in case it affects your integration:

  • audioMode: 'external' is now a first-class mode (the player can delegate audio and act as a visualization surface, with setPlayingState() / setProgress() pumps).
  • ESM/SSR packaging was fixed (exports map + type: module), so import { WaveformPlayer } resolves cleanly under bundlers/SSR.
  • accessibleSeek: false is available if you ever need the pre-1.7.2 markup back.

Happy to help smooth over any rough edges if you hit something during the upgrade — and if it's useful I can put together a short 1.2.1 → 1.7.2 migration note. Thanks again for pushing this upstream. 🙏

@scruffian

Copy link
Copy Markdown
Contributor Author

Thanks!

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.

Accessibility: expose the waveform as a keyboard-operable slider with ARIA

2 participants