diff --git a/packages/webamp-docs/docs/12_changelog.md b/packages/webamp-docs/docs/12_changelog.md index 9ebe8bba9..3462fc22c 100644 --- a/packages/webamp-docs/docs/12_changelog.md +++ b/packages/webamp-docs/docs/12_changelog.md @@ -15,6 +15,7 @@ If you want access to the changes in this section before they are officially rel - The Milkdrop window now has a stable id, `#milkdrop-window`, like `#main-window`, `#equalizer-window` and `#playlist-window`. - Added a new Webamp instance method: [`webamp.onMediaError(cb)`](./06_API/03_instance-methods.md#onmediaerrorcb-error--code-number---void---void), which is called when a track fails to load or play. - Added a new Webamp instance method: [`webamp.setTracks(tracks)`](./06_API/03_instance-methods.md#settrackstracks-track-void), which replaces the playlist without starting playback. +- Each window is now exposed to assistive technology as a labelled group: "Main window", "Equalizer", "Playlist" or "Milkdrop". ### Bug Fixes diff --git a/packages/webamp/js/components/FocusTarget.test.tsx b/packages/webamp/js/components/FocusTarget.test.tsx new file mode 100644 index 000000000..25f335eab --- /dev/null +++ b/packages/webamp/js/components/FocusTarget.test.tsx @@ -0,0 +1,49 @@ +// @types/react is still on v18, which does not declare React 19's `act`. +// @ts-ignore +import { act } from "react"; +import { createRoot, Root } from "react-dom/client"; +import { Provider } from "react-redux"; +import { legacy_createStore as createStore } from "redux"; +import reducer from "../reducers"; +import { WindowId } from "../types"; +import FocusTarget from "./FocusTarget"; + +// Tell React that this environment supports `act`. +// @ts-ignore +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe("FocusTarget", () => { + it.each([ + ["main", "Main window"], + ["equalizer", "Equalizer"], + ["playlist", "Playlist"], + ["milkdrop", "Milkdrop"], + ])("labels the %s window as a group", (windowId, label) => { + act(() => + root.render( + + +
+ + + ) + ); + const group = container.firstElementChild!; + expect(group.getAttribute("role")).toBe("group"); + expect(group.getAttribute("aria-label")).toBe(label); + }); +}); diff --git a/packages/webamp/js/components/FocusTarget.tsx b/packages/webamp/js/components/FocusTarget.tsx index bdb4df95e..ce1179a56 100644 --- a/packages/webamp/js/components/FocusTarget.tsx +++ b/packages/webamp/js/components/FocusTarget.tsx @@ -4,6 +4,15 @@ import * as Actions from "../actionCreators"; import * as Selectors from "../selectors"; import { useActionCreator, useTypedSelector } from "../hooks"; +// Accessible names for each window, so screen reader users know which window +// a control belongs to (several windows have a "Close" button, for example). +const WINDOW_LABELS: Record = { + main: "Main window", + equalizer: "Equalizer", + playlist: "Playlist", + milkdrop: "Milkdrop", +}; + interface Props { onKeyDown?(e: KeyboardEvent): void; windowId: WindowId; @@ -71,6 +80,8 @@ function FocusTarget({ onKeyDown, windowId, children }: Props) { onPointerDown={focusHandler} onFocus={focusHandler} tabIndex={-1} + role="group" + aria-label={WINDOW_LABELS[windowId]} style={{ height: "100%", width: "100%" }} > {children}