* fix(rsvp): restore in-flow control bar layout reverted by #4589 PR #4585 fixed the mobile RSVP control bar overlap by laying the audio toggle and settings gear in a single in-flow flex row flanking the centered transport. PR #4589 branched from main about five minutes before #4585 merged and merged about ten hours later without rebasing, so its squash carried the stale pre-#4585 file and reverted the entire fix, including the regression test #4585 had added. On narrow phones (360px) the audio and settings icons again overlapped the right end of the transport, hiding the "skip forward 15" control. Restore the #4585 layout and re-add a structural guard test asserting the audio toggle and settings share the transport row and live in no absolutely positioned cluster. Verified on a Xiaomi 13 (360px) via on-device CDP: no overlap, play button stays centered. Also stage the project-memory note for this regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rsvp): hide Faster/Slower buttons at 350px or below On very narrow phones (width 350px or less) the control row has no room for every control. Collapse the Faster/Slower speed buttons via a max-[350px]:hidden variant (matching the existing 350px tightening tier) so the transport, audio toggle and settings never overflow. Speed stays adjustable from the WPM dropdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -529,6 +529,75 @@ describe('RSVPOverlay — dictionary lookup (#4475)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('RSVPOverlay — playback control layout (#4585, regressed by #4589)', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
// The audio (TTS) toggle and the settings gear must sit in the SAME flex row
|
||||
// as the transport buttons, flanking the centered play button in normal flow.
|
||||
// The earlier `absolute end-0` cluster overlaid them on top of the right end
|
||||
// of the transport, hiding the audio button behind "skip forward 15" on narrow
|
||||
// phones. Keeping all three as siblings of the play button is what prevents the
|
||||
// overlap, so assert the structure here (jsdom can't measure the overlap).
|
||||
test('audio toggle and settings flank the transport in the same flex row', () => {
|
||||
const state = buildState({
|
||||
words: [{ text: 'hello', orpIndex: 1, pauseMultiplier: 1 }],
|
||||
currentIndex: 0,
|
||||
});
|
||||
const { container } = renderOverlay(state);
|
||||
|
||||
const audioButton = container.querySelector('[aria-label="Play audio"]') as HTMLElement;
|
||||
const settingsButton = container.querySelector('[aria-label="Settings"]') as HTMLElement;
|
||||
const playButton = container.querySelector('[aria-label="Play"]') as HTMLElement;
|
||||
expect(audioButton).not.toBeNull();
|
||||
expect(settingsButton).not.toBeNull();
|
||||
expect(playButton).not.toBeNull();
|
||||
|
||||
// All three share the play button's parent (the single flex row) — the audio
|
||||
// toggle and settings are not tucked into a separate absolute cluster.
|
||||
expect(audioButton.parentElement).toBe(playButton.parentElement);
|
||||
expect(settingsButton.parentElement).toBe(playButton.parentElement);
|
||||
});
|
||||
|
||||
// On very narrow phones (< 350px) the row has no room for every control, so
|
||||
// the Faster/Slower speed buttons collapse to save space (speed is still
|
||||
// adjustable from the WPM dropdown). The core transport stays put.
|
||||
test('hides the Faster/Slower buttons below 350px to save space', () => {
|
||||
const state = buildState({
|
||||
words: [{ text: 'hello', orpIndex: 1, pauseMultiplier: 1 }],
|
||||
currentIndex: 0,
|
||||
});
|
||||
const { container } = renderOverlay(state);
|
||||
|
||||
const decrease = container.querySelector('[aria-label="Decrease speed"]') as HTMLElement;
|
||||
const increase = container.querySelector('[aria-label="Increase speed"]') as HTMLElement;
|
||||
const play = container.querySelector('[aria-label="Play"]') as HTMLElement;
|
||||
const audio = container.querySelector('[aria-label="Play audio"]') as HTMLElement;
|
||||
const settings = container.querySelector('[aria-label="Settings"]') as HTMLElement;
|
||||
|
||||
expect(decrease.className).toContain('max-[350px]:hidden');
|
||||
expect(increase.className).toContain('max-[350px]:hidden');
|
||||
// Transport, audio toggle and settings must remain visible at any width.
|
||||
expect(play.className).not.toContain('max-[350px]:hidden');
|
||||
expect(audio.className).not.toContain('max-[350px]:hidden');
|
||||
expect(settings.className).not.toContain('max-[350px]:hidden');
|
||||
});
|
||||
|
||||
// The previous fix relied on absolute positioning being gone; guard against it
|
||||
// sneaking back into the row that holds the transport controls.
|
||||
test('the playback control row is not absolutely positioned', () => {
|
||||
const state = buildState({
|
||||
words: [{ text: 'hello', orpIndex: 1, pauseMultiplier: 1 }],
|
||||
currentIndex: 0,
|
||||
});
|
||||
const { container } = renderOverlay(state);
|
||||
|
||||
const playButton = container.querySelector('[aria-label="Play"]') as HTMLElement;
|
||||
const audioButton = container.querySelector('[aria-label="Play audio"]') as HTMLElement;
|
||||
expect(playButton.parentElement!.className).not.toContain('absolute');
|
||||
expect(audioButton.parentElement!.className).not.toContain('absolute');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RSVPOverlay — start delay setting (#4478)', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
|
||||
Reference in New Issue
Block a user