1- import React , { useEffect , useMemo , useRef , useState } from 'react' ;
1+ import React , { RefObject , forwardRef , useEffect , useMemo , useRef , useState } from 'react' ;
22
33import useBaseUrl from '@docusaurus/useBaseUrl' ;
44import './playground.css' ;
@@ -13,52 +13,64 @@ import TabItem from '@theme/TabItem';
1313
1414import { IconHtml , IconTs , IconVue , IconDefault , IconCss , IconDots } from './icons' ;
1515
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 >
4647 ) ;
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 ;
4757 }
48- return controlButton ;
49- } ;
58+ ) ;
5059
51- const CodeBlockButton = ( { language, usageTarget, setUsageTarget, disabled } ) => {
60+ const CodeBlockButton = ( { language, usageTarget, setAndSaveUsageTarget, disabled } ) => {
61+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
5262 const langValue = UsageTarget [ language ] ;
63+
5364 return (
5465 < ControlButton
5566 isSelected = { usageTarget === langValue }
5667 handleClick = { ( ) => {
57- setUsageTarget ( langValue ) ;
68+ setAndSaveUsageTarget ( langValue , buttonRef . current ) ;
5869 } }
5970 title = { `Show ${ language } code` }
6071 label = { language }
6172 disabled = { disabled }
73+ ref = { buttonRef }
6274 />
6375 ) ;
6476} ;
@@ -158,21 +170,66 @@ export default function Playground({
158170
159171 const { isDarkTheme } = useThemeContext ( ) ;
160172
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+
161180 const hostRef = useRef < HTMLDivElement | null > ( null ) ;
162181 const codeRef = useRef ( null ) ;
163182 const frameiOS = useRef < HTMLIFrameElement | null > ( null ) ;
164183 const frameMD = useRef < HTMLIFrameElement | null > ( null ) ;
165184 const consoleBodyRef = useRef < HTMLDivElement | null > ( null ) ;
166185
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+ } ;
168208
169209 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+ */
171225 if ( code [ UsageTarget . Angular ] !== undefined ) {
172226 return UsageTarget . Angular ;
173227 }
174228
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+ */
176233 return Object . keys ( code ) [ 0 ] ;
177234 } ;
178235
@@ -182,7 +239,7 @@ export default function Playground({
182239 */
183240 const frameSize = FRAME_SIZES [ size ] || size ;
184241 const [ usageTarget , setUsageTarget ] = useState ( getDefaultUsageTarget ( ) ) ;
185- const [ ionicMode , setIonicMode ] = useState ( defaultMode ) ;
242+ const [ ionicMode , setIonicMode ] = useState ( getDefaultMode ( ) ) ;
186243 const [ codeSnippets , setCodeSnippets ] = useState ( { } ) ;
187244 const [ renderIframes , setRenderIframes ] = useState ( false ) ;
188245 const [ iframesLoaded , setIframesLoaded ] = useState ( false ) ;
@@ -196,6 +253,52 @@ export default function Playground({
196253 */
197254 const [ resetCount , setResetCount ] = useState ( 0 ) ;
198255
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+
199302 /**
200303 * Rather than encode isDarkTheme into the frame source
201304 * url, we post a message to each frame so that
@@ -324,6 +427,47 @@ export default function Playground({
324427 io . observe ( hostRef . current ! ) ;
325428 } ) ;
326429
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+
327471 const isIOS = ionicMode === Mode . iOS ;
328472 const isMD = ionicMode === Mode . MD ;
329473
@@ -526,7 +670,7 @@ export default function Playground({
526670 key = { `code-block-${ lang } ` }
527671 language = { lang }
528672 usageTarget = { usageTarget }
529- setUsageTarget = { setUsageTarget }
673+ setAndSaveUsageTarget = { setAndSaveUsageTarget }
530674 disabled = { ! hasCode }
531675 />
532676 ) ;
@@ -536,14 +680,14 @@ export default function Playground({
536680 < ControlButton
537681 disabled = { mode && mode === 'md' }
538682 isSelected = { isIOS }
539- handleClick = { ( ) => setIonicMode ( Mode . iOS ) }
683+ handleClick = { ( ) => setAndSaveMode ( Mode . iOS ) }
540684 title = "iOS mode"
541685 label = "iOS"
542686 />
543687 < ControlButton
544688 disabled = { mode && mode === 'ios' }
545689 isSelected = { isMD }
546- handleClick = { ( ) => setIonicMode ( Mode . MD ) }
690+ handleClick = { ( ) => setAndSaveMode ( Mode . MD ) }
547691 title = "MD mode"
548692 label = "MD"
549693 />
@@ -750,3 +894,8 @@ const isFrameReady = (frame: HTMLIFrameElement) => {
750894 }
751895 return ( frame . contentWindow as any ) . demoReady === true ;
752896} ;
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