fix(a11y): name the sliders and the progress bar where the role is
`a11y.md` lists `seek-bar` and `volume-control` under "what is already correct" because both pass `aria-label`. Measured with Accessibility.getFullAXTree against the running app on all eleven views, both sliders compute a name of "": `wa-slider` puts role="slider" on a div inside its own shadow root, pointing aria-labelledby at an empty internal <label>, and that IDREF outranks the host's aria-label. `volume-control` did not have the aria-label the audit credits it with at all. The name comes from `label` now, which is the library's own API — and for a slider that is visible, so `styles/wa-slider-label.css.ts` hides it by part. Preferred over reaching into the shadow root the way name-dialog.ts must: if Web Awesome renames the part the label becomes visible rather than silently nameless. The second rule in that file is load-bearing — `#slider` takes an 8px margin the moment a label exists, which grows the bar from 6px to 14px and moves the transport with it. a11y.25 is the same family: wa-progress-bar maps `label` onto its inner aria-label, falling back to the localised word "progress" — so it was named after the widget rather than after the work, not unnamed. The existing transport test asserted the host's aria-label and called it an accessible name, so it was pinning the bug.
This commit is contained in:
@@ -5,6 +5,7 @@ import WaSlider from '@awesome.me/webawesome/dist/components/slider/slider.js';
|
||||
import { formatSeconds } from '@utils/time';
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { designTokens } from '../../../styles/tokens.css';
|
||||
import { waSliderLabel } from '../../../styles/wa-slider-label.css';
|
||||
|
||||
const ProgressIntervalMillis = 1000;
|
||||
|
||||
@@ -25,7 +26,7 @@ export class SeekBar extends LitElement {
|
||||
@state()
|
||||
private showRemaining: boolean = true;
|
||||
|
||||
static override styles = [designTokens, css`
|
||||
static override styles = [designTokens, waSliderLabel, css`
|
||||
wa-slider {
|
||||
--track-size: 6px;
|
||||
flex: 1;
|
||||
@@ -210,7 +211,7 @@ export class SeekBar extends LitElement {
|
||||
<div id="seek-bar-container">
|
||||
<small data-testid="elapsed-time">${elapsedTime}</small>
|
||||
<wa-slider
|
||||
aria-label="Seek"
|
||||
label="Seek"
|
||||
.value="${this.seekValue}"
|
||||
max="${this.trackLength}"
|
||||
?with-tooltip="${this.hasTrack}"
|
||||
|
||||
@@ -5,6 +5,7 @@ import '@awesome.me/webawesome/dist/components/slider/slider.js';
|
||||
import type WaSlider from '@awesome.me/webawesome/dist/components/slider/slider.js';
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { designTokens } from '../../../styles/tokens.css';
|
||||
import { waSliderLabel } from '../../../styles/wa-slider-label.css';
|
||||
|
||||
/** Volume change (0-100) applied per scroll-wheel tick. */
|
||||
const WHEEL_STEP = 5;
|
||||
@@ -28,7 +29,7 @@ export class VolumeControl extends LitElement {
|
||||
@state()
|
||||
private pendingVolume: number | null = null;
|
||||
|
||||
static override styles = [designTokens, css`
|
||||
static override styles = [designTokens, waSliderLabel, css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
@@ -209,6 +210,7 @@ export class VolumeControl extends LitElement {
|
||||
@click="${this.handlePopupClick}"
|
||||
>
|
||||
<wa-slider
|
||||
label="Volume"
|
||||
orientation="vertical"
|
||||
min="0"
|
||||
max="100"
|
||||
|
||||
@@ -268,6 +268,7 @@ export class JobRow extends LitElement {
|
||||
|
||||
return html`
|
||||
<wa-progress-bar
|
||||
label=${job.title}
|
||||
value=${progressPercent(job) ?? 0}
|
||||
></wa-progress-bar>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
/**
|
||||
* A `wa-slider` is named by its `label`, and its `label` is visible.
|
||||
*
|
||||
* The same trap as `utils/name-dialog.ts`, one component over, and this
|
||||
* one the audit filed under "what is already correct": `a11y.md` lists
|
||||
* `seek-bar` and `volume-control` as exemplary because they pass
|
||||
* `aria-label`. The role is not on the host. Web Awesome renders a
|
||||
* `<div id="slider" role="slider" aria-labelledby="label">` inside its
|
||||
* own shadow root, pointing at an internal `<label id="label">` that is
|
||||
* empty unless the `label` property is set — and `aria-labelledby`
|
||||
* outranks the host's `aria-label`, which the AX tree never sees. Both
|
||||
* sliders in this app computed a name of `""`. Measured with
|
||||
* `Accessibility.getFullAXTree` against the running app, on all eleven
|
||||
* views; `volume-control` did not even have the `aria-label` the audit
|
||||
* credits it with.
|
||||
*
|
||||
* So the name comes from `label`, which is the library's own API, and
|
||||
* this hides it. That is preferred over reaching into the shadow root
|
||||
* the way `name-dialog.ts` has to, for the failure mode: if Web Awesome
|
||||
* renames these parts the label becomes *visible* — wrong-looking and
|
||||
* correctly named — rather than silently nameless again.
|
||||
*
|
||||
* The second rule is not decoration. `#slider` takes an 8px
|
||||
* `margin-block-start` as soon as a label exists, so hiding the label
|
||||
* alone still grows the control from 6px to 14px and moves the transport
|
||||
* bar. `display: none` on the label is deliberate and safe: an element
|
||||
* referenced by `aria-labelledby` contributes its text even when it is
|
||||
* hidden, which is exactly the accname rule this relies on (verified —
|
||||
* the slider reports "Seek" with the label displaying nothing).
|
||||
*/
|
||||
export const waSliderLabel = css`
|
||||
wa-slider::part(label) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
wa-slider::part(slider) {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user