Skip to content

Commit 93fb40f

Browse files
authored
fix(examples/chat): keep reading pauses in the hero walkthrough under reduced motion (#979)
prefers-reduced-motion previously zeroed every scripted pause, so the first prompt was typed and sent in the same tick and a reduced-motion visitor never got to read it. Reduced motion should remove animation, not reading time. typeInto now waits READ_PAUSE_MS (1200ms) after the instant set; moveCursor holds for READ_PAUSE_MS / 2 (600ms) instead of 0. press stays instant.
1 parent 471855d commit 93fb40f

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

‎examples/chat/angular/src/app/hero/hero-mode.component.spec.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ describe('HeroMode', () => {
137137
expect(fx.componentInstance.mode()).toBe('live');
138138
});
139139

140+
it('reduced motion: typeInto sets the value instantly but keeps a reading pause before resolving', async () => {
141+
fx.componentInstance.reducedMotion = true;
142+
const el = fx.nativeElement as HTMLElement;
143+
const textarea = el.querySelector<HTMLTextAreaElement>('textarea[aria-label="Type a message"]')!;
144+
145+
const started = performance.now();
146+
const typing = fx.componentInstance.typeInto('abc');
147+
// Instant: the value is set synchronously, before any timer fires.
148+
expect(textarea.value).toBe('abc');
149+
150+
await typing;
151+
expect(performance.now() - started).toBeGreaterThanOrEqual(1200);
152+
});
153+
154+
it('reduced motion: moveCursor holds for a reading pause instead of resolving instantly', async () => {
155+
fx.componentInstance.reducedMotion = true;
156+
157+
const started = performance.now();
158+
await fx.componentInstance.moveCursor('composer');
159+
expect(performance.now() - started).toBeGreaterThanOrEqual(600);
160+
});
161+
140162
it('clears the half-typed composer on takeover', async () => {
141163
const el = fx.nativeElement as HTMLElement;
142164
await fx.componentInstance.typeInto('hello');

‎examples/chat/angular/src/app/hero/hero-mode.component.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ import { HeroScriptRunner, type CursorTarget, type HeroScriptHost } from './hero
4747

4848
export type HeroModeKind = 'replay' | 'live';
4949
const TYPE_DELAY_MS = 40;
50+
/**
51+
* Reduced motion removes animation, not reading time: a visitor who never
52+
* sees the cursor glide or the prompt type out still needs a moment to read
53+
* what just appeared before the walkthrough moves on.
54+
*/
55+
const READ_PAUSE_MS = 1200;
5056

5157
/**
5258
* The live agent's thread id, held per component instance (NOT at module
@@ -216,7 +222,8 @@ export class HeroMode implements HeroScriptHost {
216222
typeof window === 'undefined'
217223
? { postState: () => undefined, onVisibility: () => () => undefined }
218224
: browserHeroBridge();
219-
readonly reducedMotion =
225+
/** TEST SEAM. Overridable the same way `bridge` is, instead of mocking matchMedia globally. */
226+
reducedMotion =
220227
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
221228

222229
private runner: HeroScriptRunner | null = null;
@@ -389,9 +396,13 @@ export class HeroMode implements HeroScriptHost {
389396

390397
async typeInto(text: string): Promise<void> {
391398
// Deliberately does NOT focus the textarea: that would trip the takeover.
392-
await this.driving(() =>
393-
typeIntoTextarea(composerOf(this.surface()), text, TYPE_DELAY_MS, this.reducedMotion),
394-
);
399+
await this.driving(async () => {
400+
await typeIntoTextarea(composerOf(this.surface()), text, TYPE_DELAY_MS, this.reducedMotion);
401+
// Reduced motion writes the whole prompt in one tick; without a pause
402+
// here it would be typed and sent in the same tick and a reduced-motion
403+
// visitor would never get to read it.
404+
if (this.reducedMotion) await sleep(READ_PAUSE_MS);
405+
});
395406
}
396407

397408
async send(): Promise<void> {
@@ -420,7 +431,9 @@ export class HeroMode implements HeroScriptHost {
420431
this.cursorX.set(point.x);
421432
this.cursorY.set(point.y);
422433
this.cursorVisible.set(true);
423-
await sleep(this.reducedMotion ? 0 : 650);
434+
// Reduced motion skips the glide (the cursor jumps) but still holds
435+
// briefly so the target is readable before the next scripted action.
436+
await sleep(this.reducedMotion ? READ_PAUSE_MS / 2 : 650);
424437
}
425438

426439
hasInterrupt(): boolean {

0 commit comments

Comments
 (0)