Skip to content

Commit 2000276

Browse files
committed
Autoplay runnable docs examples when visible
1 parent 55f2b99 commit 2000276

5 files changed

Lines changed: 360 additions & 24 deletions

File tree

src/components/charts/ChartsCatalogDocExample.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function ChartsCatalogDocExampleClient({
7676
<div
7777
className="overflow-hidden rounded-lg border border-border-default bg-background-default"
7878
data-chart-example={caseId}
79+
data-chart-example-state="running"
7980
>
8081
{source !== 'hidden' ? (
8182
<div className="flex min-h-10 items-center justify-between gap-3 border-b border-border-default px-2 pl-3">

src/components/charts/ChartsCatalogDocExample.tsx

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ClientOnly } from '@tanstack/react-router'
2+
import { Hydrate } from '@tanstack/react-start'
3+
import { visible } from '@tanstack/react-start/hydration'
24
import * as React from 'react'
35
import { ChartsCatalogPreview } from './ChartsCatalogPreview'
46
import { Button } from '~/components/ds/ui'
@@ -10,6 +12,34 @@ const LazyChartsCatalogDocExample = React.lazy(() =>
1012
)
1113

1214
export function ChartsCatalogDocExample({
15+
...props
16+
}: {
17+
caseId: string
18+
height: number
19+
source?: 'hidden' | 'collapsed' | 'expanded'
20+
title?: string
21+
}) {
22+
return (
23+
<Hydrate
24+
key={props.caseId}
25+
when={visible({ rootMargin: '600px 0px' })}
26+
fallback={
27+
<section className="not-prose my-5">
28+
<ChartsCatalogDocExampleFallback
29+
caseId={props.caseId}
30+
height={props.height}
31+
source={props.source ?? 'collapsed'}
32+
title={props.title}
33+
/>
34+
</section>
35+
}
36+
>
37+
<ChartsCatalogDocExampleIsland {...props} />
38+
</Hydrate>
39+
)
40+
}
41+
42+
function ChartsCatalogDocExampleIsland({
1343
caseId,
1444
height,
1545
source = 'collapsed',
@@ -24,20 +54,138 @@ export function ChartsCatalogDocExample({
2454
caseId: string
2555
edit: boolean
2656
}>()
57+
const activatedCaseRef = React.useRef<string | undefined>(undefined)
58+
const exampleRef = React.useRef<HTMLElement>(null)
2759
const isActive = activation?.caseId === caseId
60+
const activateRun = React.useCallback(() => {
61+
if (activatedCaseRef.current === caseId) return
62+
activatedCaseRef.current = caseId
63+
setActivation({ caseId, edit: false })
64+
}, [caseId])
65+
const activateEdit = React.useCallback(() => {
66+
activatedCaseRef.current = caseId
67+
setActivation({ caseId, edit: true })
68+
}, [caseId])
69+
70+
React.useEffect(() => {
71+
if (isActive) return
72+
if (!exampleRef.current) return
73+
const element = exampleRef.current
74+
75+
let intersecting = false
76+
let dwellHandle: number | undefined
77+
let idleHandle: number | undefined
78+
let timeoutHandle: number | undefined
79+
let disposed = false
80+
let interactionBlocked = false
81+
82+
function cancelScheduledActivation() {
83+
if (dwellHandle !== undefined) {
84+
window.clearTimeout(dwellHandle)
85+
dwellHandle = undefined
86+
}
87+
if (idleHandle !== undefined) {
88+
window.cancelIdleCallback(idleHandle)
89+
idleHandle = undefined
90+
}
91+
if (timeoutHandle !== undefined) {
92+
window.clearTimeout(timeoutHandle)
93+
timeoutHandle = undefined
94+
}
95+
}
96+
97+
function canActivate() {
98+
const rect = element.getBoundingClientRect()
99+
return (
100+
intersecting &&
101+
rect.bottom > 0 &&
102+
rect.right > 0 &&
103+
rect.top < window.innerHeight &&
104+
rect.left < window.innerWidth &&
105+
!interactionBlocked &&
106+
document.visibilityState === 'visible' &&
107+
!element.contains(document.activeElement)
108+
)
109+
}
110+
111+
function scheduleActivation() {
112+
cancelScheduledActivation()
113+
if (disposed || !canActivate()) return
114+
115+
dwellHandle = window.setTimeout(() => {
116+
dwellHandle = undefined
117+
if (disposed || !canActivate()) return
118+
119+
const run = () => {
120+
idleHandle = undefined
121+
timeoutHandle = undefined
122+
if (!disposed && canActivate()) activateRun()
123+
}
124+
125+
if (typeof window.requestIdleCallback === 'function') {
126+
idleHandle = window.requestIdleCallback(run, { timeout: 1_700 })
127+
} else {
128+
timeoutHandle = window.setTimeout(run)
129+
}
130+
}, 300)
131+
}
132+
133+
const observer = new IntersectionObserver(([entry]) => {
134+
intersecting = Boolean(
135+
entry?.isIntersecting && entry.intersectionRatio > 0,
136+
)
137+
if (intersecting) scheduleActivation()
138+
else cancelScheduledActivation()
139+
})
140+
observer.observe(element)
141+
142+
function handleVisibilityChange() {
143+
if (document.visibilityState === 'visible') scheduleActivation()
144+
else cancelScheduledActivation()
145+
}
146+
147+
function handleFocusIn() {
148+
cancelScheduledActivation()
149+
}
150+
151+
function handleFocusOut() {
152+
queueMicrotask(scheduleActivation)
153+
}
154+
155+
function handleInteraction() {
156+
interactionBlocked = true
157+
cancelScheduledActivation()
158+
}
159+
160+
document.addEventListener('visibilitychange', handleVisibilityChange)
161+
element.addEventListener('focusin', handleFocusIn)
162+
element.addEventListener('focusout', handleFocusOut)
163+
element.addEventListener('pointerdown', handleInteraction)
164+
165+
return () => {
166+
disposed = true
167+
cancelScheduledActivation()
168+
observer.disconnect()
169+
document.removeEventListener('visibilitychange', handleVisibilityChange)
170+
element.removeEventListener('focusin', handleFocusIn)
171+
element.removeEventListener('focusout', handleFocusOut)
172+
element.removeEventListener('pointerdown', handleInteraction)
173+
}
174+
}, [activateRun, isActive])
175+
28176
const fallback = (
29177
<ChartsCatalogDocExampleFallback
30178
caseId={caseId}
31179
height={height}
32-
onEdit={() => setActivation({ caseId, edit: true })}
33-
onRun={() => setActivation({ caseId, edit: false })}
180+
onEdit={activateEdit}
181+
onRun={activateRun}
34182
source={source}
35183
title={title}
36184
/>
37185
)
38186

39187
return (
40-
<section className="not-prose my-5">
188+
<section ref={exampleRef} className="not-prose my-5">
41189
{isActive ? (
42190
<ClientOnly fallback={fallback}>
43191
<React.Suspense fallback={fallback}>
@@ -68,8 +216,8 @@ function ChartsCatalogDocExampleFallback({
68216
}: {
69217
caseId: string
70218
height: number
71-
onEdit: () => void
72-
onRun: () => void
219+
onEdit?: () => void
220+
onRun?: () => void
73221
source: 'hidden' | 'collapsed' | 'expanded'
74222
title?: string
75223
}) {
@@ -79,6 +227,7 @@ function ChartsCatalogDocExampleFallback({
79227
<div
80228
className="overflow-hidden rounded-lg border border-border-default bg-background-default"
81229
data-chart-example={caseId}
230+
data-chart-example-state="static"
82231
>
83232
<div
84233
className={`flex min-h-10 items-center gap-3 border-b border-border-default px-2 ${source === 'hidden' ? 'justify-end' : 'justify-between pl-3'}`}
@@ -90,11 +239,23 @@ function ChartsCatalogDocExampleFallback({
90239
) : null}
91240
<div className="flex shrink-0 items-center gap-1">
92241
{source !== 'hidden' ? (
93-
<Button type="button" variant="ghost" size="xs" onClick={onEdit}>
242+
<Button
243+
type="button"
244+
variant="ghost"
245+
size="xs"
246+
disabled={!onEdit}
247+
onClick={onEdit}
248+
>
94249
Edit
95250
</Button>
96251
) : null}
97-
<Button type="button" variant="primary" size="xs" onClick={onRun}>
252+
<Button
253+
type="button"
254+
variant="primary"
255+
size="xs"
256+
disabled={!onRun}
257+
onClick={onRun}
258+
>
98259
Run
99260
</Button>
100261
</div>

0 commit comments

Comments
 (0)