@graphcommerce/next-ui 7.1.0-canary.9 → 8.0.0-canary.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/ActionCard/ActionCard.tsx +2 -0
  2. package/CHANGELOG.md +255 -8
  3. package/Config.graphqls +24 -0
  4. package/Footer/Footer.tsx +97 -94
  5. package/FramerScroller/ItemScroller.tsx +3 -3
  6. package/FramerScroller/SidebarGallery.tsx +181 -162
  7. package/JsonLd/JsonLd.tsx +0 -2
  8. package/LayoutDefault/components/LayoutDefault.tsx +6 -1
  9. package/LayoutParts/DesktopNavBar.tsx +2 -0
  10. package/LayoutParts/Logo.tsx +20 -20
  11. package/LazyHydrate/LazyHydrate.tsx +72 -0
  12. package/LazyHydrate/index.ts +1 -0
  13. package/Navigation/components/NavigationFab.tsx +9 -7
  14. package/Navigation/components/NavigationOverlay.tsx +9 -1
  15. package/Navigation/components/NavigationProvider.tsx +18 -4
  16. package/Overlay/components/OverlayBase.tsx +1 -1
  17. package/Overlay/components/index.ts +1 -0
  18. package/Page/CssAndFramerMotionProvider.tsx +20 -18
  19. package/PageMeta/PageMeta.tsx +20 -13
  20. package/RenderType/filterNonNullableKeys.ts +11 -14
  21. package/Row/ColumnOne/variant/VariantMessage.tsx +12 -0
  22. package/Row/ColumnOne/variant/index.ts +1 -0
  23. package/Row/index.ts +1 -0
  24. package/SkipLink/SkipLink.tsx +38 -0
  25. package/Snackbar/DismissibleSnackbar.tsx +28 -0
  26. package/Snackbar/MessageSnackbarImpl.tsx +19 -2
  27. package/Styles/EmotionProvider.tsx +8 -8
  28. package/Styles/createEmotionCache.ts +14 -0
  29. package/Styles/withEmotionCache.tsx +26 -16
  30. package/TextInputNumber/TextInputNumber.tsx +7 -1
  31. package/Theme/MuiButton.ts +2 -2
  32. package/Theme/MuiChip.ts +2 -2
  33. package/Theme/MuiFab.ts +1 -1
  34. package/Theme/MuiSlider.ts +2 -2
  35. package/Theme/MuiSnackbar.ts +6 -4
  36. package/hooks/useDateTimeFormat.ts +3 -5
  37. package/hooks/useNumberFormat.ts +3 -5
  38. package/index.ts +4 -2
  39. package/package.json +16 -18
@@ -3,13 +3,23 @@ import {
3
3
  MotionImageAspect,
4
4
  MotionImageAspectProps,
5
5
  Scroller,
6
- ScrollerButton,
7
- ScrollerButtonProps,
8
6
  ScrollerDots,
7
+ ScrollerButton,
9
8
  ScrollerProvider,
9
+ unstable_usePreventScroll as usePreventScroll,
10
+ ScrollerButtonProps,
11
+ ScrollerThumbnails,
10
12
  } from '@graphcommerce/framer-scroller'
11
13
  import { dvh } from '@graphcommerce/framer-utils'
12
- import { Fab, useTheme, Box, styled, SxProps, Theme } from '@mui/material'
14
+ import {
15
+ Fab,
16
+ useTheme,
17
+ Box,
18
+ styled,
19
+ SxProps,
20
+ Theme,
21
+ Unstable_TrapFocus as TrapFocus,
22
+ } from '@mui/material'
13
23
  import { m, useDomEvent, useMotionValue } from 'framer-motion'
14
24
  import { useRouter } from 'next/router'
15
25
  import React, { useEffect, useRef } from 'react'
@@ -21,7 +31,7 @@ import { iconChevronLeft, iconChevronRight, iconFullscreen, iconFullscreenExit }
21
31
 
22
32
  const MotionBox = styled(m.div)({})
23
33
 
24
- type OwnerState = { zoomed: boolean }
34
+ type OwnerState = { zoomed: boolean; disableZoom: boolean }
25
35
  const name = 'SidebarGallery' as const
