diff --git a/src/elements/ia-playback-controls/assets/next-section.ts b/src/elements/ia-playback-controls/assets/next-section.ts index 8e935ac5..2521dc00 100644 --- a/src/elements/ia-playback-controls/assets/next-section.ts +++ b/src/elements/ia-playback-controls/assets/next-section.ts @@ -1,13 +1,16 @@ /** * @file Jump to next section icon * - * Inline so the button can recolor it with `currentColor`, which is not - * possible on the contents of an ``. + * Decorative: the button around it carries the accessible name, so the icon + * is hidden from assistive tech. Inline so the button can recolor it with + * `currentColor`, which is not possible on the contents of an ``. */ import { svg } from 'lit'; export default svg` - +`. + * Decorative: the button around it carries the accessible name, so the icon + * is hidden from assistive tech. Inline so the button can recolor it with + * `currentColor`, which is not possible on the contents of an ``. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - +`. + * Decorative: the button around it carries the accessible name, so the icon + * is hidden from assistive tech. Inline so the button can recolor it with + * `currentColor`, which is not possible on the contents of an ``. */ import { svg } from 'lit'; export default svg` - +`. + * Decorative: the button around it carries the accessible name, so the icon + * is hidden from assistive tech. Inline so the button can recolor it with + * `currentColor`, which is not possible on the contents of an ``. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - +`. */ import { svg } from 'lit'; export default svg` - + @@ -174,7 +186,25 @@ export class IAPlaybackControls extends LitElement { * label beside the button carries the "x". */ private get playbackRateLabel(): string { - return msg(str`Playback speed, currently ${this.playbackRate}`); + return msg(str`Playback speed, currently ${this.formattedPlaybackRate}`); + } + + /** + * The playback rate written for the reader's locale, so the quarter steps + * use whatever decimal separator they expect rather than always a point. + * + * `playbackRate` is a public property, so it can arrive as something that + * isn't a usable number. Everything that displays or announces the rate + * reads it through here, so `NaN` never reaches the screen reader. Rates + * outside the range the button steps through are shown as they are: they + * are still rates a media element will play at. + */ + private get formattedPlaybackRate(): string { + const rate = Number.isFinite(this.playbackRate) + ? this.playbackRate + : DEFAULT_PLAYBACK_RATE; + + return formatPlaybackRate(rate); } /** diff --git a/src/elements/ia-playback-controls/playback-rate-formatter.test.ts b/src/elements/ia-playback-controls/playback-rate-formatter.test.ts new file mode 100644 index 00000000..486ec420 --- /dev/null +++ b/src/elements/ia-playback-controls/playback-rate-formatter.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from 'vitest'; + +import { formatPlaybackRate } from './playback-rate-formatter'; + +describe('formatPlaybackRate', () => { + test('uses the locale’s decimal separator', () => { + expect(formatPlaybackRate(1.5, 'en-US')).to.equal('1.5'); + expect(formatPlaybackRate(1.5, 'de-DE')).to.equal('1,5'); + expect(formatPlaybackRate(0.75, 'de-DE')).to.equal('0,75'); + }); + + test('leaves whole rates without a decimal part', () => { + expect(formatPlaybackRate(1, 'en-US')).to.equal('1'); + expect(formatPlaybackRate(2, 'de-DE')).to.equal('2'); + }); +}); diff --git a/src/elements/ia-playback-controls/playback-rate-formatter.ts b/src/elements/ia-playback-controls/playback-rate-formatter.ts new file mode 100644 index 00000000..73dd9fe1 --- /dev/null +++ b/src/elements/ia-playback-controls/playback-rate-formatter.ts @@ -0,0 +1,10 @@ +/** + * Formats a playback rate for display. + * + * The rate moves in quarter steps, and the decimal separator for those varies + * by locale, so the number goes through `Intl` rather than straight into a + * template. Omitting the locale uses the reader's own. + */ +export function formatPlaybackRate(rate: number, locale?: string): string { + return new Intl.NumberFormat(locale).format(rate); +} diff --git a/src/elements/index.ts b/src/elements/index.ts index 16ee6fe6..e9c7be6e 100644 --- a/src/elements/index.ts +++ b/src/elements/index.ts @@ -13,6 +13,7 @@ export * from './ia-item-navigator/menus/ia-itemnav-sort-files-button'; export * from './ia-item-navigator/menus/ia-itemnav-share-panel'; export * from './ia-playback-controls/ia-playback-controls'; export * from './ia-playback-controls/models'; +export * from './ia-playback-controls/playback-rate-formatter'; export * from './ia-radio-player/ia-radio-player'; export * from './ia-radio-player/ia-search-results-switcher'; export * from './ia-radio-player/models';