@graphcommerce/next-ui 8.0.0-canary.73 → 8.0.0-canary.74

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 8.0.0-canary.74
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2133](https://github.com/graphcommerce-org/graphcommerce/pull/2133) [`133f908`](https://github.com/graphcommerce-org/graphcommerce/commit/133f908200a79589036420f2925835724522cab8) - Added lazy hydration to improve total blocking time. Added LazyHydrate component which can be wrapped around other components you want to lazy hydrate.
8
+ ([@Jessevdpoel](https://github.com/Jessevdpoel))
9
+
3
10
  ## 8.0.0-canary.73
4
11
 
5
12
  ## 8.0.0-canary.72
package/Footer/Footer.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ContainerProps, Container, Box } from '@mui/material'
2
2
  import React from 'react'
3
+ import { LazyHydrate } from '../LazyHydrate'
3
4
  import { extendableComponent } from '../Styles'
4
5
 
5
6
  export type FooterProps = {
@@ -30,110 +31,112 @@ export function Footer(props: FooterProps) {
30
31
  } = props
31
32
 
32
33
  return (
33
- <Container
34
- sx={[
35
- (theme) => ({
36
- gridTemplateColumns: '5fr 3fr',
37
- borderTop: `1px solid ${theme.palette.divider}`,
38
- display: 'grid',
39
- alignItems: 'center',
40
- padding: `${theme.spacings.lg} ${theme.page.horizontal} ${theme.page.vertical}`,
41
- justifyItems: 'center',
42
- gridTemplateAreas: `
34
+ <LazyHydrate>
35
+ <Container
36
+ sx={[
37
+ (theme) => ({
38
+ gridTemplateColumns: '5fr 3fr',
39
+ borderTop: `1px solid ${theme.palette.divider}`,
40
+ display: 'grid',
41
+ alignItems: 'center',
42
+ padding: `${theme.spacings.lg} ${theme.page.horizontal} ${theme.page.vertical}`,
43
+ justifyItems: 'center',
44
+ gridTemplateAreas: `
43
45
  'switcher switcher'
44
46
  'support support'
45
47
  'social social'
46
48
  'links links'
47
49
  `,
48
- gap: theme.spacings.md,
49
- '& > *': { maxWidth: 'max-content' },
50
+ gap: theme.spacings.md,
51
+ '& > *': { maxWidth: 'max-content' },
50
52
 
51
- [theme.breakpoints.up('md')]: {
52
- gap: theme.spacings.sm,
53
- gridTemplateAreas: `
53
+ [theme.breakpoints.up('md')]: {
54
+ gap: theme.spacings.sm,
55
+ gridTemplateAreas: `
54
56
  'social switcher'
55
57
  'links support'
56
58
  `,
57
- justifyItems: 'start',
58
- padding: `${theme.page.vertical} ${theme.page.horizontal}`,
59
- gridTemplateColumns: 'auto auto',
60
- gridTemplateRows: 'auto',
61
- justifyContent: 'space-between',
62
- },
63
- }),
64
- ...(Array.isArray(sx) ? sx : [sx]),
65
- ]}
66
- maxWidth={false}
67
- className={classes.root}
68
- {...containerProps}
69
- >
70
- {socialLinks && (
71
- <Box
72
- sx={(theme) => ({
73
- display: 'grid',
74
- justifyContent: 'start',
75
- gridAutoFlow: 'column',
76
- gridArea: 'social',
77
- gap: { xs: `0 ${theme.spacings.xs}`, md: `0 ${theme.spacings.xs}` },
78
- '& > *': {
79
- minWidth: 'min-content',
80
- },
81
- })}
82
- className={classes.social}
83
- >
84
- {socialLinks}
85
- </Box>
86
- )}
87
- {storeSwitcher && (
88
- <Box
89
- sx={(theme) => ({
90
- gridArea: 'switcher',
91
- justifySelf: 'end',
92
- [theme.breakpoints.down('md')]: {
93
- justifySelf: 'center',
94
- },
95
- })}
96
- className={classes.storeSwitcher}
97
- >
98
- {storeSwitcher}
99
- </Box>
100
- )}
101
- {customerService && (
102
- <Box
103
- sx={(theme) => ({
104
- gridArea: 'support',
105
- justifySelf: 'flex-end',
106
- [theme.breakpoints.down('md')]: {
107
- justifySelf: 'center',
108
- },
109
- })}
110
- className={classes.support}
111
- >
112
- {customerService}
113
- </Box>
114
- )}
115
- {copyright && (
116
- <Box
117
- sx={(theme) => ({
118
- typography: 'body2',
119
- display: 'grid',
120
- gridAutoFlow: 'column',
121
- alignContent: 'center',
122
- gridArea: 'links',
123
- gap: theme.spacings.sm,
124
- [theme.breakpoints.down('md')]: {
125
- gridAutoFlow: 'row',
126
- textAlign: 'center',
127
- gap: `8px`,
59
+ justifyItems: 'start',
60
+ padding: `${theme.page.vertical} ${theme.page.horizontal}`,
61
+ gridTemplateColumns: 'auto auto',
62
+ gridTemplateRows: 'auto',
63
+ justifyContent: 'space-between',
128
64
  },
129
- })}
130
- className={classes.copyright}
131
- >
132
- {copyright}
133
- </Box>
134
- )}
135
- {children}
136
- </Container>
65
+ }),
66
+ ...(Array.isArray(sx) ? sx : [sx]),
67
+ ]}
68
+ maxWidth={false}
69
+ className={classes.root}
70
+ {...containerProps}
71
+ >
72
+ {socialLinks && (
73
+ <Box
74
+ sx={(theme) => ({
75
+ display: 'grid',
76
+ justifyContent: 'start',
77
+ gridAutoFlow: 'column',
78
+ gridArea: 'social',
79
+ gap: { xs: `0 ${theme.spacings.xs}`, md: `0 ${theme.spacings.xs}` },
80
+ '& > *': {
81
+ minWidth: 'min-content',
82
+ },
83
+ })}
84
+ className={classes.social}
85
+ >
86
+ {socialLinks}
87
+ </Box>
88
+ )}
89
+ {storeSwitcher && (
90
+ <Box
91
+ sx={(theme) => ({
92
+ gridArea: 'switcher',
93
+ justifySelf: 'end',
94
+ [theme.breakpoints.down('md')]: {
95
+ justifySelf: 'center',
96
+ },
97
+ })}
98
+ className={classes.storeSwitcher}
99
+ >
100
+ {storeSwitcher}
101
+ </Box>
102
+ )}
103
+ {customerService && (
104
+ <Box
105
+ sx={(theme) => ({
106
+ gridArea: 'support',
107
+ justifySelf: 'flex-end',
108
+ [theme.breakpoints.down('md')]: {
109
+ justifySelf: 'center',
110
+ },
111
+ })}
112
+ className={classes.support}
113
+ >
114
+ {customerService}
115
+ </Box>
116
+ )}
117
+ {copyright && (
118
+ <Box
119
+ sx={(theme) => ({
120
+ typography: 'body2',
121
+ display: 'grid',
122
+ gridAutoFlow: 'column',
123
+ alignContent: 'center',
124
+ gridArea: 'links',
125
+ gap: theme.spacings.sm,
126
+ [theme.breakpoints.down('md')]: {
127
+ gridAutoFlow: 'row',
128
+ textAlign: 'center',
129
+ gap: `8px`,
130
+ },
131
+ })}
132
+ className={classes.copyright}
133
+ >
134
+ {copyright}
135
+ </Box>
136
+ )}
137
+ {children}
138
+ </Container>
139
+ </LazyHydrate>
137
140
  )
