Add aria-valuetext to the seeking, volume and balance sliders

Screen readers announced these range inputs as bare numbers (e.g. "26" for
the seeking bar). Describe their values instead:

- Seeking Bar: "0:32 of 2:00" (the scrub position while dragging)
- Volume Bar: "75%"
- Balance: "Center", "20% Left" or "20% Right"
This commit is contained in:
Jordan Eldredge committed 2026-10-12 12:13:37 -07:00
1 parent 504eea5288
commit 7794463bfc
5 files changed
+42 -4

No files matched your search

@@ -0,0 +1,9 @@
import { balanceValueText } from "./Balance";
describe("balanceValueText", () => {
it("describes the balance", () => {
expect(balanceValueText(0)).toBe("Center");
expect(balanceValueText(-20)).toBe("20% Left");
expect(balanceValueText(100)).toBe("100% Right");
});
});
@@ -4,6 +4,14 @@ import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { useTypedSelector, useActionCreator } from "../hooks";
// E.g. "Center" or "20% Left"
export function balanceValueText(balance: number): string {
if (balance === 0) {
return "Center";
}
return `${Math.abs(balance)}% ${balance > 0 ? "Right" : "Left"}`;
}
interface Props {
id?: string;
style?: React.CSSProperties;
@@ -29,6 +37,7 @@ export default function Balance({ style, className, id }: Props) {
onPointerDown={() => setFocus("balance")}
onPointerUp={unsetFocus}
title="Balance"
aria-valuetext={balanceValueText(balance)}
/>
);
}
@@ -125,4 +125,14 @@ describe("Position", () => {
pointer("pointerup");
expect(seeks()).toEqual([60]);
});
it("describes the position as elapsed time of total time", () => {
expect(input.getAttribute("aria-valuetext")).toBe("0:30 of 2:00");
});
it("describes the scrub position while dragging", () => {
pointer("pointerdown");
changeValue("50");
expect(input.getAttribute("aria-valuetext")).toBe("1:00 of 2:00");
});
});
@@ -1,6 +1,7 @@
import { FormEvent, KeyboardEvent, memo, useCallback, useRef } from "react";
import * as Selectors from "../../selectors";
import * as Actions from "../../actionCreators";
import { getTimeStr } from "../../utils";
import { useTypedSelector, useTypedDispatch } from "../../hooks";
function usePosition() {
@@ -10,14 +11,21 @@ function usePosition() {
const scrubPosition = useTypedSelector(Selectors.getUserInputScrubPosition);
const userInputFocus = useTypedSelector(Selectors.getUserInputFocus);
const displayedPosition =
userInputFocus === "position" ? scrubPosition : position;
const scrubbing = userInputFocus === "position";
const displayedPosition = scrubbing ? scrubPosition : position;
return [position, displayedPosition];
// E.g. "0:32 of 2:00"
let valueText;
if (duration != null) {
const time = scrubbing ? (duration * scrubPosition) / 100 : timeElapsed;
valueText = `${getTimeStr(time)} of ${getTimeStr(duration)}`;
}
return [position, displayedPosition, valueText] as const;
}
const Position = memo(() => {
const [position, displayedPosition] = usePosition();
const [position, displayedPosition, valueText] = usePosition();
const dispatch = useTypedDispatch();
const seekToPercentComplete = useCallback(
@@ -124,6 +132,7 @@ const Position = memo(() => {
onPointerUp={handlePointerUp}
onPointerDown={handlePointerDown}
title="Seeking Bar"
aria-valuetext={valueText}
/>
);
});
+1
View File
@@ -29,6 +29,7 @@ export default function Volume({ id, style, className }: Props) {
onPointerDown={() => setFocus("volume")}
onPointerUp={unsetFocus}
title="Volume Bar"
aria-valuetext={`${volume}%`}
/>
);
}