1
- import React , { useEffect , useMemo , useRef , useState } from 'react' ;
1
+ import React , { RefObject , forwardRef , useEffect , useMemo , useRef , useState } from 'react' ;
2
2
3
3
import useBaseUrl from '@docusaurus/useBaseUrl' ;
4
4
import './playground.css' ;
@@ -13,52 +13,64 @@ import TabItem from '@theme/TabItem';
13
13
14
14
import { IconHtml , IconTs , IconVue , IconDefault , IconCss , IconDots } from './icons' ;
15
15
16
- const ControlButton = ( {
17
- isSelected,
18
- handleClick,
19
- title,
20
- label,
21
- disabled,
22
- } : {
23
- isSelected : boolean ;
24
- handleClick : ( ) => void ;
25
- title : string ;
26
- label : string ;
27
- disabled ?: boolean ;
28
- } ) => {
29
- const controlButton = (
30
- < button
31
- title = { disabled ? undefined : title }
32
- disabled = { disabled }
33
- className = { `playground__control-button ${ isSelected ? 'playground__control-button--selected' : '' } ` }
34
- onClick = { handleClick }
35
- data-text = { label }
36
- >
37
- { label }
38
- </ button >
39
- ) ;
40
- if ( disabled ) {
41
- return (
42
- < Tippy theme = "playground" arrow = { false } placement = "bottom" content = { `Unavailable for ${ label } ` } >
43
- { /* Tippy requires a wrapper element for disabled elements: https://atomiks.github.io/tippyjs/v5/creating-tooltips/#disabled-elements */ }
44
- < div > { controlButton } </ div >
45
- </ Tippy >
16
+ import { useScrollPositionBlocker } from '@docusaurus/theme-common' ;
17
+ import useIsBrowser from '@docusaurus/useIsBrowser' ;
18
+
19
+ const ControlButton = forwardRef (
20
+ (
21
+ {
22
+ isSelected,
23
+ handleClick,
24
+ title,
25
+ label,
26
+ disabled,
27
+ } : {
28
+ isSelected : boolean ;
29
+ handleClick : ( ) => void ;
30
+ title : string ;
31
+ label : string ;
32
+ disabled ?: boolean ;
33
+ } ,
34
+ ref : RefObject < HTMLButtonElement >
35
+ ) => {
36
+ const controlButton = (
37
+ < button
38
+ title = { disabled ? undefined : title }
39
+ disabled = { disabled }
40
+ className = { `playground__control-button ${ isSelected ? 'playground__control-button--selected' : '' } ` }
41
+ onClick = { handleClick }
42
+ data-text = { label }
43
+ ref = { ref }
44
+ >
45
+ { label }
46
+ </ button >
46
47
) ;
48
+ if ( disabled ) {
49
+ return (
50
+ < Tippy theme = "playground" arrow = { false } placement = "bottom" content = { `Unavailable for ${ label } ` } >
51
+ { /* Tippy requires a wrapper element for disabled elements: https://atomiks.github.io/tippyjs/v5/creating-tooltips/#disabled-elements */ }
52
+ < div > { controlButton } </ div >
53
+ </ Tippy >
54
+ ) ;
55
+ }
56
+ return controlButton ;
47
57
}
48
- return controlButton ;
49
- } ;
58
+ ) ;
50
59
51
- const CodeBlockButton = ( { language, usageTarget, setUsageTarget, disabled } ) => {
60
+ const CodeBlockButton = ( { language, usageTarget, setAndSaveUsageTarget, disabled } ) => {
61
+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
52
62
const langValue = UsageTarget [ language ] ;
63
+
53
64
return (
54
65
< ControlButton
55
66
isSelected = { usageTarget === langValue }
56
67
handleClick = { ( ) => {
57
- setUsageTarget ( langValue ) ;
68
+ setAndSaveUsageTarget ( langValue , buttonRef . current ) ;
58
69
} }
59
70
title = { `Show ${ language } code` }
60
71
label = { language }
61
72
disabled = { disabled }
73
+ ref = { buttonRef }
62
74
/>
63
75
) ;
64
76
} ;
@@ -158,21 +170,66 @@ export default function Playground({
158
170
159
171
const { isDarkTheme } = useThemeContext ( ) ;
160
172
173
+ /**
174
+ * When deploying, Docusaurus builds the app in an SSR environment.
175
+ * We need to check whether we're in a browser so we know if we can
176
+ * use the window or localStorage objects.
177
+ */
178
+ const isBrowser = useIsBrowser ( ) ;
179
+
161
180
const hostRef = useRef < HTMLDivElement | null > ( null ) ;
162
181
const codeRef = useRef ( null ) ;
163
182
const frameiOS = useRef < HTMLIFrameElement | null > ( null ) ;
164
183
const frameMD = useRef < HTMLIFrameElement | null > ( null ) ;
165
184
const consoleBodyRef = useRef < HTMLDivElement | null > ( null ) ;
166
185
167
- const defaultMode = typeof mode !== 'undefined' ? mode : Mode . iOS ;
186
+ const { blockElementScrollPositionUntilNextRender } = useScrollPositionBlocker ( ) ;
187
+
188
+ const getDefaultMode = ( ) => {
189
+ /**
190
+ * If a custom mode was specified, use that.
191
+ */
192
+ if ( mode ) return mode ;
193
+
194
+ /**
195
+ * Otherwise, if there is a saved mode from previously clicking
196
+ * the mode button, use that.
197
+ */
198
+ if ( isBrowser ) {
199
+ const storedMode = localStorage . getItem ( MODE_STORAGE_KEY ) ;
200
+ if ( storedMode ) return storedMode ;
201
+ }
202
+
203
+ /**
204
+ * Default to iOS mode as a fallback.
205
+ */
206
+ return Mode . iOS ;
207
+ } ;
168
208
169
209
const getDefaultUsageTarget = ( ) => {
170
- // If defined, Angular target should be the default
210
+ /**
211
+ * If there is a saved target from previously clicking the
212
+ * framework buttons, and there is code for it, use that.
213
+ */
214
+ if ( isBrowser ) {
215
+ const storedTarget = localStorage . getItem ( USAGE_TARGET_STORAGE_KEY ) ;
216
+ if ( storedTarget && code [ storedTarget ] !== undefined ) {
217
+ return storedTarget ;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * If there is no saved target, and Angular code is available,
223
+ * default to that.
224
+ */
171
225
if ( code [ UsageTarget . Angular ] !== undefined ) {
172
226
return UsageTarget . Angular ;
173
227
}
174
228
175
- // Otherwise, default to the first target passed.
229
+ /**
230
+ * If there is no Angular code available, fall back to the
231
+ * first available framework.
232
+ */
176
233
return Object . keys ( code ) [ 0 ] ;
177
234
} ;
178
235
@@ -182,7 +239,7 @@ export default function Playground({
182
239
*/
183
240
const frameSize = FRAME_SIZES [ size ] || size ;
184
241
const [ usageTarget , setUsageTarget ] = useState ( getDefaultUsageTarget ( ) ) ;
185
- const [ ionicMode , setIonicMode ] = useState ( defaultMode ) ;
242
+ const [ ionicMode , setIonicMode ] = useState ( getDefaultMode ( ) ) ;
186
243
const [ codeSnippets , setCodeSnippets ] = useState ( { } ) ;
187
244
const [ renderIframes , setRenderIframes ] = useState ( false ) ;
188
245
const [ iframesLoaded , setIframesLoaded ] = useState ( false ) ;
@@ -196,6 +253,52 @@ export default function Playground({
196
253
*/
197
254
const [ resetCount , setResetCount ] = useState ( 0 ) ;
198
255
256
+ const setAndSaveMode = ( mode : Mode ) => {
257
+ setIonicMode ( mode ) ;
258
+
259
+ if ( isBrowser ) {
260
+ localStorage . setItem ( MODE_STORAGE_KEY , mode ) ;
261
+
262
+ /**
263
+ * Tell other playgrounds on the page that the mode has
264
+ * updated, so they can sync up.
265
+ */
266
+ window . dispatchEvent (
267
+ new CustomEvent ( MODE_UPDATED_EVENT , {
268
+ detail : mode ,
269
+ } )
270
+ ) ;
271
+ }
272
+ } ;
273
+
274
+ const setAndSaveUsageTarget = ( target : UsageTarget , tab : HTMLElement ) => {
275
+ setUsageTarget ( target ) ;
276
+
277
+ if ( isBrowser ) {
278
+ localStorage . setItem ( USAGE_TARGET_STORAGE_KEY , target ) ;
279
+
280
+ /**
281
+ * This prevents the scroll position from jumping around if
282
+ * there is a playground above this one with code that changes
283
+ * in length between frameworks.
284
+ *
285
+ * Note that we don't need this when changing the mode because
286
+ * the two mode iframes are always the same height.
287
+ */
288
+ blockElementScrollPositionUntilNextRender ( tab ) ;
289
+
290
+ /**
291
+ * Tell other playgrounds on the page that the framework
292
+ * has updated, so they can sync up.
293
+ */
294
+ window . dispatchEvent (
295
+ new CustomEvent ( USAGE_TARGET_UPDATED_EVENT , {
296
+ detail : target ,
297
+ } )
298
+ ) ;
299
+ }
300
+ } ;
301
+
199
302
/**
200
303
* Rather than encode isDarkTheme into the frame source
201
304
* url, we post a message to each frame so that
@@ -324,6 +427,47 @@ export default function Playground({
324
427
io . observe ( hostRef . current ! ) ;
325
428
} ) ;
326
429
430
+ /**
431
+ * Sometimes, the app isn't fully hydrated on the first render,
432
+ * causing isBrowser to be set to false even if running the app
433
+ * in a browser (vs. SSR). isBrowser is then updated on the next
434
+ * render cycle.
435
+ *
436
+ * This useEffect contains code that can only run in the browser,
437
+ * and also needs to run on that first go-around. Note that
438
+ * isBrowser will never be set from true back to false, so the
439
+ * code within the if(isBrowser) check will only run once.
440
+ */
441
+ useEffect ( ( ) => {
442
+ if ( isBrowser ) {
443
+ /**
444
+ * Load the stored mode and/or usage target, if present
445
+ * from previously being toggled.
446
+ */
447
+ const storedMode = localStorage . getItem ( MODE_STORAGE_KEY ) ;
448
+ if ( storedMode ) setIonicMode ( storedMode ) ;
449
+ const storedUsageTarget = localStorage . getItem ( USAGE_TARGET_STORAGE_KEY ) ;
450
+ if ( storedUsageTarget ) setUsageTarget ( storedUsageTarget ) ;
451
+
452
+ /**
453
+ * Listen for any playground on the page to have its mode or framework
454
+ * updated so this playground can switch to the same setting.
455
+ */
456
+ window . addEventListener ( MODE_UPDATED_EVENT , ( e : CustomEvent ) => {
457
+ const mode = e . detail ;
458
+ if ( Object . values ( Mode ) . includes ( mode ) ) {
459
+ setIonicMode ( mode ) ; // don't use setAndSave to avoid infinite loop
460
+ }
461
+ } ) ;
462
+ window . addEventListener ( USAGE_TARGET_UPDATED_EVENT , ( e : CustomEvent ) => {
463
+ const usageTarget = e . detail ;
464
+ if ( Object . values ( UsageTarget ) . includes ( usageTarget ) ) {
465
+ setUsageTarget ( usageTarget ) ; // don't use setAndSave to avoid infinite loop
466
+ }
467
+ } ) ;
468
+ }
469
+ } , [ isBrowser ] ) ;
470
+
327
471
const isIOS = ionicMode === Mode . iOS ;
328
472
const isMD = ionicMode === Mode . MD ;
329
473
@@ -526,7 +670,7 @@ export default function Playground({
526
670
key = { `code-block-${ lang } ` }
527
671
language = { lang }
528
672
usageTarget = { usageTarget }
529
- setUsageTarget = { setUsageTarget }
673
+ setAndSaveUsageTarget = { setAndSaveUsageTarget }
530
674
disabled = { ! hasCode }
531
675
/>
532
676
) ;
@@ -536,14 +680,14 @@ export default function Playground({
536
680
< ControlButton
537
681
disabled = { mode && mode === 'md' }
538
682
isSelected = { isIOS }
539
- handleClick = { ( ) => setIonicMode ( Mode . iOS ) }
683
+ handleClick = { ( ) => setAndSaveMode ( Mode . iOS ) }
540
684
title = "iOS mode"
541
685
label = "iOS"
542
686
/>
543
687
< ControlButton
544
688
disabled = { mode && mode === 'ios' }
545
689
isSelected = { isMD }
546
- handleClick = { ( ) => setIonicMode ( Mode . MD ) }
690
+ handleClick = { ( ) => setAndSaveMode ( Mode . MD ) }
547
691
title = "MD mode"
548
692
label = "MD"
549
693
/>
@@ -750,3 +894,8 @@ const isFrameReady = (frame: HTMLIFrameElement) => {
750
894
}
751
895
return ( frame . contentWindow as any ) . demoReady === true ;
752
896
} ;
897
+
898
+ const USAGE_TARGET_STORAGE_KEY = 'playground_usage_target' ;
899
+ const MODE_STORAGE_KEY = 'playground_mode' ;
900
+ const USAGE_TARGET_UPDATED_EVENT = 'playground-usage-target-updated' ;
901
+ const MODE_UPDATED_EVENT = 'playground-event-updated' ;
0 commit comments