138
141
  }
139
142
 
@@ -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'
@@ -77,10 +77,6 @@ export const NavigationOverlay = React.memo((props: NavigationOverlayProps) => {
77
77
  c ? false : s !== false,
78
78
  )
79
79
 
80
- useEffect(() => {
81
- animating.set(true)
82
- }, [activeAndNotClosing, animating])
83
-
84
80
  const afterClose = useEventCallback(() => {
85
81
  if (!closing.get()) return
86
82
  setTimeout(() => {
@@ -1,6 +1,8 @@
1
- import { MotionConfig, useMotionValue } from 'framer-motion'
1
+ import { MotionConfig, useMotionValue, useTransform } from 'framer-motion'
2
2
  import React, { useMemo } from 'react'
3
3
  import { isElement } from 'react-is'
4
+ import { LazyHydrate } from '../../LazyHydrate'
5
+ import { nonNullable } from '../../RenderType/nonNullable'
4
6
  import {
5
7
  NavigationNode,
6
8
  NavigationContextType,
@@ -9,8 +11,9 @@ import {
9
11
  NavigationNodeType,
10
12
  NavigationNodeComponent,
11
13
  } from '../hooks/useNavigation'
14
+ import { useMotionValueValue } from '@graphcommerce/framer-utils'
12
15
 
13
- export type NavigationProviderProps = {
16
+ export type NavigationProviderBaseProps = {
14
17
  items: (NavigationNode | React.ReactElement)[]
15
18
  hideRootOnNavigate?: boolean
16
19
  closeAfterNavigate?: boolean
@@ -20,9 +23,9 @@ export type NavigationProviderProps = {
20
23
  serverRenderDepth?: number
21
24
  }
22
25
 
23
- const nonNullable = <T,>(value: T): value is NonNullable<T> => value !== null && value !== undefined
26
+ export type NavigationProviderProps = NavigationProviderBaseProps & { hold?: boolean }
24
27
 
25
- export const NavigationProvider = React.memo<NavigationProviderProps>((props) => {
28
+ const NavigationProviderBase = React.memo<NavigationProviderBaseProps>((props) => {
26
29
  const {
27
30
  items,
28
31
  hideRootOnNavigate = true,
@@ -73,3 +76,14 @@ export const NavigationProvider = React.memo<NavigationProviderProps>((props) =>
73
76
  </MotionConfig>
74
77
  )
75
78
  })
79
+
80
+ export function NavigationProvider(props: NavigationProviderProps) {
81
+ const { selection } = props
82
+ const hydrateManually = useMotionValueValue(selection, (s) => s !== false)
83
+
84
+ return (
85
+ <LazyHydrate hydrated={hydrateManually}>
86
+ <NavigationProviderBase {...props} />
87
+ </LazyHydrate>
88
+ )
89
+ }
package/index.ts CHANGED
@@ -22,15 +22,14 @@ export * from './Form/InputCheckmark'
22
22
  export * from './FramerScroller'
23
23
  export * from './FullPageMessage/FullPageMessage'
24
24
  export * from './Highlight/Highlight'
25
- export * from './hooks'
26
25
  export * from './IconHeader/IconHeader'
27
- export * from './icons'
28
26
  export * from './IconSvg'
29
27
  export * from './JsonLd/JsonLd'
30
28
  export * from './Layout'
31
29
  export * from './LayoutDefault'
32
30
  export * from './LayoutOverlay'
33
31
  export * from './LayoutParts'
32
+ export * from './LazyHydrate'
34
33
  export * from './Navigation'
35
34
  export * from './Overlay'
36
35
  export * from './OverlayOrPopperChip'
@@ -56,4 +55,6 @@ export * from './ToggleButton/ToggleButton'
56
55
  export * from './ToggleButtonGroup/ToggleButtonGroup'
57
56
  export * from './UspList/UspList'
58
57
  export * from './UspList/UspListItem'
58
+ export * from './hooks'
59
+ export * from './icons'
59
60
  export * from './utils/cookie'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/next-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "8.0.0-canary.73",
5
+ "version": "8.0.0-canary.74",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -26,13 +26,13 @@
26
26
  "typescript": "5.3.3"
27
27
  },
28
28
  "peerDependencies": {
29
- "@graphcommerce/eslint-config-pwa": "^8.0.0-canary.73",
30
- "@graphcommerce/framer-next-pages": "^8.0.0-canary.73",
31
- "@graphcommerce/framer-scroller": "^8.0.0-canary.73",
32
- "@graphcommerce/framer-utils": "^8.0.0-canary.73",
33
- "@graphcommerce/image": "^8.0.0-canary.73",
34
- "@graphcommerce/prettier-config-pwa": "^8.0.0-canary.73",
35
- "@graphcommerce/typescript-config-pwa": "^8.0.0-canary.73",
29
+ "@graphcommerce/eslint-config-pwa": "^8.0.0-canary.74",
30
+ "@graphcommerce/framer-next-pages": "^8.0.0-canary.74",
31
+ "@graphcommerce/framer-scroller": "^8.0.0-canary.74",
32
+ "@graphcommerce/framer-utils": "^8.0.0-canary.74",
33
+ "@graphcommerce/image": "^8.0.0-canary.74",
34
+ "@graphcommerce/prettier-config-pwa": "^8.0.0-canary.74",
35
+ "@graphcommerce/typescript-config-pwa": "^8.0.0-canary.74",
36
36
  "@lingui/core": "^4.2.1",
37
37
  "@lingui/macro": "^4.2.1",
38
38
  "@lingui/react": "^4.2.1",