26
36
  const parts = [
27
37
  'row',
@@ -59,8 +69,8 @@ export function SidebarGallery(props: SidebarGalleryProps) {
59
69
  aspectRatio: [width, height] = [1, 1],
60
70
  sx,
61
71
  routeHash = 'gallery',
62
- disableZoom,
63
72
  showButtons,
73
+ disableZoom = false,
64
74
  } = props
65
75
 
66
76
  const router = useRouter()
@@ -70,6 +80,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
70
80
  const route = `#${routeHash}`
71
81
  // We're using the URL to manage the state of the gallery.
72
82
  const zoomed = router.asPath.endsWith(route)
83
+ usePreventScroll(zoomed)
73
84
 
74
85
  // cleanup if someone enters the page with #gallery
75
86
  useEffect(() => {
@@ -86,14 +97,13 @@ export function SidebarGallery(props: SidebarGalleryProps) {
86
97
  if (!zoomed) {
87
98
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
88
99
  router.push(route, undefined, { shallow: true })
89
- document.body.style.overflow = 'hidden'
90
100
  window.scrollTo({ top: 0, behavior: 'smooth' })
91
101
  } else {
92
102
  router.back()
93
103
  }
94
104
  }
95
105
 
96
- const classes = withState({ zoomed })
106
+ const classes = withState({ zoomed, disableZoom })
97
107
  const theme = useTheme()
98
108
  const windowRef = useRef(typeof window !== 'undefined' ? window : null)
99
109
 
@@ -132,8 +142,10 @@ export function SidebarGallery(props: SidebarGalleryProps) {
132
142
  {
133
143
  willChange: 'transform',
134
144
  display: 'grid',
145
+ gridTemplate: '"left" "right"',
135
146
  [theme.breakpoints.up('md')]: {
136
- gridTemplateColumns: '1fr auto',
147
+ // gridTemplateColumns: '1fr auto',
148
+ gridTemplate: '"left right" / 1fr auto',
137
149
  },
138
150
  background:
139
151
  theme.palette.mode === 'light'
@@ -152,178 +164,185 @@ export function SidebarGallery(props: SidebarGalleryProps) {
152
164
  },
153
165
  ]}
154
166
  >
155
- <MotionBox
156
- layout
157
- layoutDependency={zoomed}
158
- className={classes.scrollerContainer}
159
- sx={[
160
- {
161
- willChange: 'transform',
162
- height: 0, // https://stackoverflow.com/questions/44770074/css-grid-row-height-safari-bug
163
- backgroundColor: theme.palette.background.image,
164
- position: 'relative',
165
- paddingTop: `min(${ratio}, ${maxHeight})`,
166
- [theme.breakpoints.down('md')]: {
167
- width: '100vw',
168
- },
169
- [theme.breakpoints.up('md')]: {
170
- height: `calc(${dvh(100)} - ${theme.appShell.headerHeightMd} - ${
171
- theme.spacings.lg
172
- })`,
173
- position: 'sticky',
174
- top: theme.appShell.headerHeightMd,
175
- },
176
- },
177
- zoomed && {
178
- position: 'relative',
179
- top: 0,
180
- marginTop: 0,
181
- paddingTop: dvh(100),
182
- },
183
- ]}
184
- onLayoutAnimationComplete={() => {
185
- if (!zoomed) document.body.style.overflow = ''
186
- }}
187
- >
188
- <Scroller
189
- className={classes.scroller}
190
- hideScrollbar
191
- onMouseDown={onMouseDownScroller}
192
- onMouseUp={onMouseUpScroller}
167
+ <TrapFocus open={zoomed}>
168
+ <MotionBox
169
+ layout
170
+ layoutDependency={zoomed}
171
+ className={classes.scrollerContainer}
193
172
  sx={[
194
173
  {
174
+ gridArea: 'left',
195
175
  willChange: 'transform',
196
- position: 'absolute',
197
- top: 0,
198
- width: '100%',
199
- height: '100%',
200
- gridAutoColumns: `100%`,
201
- gridTemplateRows: `100%`,
202
- cursor: disableZoom ? 'auto' : 'zoom-in',
176
+ height: 0, // https://stackoverflow.com/questions/44770074/css-grid-row-height-safari-bug
177
+ backgroundColor: theme.palette.background.image,
178
+ position: 'relative',
179
+ paddingTop: `min(${ratio}, ${maxHeight})`,
180
+ [theme.breakpoints.down('md')]: {
181
+ width: '100vw',
182
+ },
183
+ [theme.breakpoints.up('md')]: {
184
+ height: `calc(${dvh(100)} - ${theme.appShell.headerHeightMd} - ${
185
+ theme.spacings.lg
186
+ })`,
187
+ position: 'sticky',
188
+ top: theme.appShell.headerHeightMd,
189
+ },
203
190
  },
204
191
  zoomed && {
205
- height: `var(--client-size-y)`,
206
- cursor: 'inherit',
192
+ position: 'relative',
193
+ top: { xs: 0, md: 0 },
194
+ marginTop: 0,
195
+ paddingTop: dvh(100),
207
196
  },
208
197
  ]}
198
+ onLayoutAnimationComplete={() => {
199
+ if (!zoomed) document.body.style.overflow = ''
200
+ }}
209
201
  >
210
- {images.map((image, idx) => (
211
- <MotionImageAspect
212
- key={typeof image.src === 'string' ? image.src : idx}
202
+ <Scroller
203
+ className={classes.scroller}
204
+ hideScrollbar
205
+ onMouseDown={onMouseDownScroller}
206
+ onMouseUp={onMouseUpScroller}
207
+ sx={[
208
+ {
209
+ willChange: 'transform',
210
+ position: 'absolute',
211
+ top: 0,
212
+ width: '100%',
213
+ height: '100%',
214
+ gridAutoColumns: `100%`,
215
+ gridTemplateRows: `100%`,
216
+ cursor: disableZoom ? 'auto' : 'zoom-in',
217
+ },
218
+ zoomed && {
219
+ height: `var(--client-size-y)`,
220
+ cursor: 'inherit',
221
+ },
222
+ ]}
223
+ >
224
+ {images.map((image, idx) => (
225
+ <MotionImageAspect
226
+ key={typeof image.src === 'string' ? image.src : idx}
227
+ layout
228
+ layoutDependency={zoomed}
229
+ src={image.src}
230
+ width={image.width}
231
+ height={image.height}
232
+ loading={idx === 0 ? 'eager' : 'lazy'}
233
+ sx={{ display: 'block', objectFit: 'contain' }}
234
+ sizes={{
235
+ 0: '100vw',
236
+ [theme.breakpoints.values.md]: zoomed ? '100vw' : '60vw',
237
+ }}
238
+ alt={image.alt || `Product Image ${idx}` || undefined}
239
+ dontReportWronglySizedImages
240
+ />
241
+ ))}
242
+ </Scroller>
243
+ <MotionBox
244
+ layout='position'
245
+ layoutDependency={zoomed}
246
+ className={classes.topRight}
247
+ sx={{
248
+ display: 'grid',
249
+ gridAutoFlow: 'column',
250
+ top: theme.spacings.sm,
251
+ gap: theme.spacings.xxs,
252
+ position: 'absolute',
253
+ right: theme.spacings.sm,
254
+ }}
255
+ >
256
+ {!disableZoom && (
257
+ <Fab
258
+ size='small'
259
+ className={classes.toggleIcon}
260
+ disabled={!hasImages}
261
+ onMouseUp={toggle}
262
+ aria-label='Toggle Fullscreen'
263
+ sx={{ boxShadow: 6 }}
264
+ >
265
+ {!zoomed ? (
266
+ <IconSvg src={iconFullscreen} />
267
+ ) : (
268
+ <IconSvg src={iconFullscreenExit} />
269
+ )}
270
+ </Fab>
271
+ )}
272
+ </MotionBox>
273
+ <Box
274
+ className={classes.centerLeft}
275
+ sx={{
276
+ display: 'grid',
277
+ gridAutoFlow: 'row',
278
+ gap: theme.spacings.xxs,
279
+ position: 'absolute',
280
+ left: theme.spacings.sm,
281
+ top: `calc(50% - 28px)`,
282
+ }}
283
+ >
284
+ <ScrollerButton
213
285
  layout
214
286
  layoutDependency={zoomed}
215
- src={image.src}
216
- width={image.width}
217
- height={image.height}
218
- loading={idx === 0 ? 'eager' : 'lazy'}
219
- sx={{ display: 'block', objectFit: 'contain' }}
220
- sizes={{
221
- 0: '100vw',
222
- [theme.breakpoints.values.md]: zoomed ? '100vw' : '60vw',
223
- }}
224
- alt={image.alt || `Product Image ${idx}` || undefined}
225
- dontReportWronglySizedImages
226
- />
227
- ))}
228
- </Scroller>
229
- <MotionBox
230
- layout='position'
231
- layoutDependency={zoomed}
232
- className={classes.topRight}
233
- sx={{
234
- display: 'grid',
235
- gridAutoFlow: 'column',
236
- top: theme.spacings.sm,
237
- gap: theme.spacings.xxs,
238
- position: 'absolute',
239
- right: theme.spacings.sm,
240
- }}
241
- >
242
- {!disableZoom && (
243
- <Fab
287
+ direction='left'
288
+ showButtons={showButtons}
244
289
  size='small'
245
- className={classes.toggleIcon}
246
- disabled={!hasImages}
247
- onMouseUp={toggle}
248
- aria-label='Toggle Fullscreen'
249
- sx={{ boxShadow: 6 }}
290
+ className={classes.sliderButtons}
250
291
  >
251
- {!zoomed ? (
252
- <IconSvg src={iconFullscreen} />
253
- ) : (
254
- <IconSvg src={iconFullscreenExit} />
255
- )}
256
- </Fab>
257
- )}
258
- </MotionBox>
259
- <Box
260
- className={classes.centerLeft}
261
- sx={{
262
- display: 'grid',
263
- gridAutoFlow: 'row',
264
- gap: theme.spacings.xxs,
265
- position: 'absolute',
266
- left: theme.spacings.sm,
267
- top: `calc(50% - 28px)`,
268
- }}
269
- >
270
- <ScrollerButton
271
- layout
272
- layoutDependency={zoomed}
273
- direction='left'
274
- showButtons={showButtons}
275
- size='small'
276
- className={classes.sliderButtons}
277
- >
278
- <IconSvg src={iconChevronLeft} />
279
- </ScrollerButton>
280
- </Box>
281
- <Box
282
- className={classes.centerRight}
283
- sx={{
284
- display: 'grid',
285
- gap: theme.spacings.xxs,
286
- position: 'absolute',
287
- right: theme.spacings.sm,
288
- top: `calc(50% - 28px)`,
289
- }}
290
- >
291
- <ScrollerButton
292
- layout
293
- layoutDependency={zoomed}
294
- direction='right'
295
- showButtons={showButtons}
296
- size='small'
297
- className={classes.sliderButtons}
292
+ <IconSvg src={iconChevronLeft} />
293
+ </ScrollerButton>
294
+ </Box>
295
+ <Box
296
+ className={classes.centerRight}
297
+ sx={{
298
+ display: 'grid',
299
+ gap: theme.spacings.xxs,
300
+ position: 'absolute',
301
+ right: theme.spacings.sm,
302
+ top: `calc(50% - 28px)`,
303
+ }}
298
304
  >
299
- <IconSvg src={iconChevronRight} />
300
- </ScrollerButton>
301
- </Box>
302
-
303
- <Box
304
- className={classes.bottomCenter}
305
- sx={{
306
- display: 'flex',
307
- px: theme.page.horizontal,
308
- gap: theme.spacings.xxs,
309
- position: 'absolute',
310
- bottom: theme.spacings.xxs,
311
- justifyContent: 'center',
312
- width: '100%',
313
- pointerEvents: 'none',
314
- '& > *': {
315
- pointerEvents: 'all',
316
- },
317
- }}
318
- >
319
- <ScrollerDots layout='position' layoutDependency={zoomed} />
320
- </Box>
321
- </MotionBox>
305
+ <ScrollerButton
306
+ layout
307
+ layoutDependency={zoomed}
308
+ direction='right'
309
+ showButtons={showButtons}
310
+ size='small'
311
+ className={classes.sliderButtons}
312
+ >
313
+ <IconSvg src={iconChevronRight} />
314
+ </ScrollerButton>
315
+ </Box>
322
316
 
317
+ <Box
318
+ className={classes.bottomCenter}
319
+ sx={{
320
+ display: 'flex',
321
+ gap: theme.spacings.xxs,
322
+ position: 'absolute',
323
+ bottom: theme.spacings.xxs,
324
+ justifyContent: 'center',
325
+ width: '100%',
326
+ pointerEvents: 'none',
327
+ '& > *': {
328
+ pointerEvents: 'all',
329
+ },
330
+ }}
331
+ >
332
+ {import.meta.graphCommerce.sidebarGallery?.paginationVariant ===
333
+ 'THUMBNAILS_BOTTOM' ? (
334
+ <ScrollerThumbnails images={images} />
335
+ ) : (
336
+ <ScrollerDots />
337
+ )}
338
+ </Box>
339
+ </MotionBox>
340
+ </TrapFocus>
323
341
  <Box
324
342
  className={classes.sidebarWrapper}
325
343
  sx={[
326
344
  {
345
+ gridArea: 'right',
327
346
  boxSizing: 'content-box',
328
347
  display: 'grid',
329
348
  justifyItems: 'start',
package/JsonLd/JsonLd.tsx CHANGED
@@ -1,8 +1,6 @@
1
1
  import Head from 'next/head'
2
2
  import { safeJsonLdReplacer } from './safeJsonLdReplacer'
3
3
 
4
- export * as SchemaDts from 'schema-dts'
5
-
6
4
  export function JsonLd<T extends { '@type': string }>(props: {
7
5
  item: T & { '@context': 'https://schema.org' }
8
6
  }) {
@@ -3,6 +3,7 @@ import { dvh } from '@graphcommerce/framer-utils'
3
3
  import { Box, SxProps, Theme } from '@mui/material'
4
4
  import { useTransform, useScroll } from 'framer-motion'
5
5
  import { LayoutProvider } from '../../Layout/components/LayoutProvider'
6
+ import { SkipLink } from '../../SkipLink/SkipLink'
6
7
  import { extendableComponent } from '../../Styles'
7
8
  import { useFabSize } from '../../Theme'
8
9
 
@@ -67,6 +68,7 @@ export function LayoutDefault(props: LayoutDefaultProps) {
67
68
  ...(Array.isArray(sx) ? sx : [sx]),
68
69
  ]}
69
70
  >
71
+ <SkipLink />
70
72
  <LayoutProvider scroll={scrollYOffset}>
71
73
  {beforeHeader}
72
74
  <Box
@@ -144,7 +146,10 @@ export function LayoutDefault(props: LayoutDefaultProps) {
144
146
  ) : (
145
147
  <div />
146
148
  )}
147
- <div className={classes.children}>{children}</div>
149
+ <div className={classes.children}>
150
+ <div id='skip-nav' tabIndex={-1} />
151
+ {children}
152
+ </div>
148
153
  <div className={classes.footer}>{footer}</div>
149
154
  </LayoutProvider>
150
155
  </Box>
@@ -69,6 +69,7 @@ export function DesktopNavBar(props: MenuTabsProps) {
69
69
  }}
70
70
  direction='left'
71
71
  size='small'
72
+ tabIndex={-1}
72
73
  className={`${classes.left} ${classes.button}`}
73
74
  >
74
75
  <IconSvg src={iconLeft ?? iconChevronLeft} />
@@ -94,6 +95,7 @@ export function DesktopNavBar(props: MenuTabsProps) {
94
95
  }}
95
96
  direction='right'
96
97
  size='small'
98
+ tabIndex={-1}
97
99
  className={`${classes.right} ${classes.button}`}
98
100
  >
99
101
  <IconSvg src={iconRight ?? iconChevronRight} />
@@ -5,6 +5,7 @@ import {
5
5
  SxProps,
6
6
  Theme,
7
7
  unstable_composeClasses as composeClasses,
8
+ Box,
8
9
  } from '@mui/material'
9
10
  import { useRouter } from 'next/router'
10
11
  import { forwardRef } from 'react'
@@ -22,12 +23,7 @@ const getLogoUtilityClass = (slot: string) => generateUtilityClass(name, slot)
22
23
  const useUtilityClasses = ({ classes }: LogoClassProps) =>
23
24
  composeClasses({ logo: ['logo'], parent: ['parent'] }, getLogoUtilityClass, classes)
24
25
 
25
- /** Creating styled components */
26
- const LogoContainer = styled(NextLink, {
27
- name,
28
- slot: 'parent',
29
- overridesResolver: (_props, styles) => styles.parent,
30
- })(({ theme }) => ({
26
+ const commonLogoStyles: SxProps<Theme> = {
31
27
  height: '100%',
32
28
  width: 'max-content',
33
29
  display: 'flex',
@@ -35,12 +31,13 @@ const LogoContainer = styled(NextLink, {
35
31
  margin: '0 auto',
36
32
  justifyContent: 'center',
37
33
  pointerEvents: 'all',
38
- [theme.breakpoints.up('md')]: {
39
- display: 'flex',
40
- margin: 'unset',
41
- justifyContent: 'left',
42
- },
43
- }))
34
+ }
35
+
36
+ const LogoContainer = styled(NextLink, {
37
+ name,
38
+ slot: 'parent',
39
+ overridesResolver: (_props, styles) => styles.parent,
40
+ })({})
44
41
 
45
42
  export type LogoProps = {
46
43
  href?: `/${string}`
@@ -51,7 +48,6 @@ export type LogoProps = {
51
48
  export const Logo = forwardRef<HTMLAnchorElement, LogoProps>((props, ref) => {
52
49
  const { href = '/', image, sx } = props
53
50
  const router = useRouter()
54
-
55
51
  const classes = useUtilityClasses(props)
56
52
 
57
53
  const img = (
@@ -63,13 +59,17 @@ export const Logo = forwardRef<HTMLAnchorElement, LogoProps>((props, ref) => {
63
59
  />
64
60
  )
65
61
 
66
- return router.asPath.split('?')[0] === '/' ? (
67
- <LogoContainer ref={ref} sx={sx} className={classes.parent}>
68
- {img}
69
- </LogoContainer>
70
- ) : (
71
- <LogoContainer href={href} ref={ref} sx={sx} className={classes.parent}>
62
+ const shouldRedirect = router.asPath.split('?')[0] !== href
63
+
64
+ return (
65
+ <Box
66
+ component={shouldRedirect ? LogoContainer : 'div'}
67
+ href={shouldRedirect ? href : undefined}
68
+ ref={ref}
69
+ sx={[...(Array.isArray(sx) ? sx : [sx]), commonLogoStyles]}
70
+ className={classes.parent}
71
+ >
72
72
  {img}
73
- </LogoContainer>
73
+ </Box>
74
74
  )
75
75
  })
@@ -0,0 +1,72 @@
1
+ import React, { useState, useRef, startTransition, useLayoutEffect, useEffect } from 'react'
2
+
3
+ // Make sure the server doesn't choke on the useLayoutEffect
4
+ export const useLayoutEffect2 = typeof window !== 'undefined' ? useLayoutEffect : useEffect
5
+
6
+ export type LazyHydrateProps = {
7
+ /**
8
+ * The content is always rendered on the server and on the client it uses the server rendered HTML until it is hydrated.
9
+ */
10
+ children: React.ReactNode
11
+
12
+ /**
13
+ * When a boolean is provided, the IntersectionObserver is disabled and hydrates the component when the value becomes true.
14
+ *
15
+ * For example:
16
+ * - Disable the hydration functionality completely: `<LazyHydrate hydrated={true}>`
17
+ * - Hydrate the component on some state `<LazyHydrate hydrated={someState}>` where someState initially is false and later becomes true.
18
+ */
19
+ hydrated?: boolean
20
+ }
21
+
22
+ /**
23
+ * LazyHydrate can defer the hydration of a component until it becomes visible.
24
+ * OR manually by using the hydrated prop.
25
+ * This can be a way to improve the TBT of a page.
26
+ */
27
+ export function LazyHydrate(props: LazyHydrateProps) {
28
+ const { hydrated, children } = props
29
+ const rootRef = useRef<HTMLElement>(null)
30
+
31
+ const [isHydrated, setIsHydrated] = useState(hydrated || false)
32
+ if (!isHydrated && hydrated) setIsHydrated(true)
33
+
34
+ useLayoutEffect2(() => {
35
+ // If we are manually hydrating, we watch that value and do not use the IntersectionObserver
36
+ if (isHydrated || !rootRef.current) return undefined
37
+
38
+ // If the element wasn't rendered on the server, we hydrate it immediately
39
+ if (!rootRef.current?.hasAttribute('data-lazy-hydrate')) {
40
+ setIsHydrated(true)
41
+ return undefined
42
+ }
43
+
44
+ // The user has opted to manually hydrate the component
45
+ if (hydrated !== undefined) return undefined
46
+
47
+ const observer = new IntersectionObserver(
48
+ ([entry]) => {
49
+ if (entry.isIntersecting && entry.intersectionRatio > 0) {
50
+ startTransition(() => setIsHydrated(true))
51
+ }
52
+ },
53
+ { rootMargin: '200px' },
54
+ )
55
+ observer.observe(rootRef.current)
56
+
57
+ return () => observer.disconnect()
58
+ }, [hydrated, isHydrated])
59
+
60
+ if (isHydrated) {
61
+ return <section>{children}</section>
62
+ }
63
+
64
+ if (typeof window === 'undefined') {
65
+ return <section data-lazy-hydrate>{children}</section>
66
+ }
67
+
68
+ return (
69
+ // eslint-disable-next-line react/no-danger
70
+ <section ref={rootRef} dangerouslySetInnerHTML={{ __html: '' }} suppressHydrationWarning />
71
+ )
72
+ }
@@ -0,0 +1 @@
1
+ export * from './LazyHydrate'