v2.1.0
v2.1.0 (2026-04-14)
♿ Accessibility
-
SortableList keyboard support: playlist items are now focusable (
tabIndex=0) and expose full keyboard control following the WAI-ARIA "Listbox with Rearrangeable Options" pattern:Key Action TabMove focus between controls ArrowUp/ArrowDownMove focus between playlist items Alt+ArrowUp/Alt+ArrowDownReorder the focused item Enter/SpaceSelect the focused track -
Native
<button>for Dropdown trigger: replaced<div role="button">with a real<button>element. Enter/Space activation is now handled by the browser natively, and the trigger exposesaria-haspopup="true",aria-expanded, andaria-controlswired to the dropdown id. -
Eliminated nested
<button>anti-pattern in Volume control: the volume tooltip trigger was rendering a<button>insideDropdown.Trigger(itself a button). Refactored so the outerDropdown.Triggerowns the button role andVolumeIconrenders only the icon — screen readers now announce a single control. -
PlayBtn ARIA correction: removed incorrect
aria-pressedfrom the play/pause button. Play → Pause changes the button's function (not a toggle state), so the dynamicaria-labelis the correct signal. Usingaria-pressedsimultaneously announced a conflicting state to screen readers. -
vitest-axe smoke tests: added automated
axe-corechecks forPlayBtn,RepeatTypeBtn,BarProgress,Dropdown(both closed and expanded states),SortableList, andDrawer.
🐛 Bug Fixes
- Empty playlist crash (Fixes #25): passing
playList={[]}or dynamically switching to an empty list no longer throws "fetch url missing" and no longer crashes the component tree.- Reducer
NEXT_AUDIOearly-returns on empty playlist, preventing(idx + 1) % 0 = NaNstate corruption. createInitialStateuses nullish checks instead of truthy checks so tracks withid: 0are handled correctly, and falls back toplayList[0].idwhenaudioInitialState.curPlayIddoesn't match any track.UPDATE_PLAY_LISTaccepts empty arrays (previously silently rejected) and resetsisPlaying,currentTime,duration, andisLoadedMetaDatato safe defaults. When the current track disappears from the new list, playback falls back to the first track.- WaveSurfer
load()is skipped when the audio element has nosrc, so the waveform renderer no longer throws during empty-state transitions.
- Reducer
- SortableList focus restoration after reorder: replaced
requestAnimationFramewithflushSyncso the reorder commits synchronously before focus is restored. The previous rAF-based path could race React's render cycle and focus the pre-reorder element.
🔧 Internal
DropdownTriggerspread order:{...props}is placed before context-driven ARIA attributes so consumers cannot accidentally overridearia-expandedoraria-controls.- SortableList a11y tests migrated from
fireEvent.keyDownto@testing-library/user-eventuser.keyboard()for more realistic browser event sequences.
Full Changelog: v2.0.0...v2.1.0