Skip to content

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 29 Apr 15:07
· 14 commits to main since this release

v2.3.0 (2026-04-30)

✨ New Features

  • Playback speed support (Closes #3): the player now supports per-track playback speed via three new public surfaces.

    • 1. AudioPlayer.SpeedSelector compound slot β€” a Dropdown-based UI that displays the current rate as a clickable label (e.g. 1Γ—, 1.5Γ—) and opens a menu of selectable rates. Defaults to [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]. Mounts automatically alongside the preset when activeUI.playbackRate is true (or activeUI.all is true); accepts options?: number[] and formatRate?: (rate: number) => string for customization.

      <AudioPlayer playList={list} activeUI={{ all: true, playbackRate: true }} />
      
      // or as a compound child with custom rate options
      <AudioPlayer playList={list} activeUI={{ all: true, playbackRate: false }}>
        <AudioPlayer.SpeedSelector
          options={[1, 1.5, 2, 3]}
          formatRate={(r) => `${r}x`}
          gridArea="row1-11"
        />
      </AudioPlayer>

      The dropdown menu uses role="menu" + role="menuitemradio" with aria-checked reflecting the active rate (WAI-ARIA APG menu pattern, matches video.js / Vidstack).

    • 2. useAudioPlayer().playbackRate + setPlaybackRate(rate) β€” the imperative API exposes the current rate and a setter. Available on the facade and on useAudioPlayerPlayback for fine-grained subscriptions.

      const { playbackRate, setPlaybackRate } = useAudioPlayer();
      // ... later
      setPlaybackRate(1.5);
    • 3. audioInitialState.playbackRate β€” initial rate at mount, defaults to 1. No clamping is applied at any layer; the browser enforces HTML5 playbackRate bounds.

    Implementation notes: the new SET_PLAYBACK_RATE reducer action is the single write path; useAudio mirrors the rate to audioEl.playbackRate via a sync effect plus a re-apply inside onLoadedMetadata (the browser resets the DOM playbackRate to 1 on src change, mirroring the existing volume re-apply pattern). DEFAULT_INTERFACE_GRID_BOUND (renamed from defaultInterfacePlacementMaxLength in this release β€” see Breaking Changes) was bumped from 10 to 11 to accommodate playbackRate: "row1-10" in the default template area; consumers passing an explicit interfacePlacement length parameter are unaffected.

  • Volume and SpeedSelector dropdown customization: both compound slots now expose triggerType?: "click" \| "hover" and a placement? prop typed as the per-slot domain alias β€” VolumeSliderPlacement for <AudioPlayer.Volume> and SpeedSelectorPlacement for <AudioPlayer.SpeedSelector> (both currently resolve to "top" \| "bottom" \| "left" \| "right"). AudioPlayer.SpeedSelector also gains a top-level placement.speedSelector?: SpeedSelectorPlacement provider option that mirrors the existing placement.volumeSlider. Resolution order (both knobs, both components): compound prop > UIContext > component default. Defaults are unchanged β€” Volume stays on triggerType="hover" with viewport-aware auto-placement, SpeedSelector stays on triggerType="click" with placement="top".

    // Switch Volume to a click-opened panel and force the menu below the trigger
    <AudioPlayer.Volume triggerType="click" placement="bottom" />
    
    // Per-instance SpeedSelector placement
    <AudioPlayer.SpeedSelector placement="bottom" triggerType="hover" />
    
    // Or set a default for every SpeedSelector mounted under this provider
    <AudioPlayer playList={list} placement={{ speedSelector: "bottom" }} />

    Implementation note: when Volume resolves to triggerType="click", the inner <Dropdown.Content> role switches from "tooltip" to "dialog" for semantic correctness β€” tooltip semantics are reserved for non-interactive informational popovers and don't fit a click-opened slider panel. SpeedSelector keeps role="menu" regardless of triggerType (the menu pattern allows either trigger style).

πŸ’₯ Breaking Changes

  • row1-10 grid slot now occupied by default playbackRate control: The new default playbackRate placement at row1-10 will collide with any existing customComponentsArea or templateArea entry that targets the same cell. Two ways to resolve:

    • Move the conflicting custom area to a different cell (e.g. row1-11)
    • Disable the new slot by setting activeUI={{ ..., playbackRate: false }}

    No collision warning is emitted at runtime β€” the symptom is overlapping cells in the rendered grid.

  • defaultInterfacePlacementMaxLength renamed to DEFAULT_INTERFACE_GRID_BOUND: the magic-number constant exported from the package barrel is renamed to follow the project's UPPER_SNAKE_CASE convention for enum-like constants and to clarify its semantics. The value (11) and behavior are unchanged β€” it is the exclusive upper bound on grid indices, so usable cells are 1..(DEFAULT_INTERFACE_GRID_BOUND - 1) (i.e. 1..10 by default). Consumers importing the old name must update their import:

    - import { defaultInterfacePlacementMaxLength } from "react-modern-audio-player";
    + import { DEFAULT_INTERFACE_GRID_BOUND } from "react-modern-audio-player";
  • AudioPlayer.Volume role attribute change with triggerType="click": When Volume resolves to triggerType="click" (via the compound prop, the placement.volumeSlider provider option, or direct usage), the inner <Dropdown.Content> role switches from "tooltip" to "dialog". Consumers asserting role="tooltip" in tests, targeting [role="tooltip"] in CSS, or relying on tooltip semantics in accessibility tooling must update those expectations. Hover mode (the default) is unchanged.

πŸ› Bug Fixes

  • Auto-play after track swap on bar-progress players: pressing next (or letting a track end) while playing now correctly continues into the new track when activeUI.progress === "bar". Previously, only waveform-mounted players auto-played the next track β€” useWavesurfer's onReady callback was the only path calling audioEl.play() after a src change, so bar-only players (and waveform players that had never been mounted) loaded the new track and sat silent while the play button still showed the "playing" icon. Fix moves the responsibility into useAudio's play effect by adding audioResetKey to its deps; the wavesurfer onReady handler no longer calls play(), removing the duplicate path.

  • PREV button silent on bar-progress players when crossing tracks: the same root cause manifested on the previous-track path. The PREV_AUDIO reducer's track-change branches (SHUFFLE and the default infinite-loop wrap) used to skip the audioResetKey bump, so the play effect's new key-based dep wouldn't fire on those branches either. Both branches now bump audioResetKey to match NEXT_AUDIO, restoring playback continuity across all repeat modes.


Full Changelog: v2.2.0...v2.3.0