fix(core): derive effective mute state for volume UI components (#753)

This commit is contained in:
rahim
2026-03-05 23:10:25 -08:00
committed by GitHub
parent dce16ae5fd
commit 14bcdc833f
6 changed files with 43 additions and 29 deletions
@@ -68,7 +68,7 @@ export class MuteButtonCore {
getState(): MuteButtonState {
const media = this.#media!;
return {
muted: media.muted,
muted: media.muted || media.volume === 0,
volumeLevel: getVolumeLevel(media),
};
}
@@ -50,6 +50,15 @@ describe('MuteButtonCore', () => {
expect(core.getState().volumeLevel).toBe('off');
});
it('derives muted as true when volume is 0 and not muted', () => {
const core = new MuteButtonCore();
core.setMedia(createMediaState({ volume: 0, muted: false }));
const state = core.getState();
expect(state.muted).toBe(true);
expect(state.volumeLevel).toBe('off');
});
it('returns low when volume < 0.5', () => {
const core = new MuteButtonCore();
core.setMedia(createMediaState({ volume: 0.3 }));
@@ -105,6 +105,16 @@ describe('VolumeSliderCore', () => {
expect(state.muted).toBe(false);
});
it('derives muted as true when volume is 0 and not muted', () => {
const core = new VolumeSliderCore();
core.setInput(createInput());
core.setMedia(createMediaState({ volume: 0, muted: false }));
const state = core.getState();
expect(state.muted).toBe(true);
expect(state.fillPercent).toBe(0);
});
});
describe('getAttrs', () => {
@@ -150,14 +160,14 @@ describe('VolumeSliderCore', () => {
expect(attrs['aria-label']).toBe('Audio');
});
it('shows 0 percent when volume is 0', () => {
it('shows 0 percent muted when volume is 0', () => {
const core = new VolumeSliderCore();
core.setInput(createInput());
core.setMedia(createMediaState({ volume: 0 }));
const state = core.getState();
const attrs = core.getAttrs(state);
expect(attrs['aria-valuetext']).toBe('0 percent');
expect(attrs['aria-valuetext']).toBe('0 percent, muted');
});
});
@@ -40,6 +40,7 @@ export class VolumeSliderCore extends SliderCore {
getState(): VolumeSliderState {
const media = this.#media!;
const { volume, muted } = media;
const effectivelyMuted = muted || volume === 0;
const { dragging, dragPercent } = this.input;
const volumePercent = volume * 100;
const value = dragging ? this.valueFromPercent(dragPercent) : volumePercent;
@@ -47,9 +48,9 @@ export class VolumeSliderCore extends SliderCore {
return {
...base,
fillPercent: muted ? 0 : base.fillPercent,
fillPercent: effectivelyMuted ? 0 : base.fillPercent,
volume,
muted,
muted: effectivelyMuted,
};
}
@@ -114,25 +114,27 @@ describe('volumeFeature', () => {
});
describe('toggleMuted', () => {
it('toggles mute from false to true', async () => {
const video = createMockVideo({ muted: false });
it('mutes when unmuted with volume > 0', async () => {
const video = createMockVideo({ muted: false, volume: 0.8 });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
const result = await store.toggleMuted();
expect(video.muted).toBe(true);
expect(video.volume).toBe(0.8);
expect(result).toBe(true);
});
it('toggles mute from true to false', async () => {
const video = createMockVideo({ muted: true });
it('unmutes when muted with volume > 0', async () => {
const video = createMockVideo({ muted: true, volume: 0.6 });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
const result = await store.toggleMuted();
expect(video.muted).toBe(false);
expect(video.volume).toBe(0.6);
expect(result).toBe(false);
});
@@ -147,26 +149,16 @@ describe('volumeFeature', () => {
expect(video.volume).toBe(0.25);
});
it('preserves volume when unmuting with volume > 0', async () => {
const video = createMockVideo({ muted: true, volume: 0.6 });
it('unmutes and restores volume when volume is 0 and not muted', async () => {
const video = createMockVideo({ muted: false, volume: 0 });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
await store.toggleMuted();
const result = await store.toggleMuted();
expect(video.muted).toBe(false);
expect(video.volume).toBe(0.6);
});
it('does not change volume when muting', async () => {
const video = createMockVideo({ muted: false, volume: 0.8 });
const store = createStore<PlayerTarget>()(volumeFeature);
store.attach({ media: video, container: null });
await store.toggleMuted();
expect(video.muted).toBe(true);
expect(video.volume).toBe(0.8);
expect(video.volume).toBe(0.25);
expect(result).toBe(false);
});
});
});
@@ -28,12 +28,14 @@ export const volumeFeature = definePlayerFeature({
toggleMuted() {
const { media } = target();
const willUnmute = media.muted;
media.muted = !media.muted;
const effectivelyMuted = media.muted || media.volume === 0;
// Restore a sensible volume when unmuting at zero.
if (willUnmute && media.volume === 0) {
media.volume = UNMUTE_VOLUME;
if (effectivelyMuted) {
media.muted = false;
// Restore a sensible volume when unmuting at zero.
if (media.volume === 0) media.volume = UNMUTE_VOLUME;
} else {
media.muted = true;
}
return media.muted;