PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / js / components / ManageMenu / BlueprintsDemo / useBlueprintsDemo.ts

useBlueprintsDemo.ts in Code Snippets 4.0.0-beta.2, at js/components/ManageMenu/BlueprintsDemo/useBlueprintsDemo.ts

146 lines 3.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2 import { DEMO_SECTIONS } from './demoBlueprint'
3 import type { DemoStage } from './types'
4
5 /**
6 * How long each stage holds before the driver advances. Each stage carries one
7 * step of commentary, so it is given long enough to read the callout and take
8 * in the section that changed.
9 */
10 const STAGE_DURATIONS: Partial<Record<DemoStage, number>> = {
11 general: 4500,
12 attributes: 4500,
13 output: 4800,
14 generating: 3600,
15 generated: 3000
16 }
17
18 const DEFAULT_DURATION = 3000
19
20 /** Uniform pause between stages when the visitor has asked for reduced motion. */
21 const REDUCED_MOTION_DURATION = 250
22
23 /** How long the panel spends faded out while its section is swapped. */
24 const FADE_DURATION = 220
25
26 const FOLLOW_UP: Partial<Record<DemoStage, DemoStage>> = {
27 general: 'attributes',
28 attributes: 'output',
29 output: 'generating',
30 generating: 'generated',
31 generated: 'finished'
32 }
33
34 const prefersReducedMotion = (): boolean =>
35 window.matchMedia('(prefers-reduced-motion: reduce)').matches
36
37 export interface BlueprintsDemoState {
38 stage: DemoStage
39 activeSection: string
40 /** Whether the panel is mid-crossfade between two sections. */
41 isFading: boolean
42 /** Whether the visitor may pick sections themselves. */
43 sectionsBrowsable: boolean
44 hasStarted: boolean
45 isFinished: boolean
46 reducedMotion: boolean
47 selectSection: (id: string) => void
48 play: VoidFunction
49 skip: VoidFunction
50 replay: VoidFunction
51 }
52
53 // eslint-disable-next-line max-lines-per-function -- the timeline driver and its teardown belong together.
54 export const useBlueprintsDemo = (): BlueprintsDemoState => {
55 const reducedMotion = useMemo(prefersReducedMotion, [])
56
57 const [stage, setStage] = useState<DemoStage>('idle')
58 const [activeSection, setActiveSection] = useState(DEMO_SECTIONS[0].id)
59 const [isFading, setIsFading] = useState(false)
60
61 const timers = useRef<number[]>([])
62
63 const clearTimers = useCallback(() => {
64 timers.current.forEach(window.clearTimeout)
65 timers.current = []
66 }, [])
67
68 useEffect(() => () => {
69 timers.current.forEach(window.clearTimeout)
70 }, [])
71
72 // Swapping the panel's contents outright reads as a jump cut, so the panel
73 // fades out, changes section, and fades back in.
74 const crossfadeTo = useCallback((id: string) => {
75 setActiveSection(previous => {
76 if (previous === id) {
77 return previous
78 }
79
80 setIsFading(true)
81 timers.current.push(window.setTimeout(() => {
82 setActiveSection(id)
83 setIsFading(false)
84 }, reducedMotion ? 0 : FADE_DURATION))
85
86 return previous
87 })
88 }, [reducedMotion])
89
90 const advance = useCallback((next: DemoStage) => {
91 setStage(next)
92
93 // The first three stages are the section tabs themselves, so entering
94 // one selects it.
95 if (DEMO_SECTIONS.some(section => section.id === next)) {
96 crossfadeTo(next)
97 }
98
99 const upcoming = FOLLOW_UP[next]
100
101 if (!upcoming) {
102 return
103 }
104
105 const delay = reducedMotion
106 ? REDUCED_MOTION_DURATION
107 : STAGE_DURATIONS[next] ?? DEFAULT_DURATION
108
109 timers.current.push(window.setTimeout(() => advance(upcoming), delay))
110 }, [crossfadeTo, reducedMotion])
111
112 const play = useCallback(() => {
113 clearTimers()
114 advance('general')
115 }, [advance, clearTimers])
116
117 const replay = useCallback(() => {
118 clearTimers()
119 setStage('idle')
120 setIsFading(false)
121 setActiveSection(DEMO_SECTIONS[0].id)
122 timers.current.push(window.setTimeout(() => advance('general'), REDUCED_MOTION_DURATION))
123 }, [advance, clearTimers])
124
125 const skip = useCallback(() => {
126 clearTimers()
127 setStage('finished')
128 setIsFading(false)
129 setActiveSection(DEMO_SECTIONS[DEMO_SECTIONS.length - 1].id)
130 }, [clearTimers])
131
132 return {
133 stage,
134 activeSection,
135 isFading,
136 sectionsBrowsable: 'finished' === stage,
137 hasStarted: 'idle' !== stage,
138 isFinished: 'finished' === stage,
139 reducedMotion,
140 selectSection: crossfadeTo,
141 play,
142 skip,
143 replay
144 }
145 }
146