diff --git a/apps/client/src/views/teleprompter/Teleprompter.tsx b/apps/client/src/views/teleprompter/Teleprompter.tsx index c2f71ea2a..747576f37 100644 --- a/apps/client/src/views/teleprompter/Teleprompter.tsx +++ b/apps/client/src/views/teleprompter/Teleprompter.tsx @@ -161,7 +161,7 @@ function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterDa void; @@ -23,11 +25,16 @@ interface ControlOverlayProps { * Visibility follows the same rule as the rest of Ontime's floating chrome: it * fades once the operator stops moving, so it leaves the talent's eyeline * without needing to know whether the script happens to be rolling. + * + * Every control is present for the life of the view and goes disabled when it + * has nothing to do. A control which comes and goes moves every control after + * it, so the operator reaches for pause during a show and presses whatever slid + * under their finger instead. */ export default function ControlOverlay({ isRunning, speed, - followLocked, + canReengageFollow, atEnd, controller, onToggleHelp, @@ -50,47 +57,67 @@ export default function ControlOverlay({ return (
- + } > {isRunning ? : } - + - controller.changeSpeed(-SPEED_STEP))} - aria-label='Slow down' + controller.changeSpeed(-SPEED_STEP))} + aria-label='Slow down' + /> + } > - +
{speed} lpm
- controller.changeSpeed(SPEED_STEP))} - aria-label='Speed up' + controller.changeSpeed(SPEED_STEP))} + aria-label='Speed up' + /> + } > - + - controller.rewind())} - aria-label='Rewind to top' + controller.rewind())} + aria-label='Rewind to top' + /> + } > - + {/* The operator view solves this with its own FollowButton, a labelled pill @@ -98,21 +125,37 @@ export default function ControlOverlay({ two would land on top of each other. Same icon and same job, folded into the transport rather than floating separately. */} - {followLocked && ( - - - - )} + + } + > + + - + + } + > - +
); } diff --git a/e2e/tests/features/215-teleprompter.spec.ts b/e2e/tests/features/215-teleprompter.spec.ts index 73a877e25..e612f223c 100644 --- a/e2e/tests/features/215-teleprompter.spec.ts +++ b/e2e/tests/features/215-teleprompter.spec.ts @@ -116,6 +116,27 @@ test.describe('teleprompter', () => { expect(slower).toBe(before); }); + test('keeps every transport control in place', async ({ page }) => { + // a control which appears mid show moves the ones after it, and the operator + // reaches for pause and presses whatever slid under their finger instead + await page.goto(teleprompterUrl); + await expect(scroller(page)).toBeVisible(); + + const controls = page.locator('.teleprompter__controls button'); + const follow = page.getByTestId('teleprompter-follow'); + + await expect(controls).toHaveCount(6); + await expect(follow).toBeVisible(); + await expect(follow).toBeDisabled(); + + // taking over by hand offers the follow back, without restacking the row + await page.mouse.move(640, 300); + await page.mouse.wheel(0, 200); + + await expect(controls).toHaveCount(6); + await expect(follow).toBeVisible(); + }); + test('f mirrors the view for a beam splitter rig', async ({ page }) => { await page.goto(teleprompterUrl); await expect(scroller(page)).toBeVisible();