(
+ html``,
+ );
+
+ expect(
+ buttonIn(el, 'playback-rate-btn').getAttribute('aria-label'),
+ ).to.equal('Playback speed, currently 1');
+ expect(el.shadowRoot?.textContent).to.include('1x');
+ });
});
diff --git a/src/elements/ia-playback-controls/ia-playback-controls.ts b/src/elements/ia-playback-controls/ia-playback-controls.ts
index acce0108..4bd0dc31 100644
--- a/src/elements/ia-playback-controls/ia-playback-controls.ts
+++ b/src/elements/ia-playback-controls/ia-playback-controls.ts
@@ -8,6 +8,7 @@ import {
import { customElement, property } from 'lit/decorators.js';
import { msg, str } from '@lit/localize';
+import { formatPlaybackRate } from './playback-rate-formatter';
import { PlaybackMode } from './models';
import nextSectionIcon from './assets/next-section';
@@ -34,11 +35,22 @@ const Events = {
NextSectionButtonPressed: 'next-section-button-pressed',
};
+/** The rate the controls start at, and the fallback for an unusable one */
+const DEFAULT_PLAYBACK_RATE = 1;
+
/** How much one press of the speed button moves the playback rate */
const PLAYBACK_RATE_STEP = 0.25;
-/** The rate the speed button wraps back around to once it passes the top */
+/**
+ * The slowest rate the speed button offers, and the one it wraps back around
+ * to once it passes the top.
+ */
const MIN_PLAYBACK_RATE = 0.5;
+
+/**
+ * The fastest rate the speed button offers. Pressing it here is what sends the
+ * rate back round to the slowest.
+ */
const MAX_PLAYBACK_RATE = 2;
/** How much one press of the volume button moves the volume */
@@ -57,7 +69,7 @@ export class IAPlaybackControls extends LitElement {
@property({ type: String }) playbackMode: PlaybackMode = PlaybackMode.paused;
/** Playback speed multiplier, where 1 is normal speed */
- @property({ type: Number }) playbackRate = 1;
+ @property({ type: Number }) playbackRate = DEFAULT_PLAYBACK_RATE;
/** Playback volume, from 0 (muted) to 1 (full) */
@property({ type: Number }) volume = 1;
@@ -77,7 +89,7 @@ export class IAPlaybackControls extends LitElement {
- ${this.playbackRate}x
+ ${this.formattedPlaybackRate}x
@@ -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';