@graphcommerce/next-ui 3.19.1 → 3.20.3

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 (95) hide show
  1. package/AppShell/AppShellSticky/index.tsx +3 -19
  2. package/AppShell/DesktopNavActions.tsx +3 -4
  3. package/AppShell/DesktopNavBar.tsx +2 -3
  4. package/AppShell/FixedFab.tsx +1 -6
  5. package/AppShell/GlobalHead.tsx +36 -0
  6. package/AppShell/Logo.tsx +11 -20
  7. package/AppShell/MenuFab.tsx +5 -3
  8. package/AppShell/MenuFabSecondaryItem.tsx +3 -3
  9. package/AppShell/PlaceholderFab/index.tsx +8 -27
  10. package/AppShell/index.tsx +19 -0
  11. package/AppShell/useFabAnimation.tsx +3 -2
  12. package/AppShell/useFixedFabAnimation.tsx +3 -2
  13. package/Blog/BlogAuthor/index.tsx +1 -1
  14. package/Blog/BlogHeader/index.tsx +2 -3
  15. package/Blog/BlogList/index.tsx +1 -1
  16. package/Blog/BlogListItem/index.tsx +1 -1
  17. package/Blog/BlogTitle/index.tsx +5 -5
  18. package/Button/index.tsx +26 -21
  19. package/CHANGELOG.md +56 -0
  20. package/ChipMenu/index.tsx +2 -2
  21. package/FlagAvatar/index.tsx +3 -3
  22. package/{AppShell/Footer/index.tsx → Footer/Footer.tsx} +3 -3
  23. package/{AppShell/Footer → Footer}/SocialIcon.tsx +3 -3
  24. package/Footer/index.ts +2 -0
  25. package/Form/InputCheckmark.tsx +3 -3
  26. package/Form/index.tsx +1 -1
  27. package/FramerScroller/components/SidebarGallery.tsx +24 -20
  28. package/FramerScroller/components/SidebarSlider.tsx +1 -3
  29. package/FullPageMessage/index.tsx +1 -1
  30. package/IconHeader/index.tsx +2 -15
  31. package/Layout/components/LayoutHeader.tsx +151 -0
  32. package/Layout/components/LayoutHeaderBack.tsx +58 -0
  33. package/Layout/components/LayoutHeaderClose.tsx +27 -0
  34. package/Layout/components/LayoutHeaderContent.tsx +178 -0
  35. package/Layout/components/LayoutHeadertypes.ts +10 -0
  36. package/Layout/components/LayoutProvider.tsx +17 -0
  37. package/{Title/index.tsx → Layout/components/LayoutTitle.tsx} +24 -15
  38. package/Layout/context/layoutContext.tsx +7 -0
  39. package/Layout/hooks/useScrollY.tsx +6 -0
  40. package/Layout/index.ts +5 -0
  41. package/Layout/types.ts +5 -0
  42. package/LayoutDefault/components/LayoutDefault.tsx +90 -0
  43. package/LayoutDefault/index.ts +1 -0
  44. package/LayoutOverlay/components/LayoutOverlay.tsx +25 -0
  45. package/LayoutOverlay/components/LayoutOverlayBase.tsx +362 -0
  46. package/LayoutOverlay/components/LayoutOverlayHeader.tsx +5 -0
  47. package/LayoutOverlay/hooks/useOverlayPosition.ts +70 -0
  48. package/LayoutOverlay/index.ts +2 -0
  49. package/Page/App.tsx +2 -0
  50. package/PageMeta/index.tsx +3 -0
  51. package/Pagination/index.tsx +0 -1
  52. package/Row/ButtonLinkList/index.tsx +1 -1
  53. package/Row/ColumnOneBoxed/index.tsx +1 -1
  54. package/Row/ColumnTwoWithTop/index.tsx +3 -3
  55. package/Row/ContentLinks/index.tsx +3 -3
  56. package/Row/HeroBanner/index.tsx +7 -11
  57. package/Row/IconBlocks/index.tsx +1 -1
  58. package/Row/ImageText/index.tsx +1 -1
  59. package/Row/ImageTextBoxed/index.tsx +1 -1
  60. package/Row/ParagraphWithSidebarSlide/index.tsx +1 -1
  61. package/Row/SpecialBanner/index.tsx +4 -3
  62. package/Row/index.tsx +1 -1
  63. package/Snackbar/MessageSnackbarImpl.tsx +1 -1
  64. package/StarRatingField/index.tsx +3 -4
  65. package/Stepper/Stepper.tsx +1 -1
  66. package/Styles/breakpointVal.tsx +2 -2
  67. package/Styles/classesPicker.ts +41 -0
  68. package/Styles/responsiveVal.tsx +1 -1
  69. package/SvgImage/SvgImageSimple.tsx +9 -8
  70. package/SvgImage/index.tsx +9 -11
  71. package/TextInputNumber/index.tsx +3 -4
  72. package/Theme/types.ts +14 -12
  73. package/ToggleButton/index.tsx +2 -4
  74. package/UspList/UspListItem.tsx +1 -1
  75. package/index.ts +9 -43
  76. package/package.json +8 -9
  77. package/AppShell/AppShellHeader/appShellHeaderContext.tsx +0 -11
  78. package/AppShell/AppShellHeader/index.tsx +0 -439
  79. package/AppShell/AppShellHeader/useAppShellHeaderContext.tsx +0 -6
  80. package/AppShell/AppShellProvider/index.tsx +0 -18
  81. package/AppShell/AppShellTitle/index.tsx +0 -45
  82. package/AppShell/ForwardButton.tsx +0 -53
  83. package/AppShell/FullPageShellBase.tsx +0 -82
  84. package/AppShell/MinimalPageShellBase.tsx +0 -22
  85. package/AppShell/PageShellHeader/index.tsx +0 -14
  86. package/AppShell/SheetShellBase/index.tsx +0 -114
  87. package/AppShell/SheetShellBase/useSheetStyles.ts +0 -18
  88. package/AppShell/SheetShellDragIndicator/index.tsx +0 -55
  89. package/AppShell/SheetShellHeader/index.tsx +0 -28
  90. package/AppShell/ShellBase.tsx +0 -45
  91. package/Debug/DebugSpacer.tsx +0 -51
  92. package/FramerNextPagesSlider/Slide.tsx +0 -71
  93. package/FramerNextPagesSlider/Slider.tsx +0 -39
  94. package/FramerNextPagesSlider/index.ts +0 -1
  95. package/FramerNextPagesSlider/types.ts +0 -3
@@ -1,10 +1,10 @@
1
1
  import { makeStyles, Theme, Typography, TypographyProps } from '@material-ui/core'
2
- import clsx from 'clsx'
3
2
  import React from 'react'
4
- import { responsiveVal } from '..'
5
- import { UseStyles } from '../Styles'
6
- import SvgImage, { SvgImageProps } from '../SvgImage'
7
- import SvgImageSimple from '../SvgImage/SvgImageSimple'
3
+ import { UseStyles } from '../../Styles'
4
+ import { classesPicker } from '../../Styles/classesPicker'
5
+ import { responsiveVal } from '../../Styles/responsiveVal'
6
+ import { SvgImageProps } from '../../SvgImage'
7
+ import SvgImageSimple from '../../SvgImage/SvgImageSimple'
8
8
 
9
9
  const useStyles = makeStyles(
10
10
  (theme: Theme) => ({
@@ -18,8 +18,7 @@ const useStyles = makeStyles(
18
18
  flexFlow: 'column',
19
19
  },
20
20
  },
21
- typography: {},
22
- small: {
21
+ containerSizeSmall: {
23
22
  flexFlow: 'unset',
24
23
  '& svg': {
25
24
  width: responsiveVal(24, 28),
@@ -27,10 +26,15 @@ const useStyles = makeStyles(
27
26
  strokeWidth: 1.4,
28
27
  },
29
28
  },
29
+ containerGutterTop: {
30
+ marginTop: theme.spacings.xl,
31
+ },
32
+ containerGutterBottom: {
33
+ marginBottom: theme.spacings.lg,
34
+ },
35
+ typography: {},
30
36
  }),
31
- {
32
- name: 'Title',
33
- },
37
+ { name: 'Title' },
34
38
  )
35
39
 
36
40
  export type TitleProps = {
@@ -38,17 +42,24 @@ export type TitleProps = {
38
42
  icon?: SvgImageProps['src']
39
43
  size?: 'small' | 'medium'
40
44
  variant?: TypographyProps['variant']
45
+ gutterTop?: boolean
46
+ gutterBottom?: boolean
41
47
  component?: React.ElementType
42
48
  } & UseStyles<typeof useStyles>
43
49
 
44
- const Title = React.forwardRef<HTMLDivElement, TitleProps>((props, ref) => {
50
+ export const LayoutTitle = React.forwardRef<HTMLDivElement, TitleProps>((props, ref) => {
45
51
  const { children, icon, size = 'medium', component, variant } = props
46
52
  const classes = useStyles(props)
47
53
  const small = size === 'small'
48
54
 
55
+ const gutterTop = !!(props.gutterTop ?? size !== 'small')
56
+ const gutterBottom = !!(props.gutterBottom ?? size !== 'small')
57
+
58
+ const className = classesPicker(classes, { size, gutterBottom, gutterTop })
59
+
49
60
  return (
50
- <div className={clsx(classes.container, small && classes.small)}>
51
- {icon && <SvgImageSimple src={icon} size='xl' />}
61
+ <div {...className('container')}>
62
+ {icon && <SvgImageSimple src={icon} size={small ? 'large' : 'xl'} />}
52
63
  <Typography
53
64
  ref={ref}
54
65
  variant={variant || (small ? 'h6' : 'h3')}
@@ -60,5 +71,3 @@ const Title = React.forwardRef<HTMLDivElement, TitleProps>((props, ref) => {
60
71
  </div>
61
72
  )
62
73
  })
63
-
64
- export default Title
@@ -0,0 +1,7 @@
1
+ import React from 'react'
2
+ import { LayoutContext } from '../types'
3
+
4
+ const layoutContext = React.createContext(undefined as unknown as LayoutContext)
5
+ layoutContext.displayName = 'layoutContext'
6
+
7
+ export default layoutContext
@@ -0,0 +1,6 @@
1
+ import { useContext } from 'react'
2
+ import layoutContext from '../context/layoutContext'
3
+
4
+ export function useScrollY() {
5
+ return useContext(layoutContext).scroll
6
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types'
2
+ export * from './components/LayoutHeader'
3
+ export * from './components/LayoutProvider'
4
+ export * from './components/LayoutTitle'
5
+ export * from './hooks/useScrollY'
@@ -0,0 +1,5 @@
1
+ import { MotionValue } from 'framer-motion'
2
+
3
+ export type LayoutContext = {
4
+ scroll: MotionValue<number>
5
+ }
@@ -0,0 +1,90 @@
1
+ import { usePageRouter, useScrollOffset } from '@graphcommerce/framer-next-pages'
2
+ import { makeStyles, Theme } from '@material-ui/core'
3
+ import clsx from 'clsx'
4
+ import { useTransform, useViewportScroll } from 'framer-motion'
5
+ import React from 'react'
6
+ import LayoutProvider from '../../Layout/components/LayoutProvider'
7
+ import { UseStyles } from '../../Styles'
8
+
9
+ const useStyles = makeStyles(
10
+ (theme: Theme) => ({
11
+ root: {
12
+ minHeight: '100vh',
13
+ '@supports (-webkit-touch-callout: none)': {
14
+ minHeight: '-webkit-fill-available',
15
+ },
16
+ display: 'grid',
17
+ gridTemplateRows: `auto 1fr auto`,
18
+ gridTemplateColumns: '100%',
19
+ background: theme.palette.background.default,
20
+ },
21
+ hideFabsOnVirtualKeyboardOpen: {
22
+ [theme.breakpoints.down('sm')]: {
23
+ '@media (max-height: 530px)': {
24
+ display: 'none',
25
+ },
26
+ },
27
+ },
28
+ header: {
29
+ zIndex: theme.zIndex.appBar - 1,
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ height: theme.appShell.headerHeightSm,
34
+ pointerEvents: 'none',
35
+ '& > *': {
36
+ pointerEvents: 'all',
37
+ },
38
+ [theme.breakpoints.up('md')]: {
39
+ height: theme.appShell.headerHeightMd,
40
+ padding: `0 ${theme.page.horizontal} 0`,
41
+ top: 0,
42
+ display: 'flex',
43
+ justifyContent: 'left',
44
+ width: '100%',
45
+ },
46
+ },
47
+ headerSticky: {
48
+ [theme.breakpoints.down('sm')]: {
49
+ position: 'sticky',
50
+ top: 0,
51
+ },
52
+ },
53
+ }),
54
+ { name: 'LayoutDefault' },
55
+ )
56
+
57
+ export type LayoutDefaultProps = {
58
+ header: React.ReactNode
59
+ footer: React.ReactNode
60
+ menuFab?: React.ReactNode
61
+ cartFab?: React.ReactNode
62
+ children?: React.ReactNode
63
+ noSticky?: boolean
64
+ } & UseStyles<typeof useStyles>
65
+
66
+ export function LayoutDefault(props: LayoutDefaultProps) {
67
+ const { children, header, footer, menuFab, cartFab, noSticky } = props
68
+ const classes = useStyles(props)
69
+
70
+ const offset = useScrollOffset().y
71
+ const scrollWithOffset = useTransform(useViewportScroll().scrollY, (y) => y + offset)
72
+
73
+ return (
74
+ <div className={classes.root}>
75
+ <LayoutProvider scroll={scrollWithOffset}>
76
+ <header className={clsx(classes.header, !noSticky && classes.headerSticky)}>
77
+ {header}
78
+ </header>
79
+ <div>
80
+ <div className={classes.hideFabsOnVirtualKeyboardOpen}>
81
+ {menuFab}
82
+ {cartFab}
83
+ </div>
84
+ {children}
85
+ </div>
86
+ <div>{footer}</div>
87
+ </LayoutProvider>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1 @@
1
+ export * from './components/LayoutDefault'
@@ -0,0 +1,25 @@
1
+ import { ScrollerProvider, ScrollSnapType } from '@graphcommerce/framer-scroller'
2
+ import React from 'react'
3
+ import { SetOptional } from 'type-fest'
4
+ import { LayoutOverlayBase, LayoutOverlayBaseProps } from './LayoutOverlayBase'
5
+
6
+ export type { LayoutOverlayVariant } from './LayoutOverlayBase'
7
+
8
+ export type LayoutOverlayProps = SetOptional<LayoutOverlayBaseProps, 'variantSm' | 'variantMd'>
9
+
10
+ export function LayoutOverlay(props: LayoutOverlayProps) {
11
+ const { children, variantSm = 'bottom', variantMd = 'right', classes } = props
12
+
13
+ const scrollSnapTypeSm: ScrollSnapType =
14
+ variantSm === 'left' || variantSm === 'right' ? 'both mandatory' : 'block proximity'
15
+ const scrollSnapTypeMd: ScrollSnapType =
16
+ variantMd === 'left' || variantMd === 'right' ? 'both mandatory' : 'block proximity'
17
+
18
+ return (
19
+ <ScrollerProvider scrollSnapTypeSm={scrollSnapTypeSm} scrollSnapTypeMd={scrollSnapTypeMd}>
20
+ <LayoutOverlayBase variantMd={variantMd} variantSm={variantSm} classes={classes}>
21
+ {children}
22
+ </LayoutOverlayBase>
23
+ </ScrollerProvider>
24
+ )
25
+ }
@@ -0,0 +1,362 @@
1
+ import { usePageContext, usePageRouter, useScrollOffset } from '@graphcommerce/framer-next-pages'
2
+ import { Scroller, useScrollerContext, useScrollTo } from '@graphcommerce/framer-scroller'
3
+ import { useElementScroll, useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
4
+ import { makeStyles, Theme, ClickAwayListener } from '@material-ui/core'
5
+ import {
6
+ m,
7
+ MotionValue,
8
+ useDomEvent,
9
+ useMotionValue,
10
+ usePresence,
11
+ useTransform,
12
+ } from 'framer-motion'
13
+ import React, { useCallback, useEffect, useRef } from 'react'
14
+ import LayoutProvider from '../../Layout/components/LayoutProvider'
15
+ import { UseStyles } from '../../Styles'
16
+ import { classesPicker } from '../../Styles/classesPicker'
17
+ import { useOverlayPosition } from '../hooks/useOverlayPosition'
18
+
19
+ const useStyles = makeStyles(
20
+ (theme: Theme) => ({
21
+ root: {
22
+ display: 'grid',
23
+ cursor: 'default',
24
+ overflow: 'auto',
25
+ height: '100vh',
26
+ '@supports (-webkit-touch-callout: none)': {
27
+ height: '-webkit-fill-available',
28
+ },
29
+ [theme.breakpoints.down('sm')]: {
30
+ width: '100vw',
31
+ borderRadius: theme.shape.borderRadius * 3,
32
+ },
33
+ [theme.breakpoints.up('md')]: {
34
+ width: '100vw',
35
+ borderRadius: theme.shape.borderRadius * 4,
36
+ },
37
+ },
38
+ rootVariantSmLeft: {
39
+ [theme.breakpoints.down('sm')]: {
40
+ gridTemplate: `
41
+ "overlay beforeOverlay"
42
+ "afterOverlay afterOverlay"
43
+ `,
44
+ },
45
+ },
46
+ rootVariantMdLeft: {
47
+ [theme.breakpoints.up('md')]: {
48
+ gridTemplate: `
49
+ "overlay beforeOverlay"
50
+ "afterOverlay afterOverlay"
51
+ `,
52
+ },
53
+ },
54
+ rootVariantSmRight: {
55
+ [theme.breakpoints.down('sm')]: {
56
+ gridTemplate: `
57
+ "beforeOverlay overlay"
58
+ "afterOverlay afterOverlay"
59
+ `,
60
+ },
61
+ },
62
+ rootVariantMdRight: {
63
+ [theme.breakpoints.up('md')]: {
64
+ gridTemplate: `
65
+ "beforeOverlay overlay"
66
+ "afterOverlay afterOverlay"
67
+ `,
68
+ },
69
+ },
70
+ rootVariantSmBottom: {
71
+ [theme.breakpoints.down('sm')]: {
72
+ gridTemplate: `"beforeOverlay" "overlay" "afterOverlay"`,
73
+ height: '100vh',
74
+ '@supports (-webkit-touch-callout: none)': {
75
+ height: '-webkit-fill-available',
76
+ },
77
+ },
78
+ },
79
+ rootVariantMdBottom: {
80
+ [theme.breakpoints.up('md')]: {
81
+ gridTemplate: `"beforeOverlay" "overlay" "afterOverlay"`,
82
+ height: '100vh',
83
+ },
84
+ },
85
+ beforeOverlay: {
86
+ gridArea: 'beforeOverlay',
87
+ scrollSnapAlign: 'start',
88
+ scrollSnapStop: 'always',
89
+ display: 'grid',
90
+ alignContent: 'end',
91
+ },
92
+ beforeOverlayVariantSmRight: {
93
+ [theme.breakpoints.down('sm')]: {
94
+ width: '100vw',
95
+ },
96
+ },
97
+ beforeOverlayVariantMdRight: {
98
+ [theme.breakpoints.up('md')]: {
99
+ width: '100vw',
100
+ },
101
+ },
102
+
103
+ beforeOverlayVariantSmLeft: {
104
+ [theme.breakpoints.down('sm')]: {
105
+ width: '100vw',
106
+ },
107
+ },
108
+ beforeOverlayVariantMdLeft: {
109
+ [theme.breakpoints.up('md')]: {
110
+ width: '100vw',
111
+ },
112
+ },
113
+
114
+ beforeOverlayVariantMdBottom: {
115
+ [theme.breakpoints.up('md')]: {
116
+ height: '100vh',
117
+ },
118
+ },
119
+ beforeOverlayVariantSmBottom: {
120
+ [theme.breakpoints.down('sm')]: {
121
+ height: '100vh',
122
+ '@supports (-webkit-touch-callout: none)': {
123
+ height: '-webkit-fill-available',
124
+ },
125
+ },
126
+ },
127
+ overlay: {
128
+ pointerEvents: 'none',
129
+ gridArea: 'overlay',
130
+ scrollSnapAlign: 'start',
131
+ width: 'min-content',
132
+ minHeight: '100vh',
133
+ '@supports (-webkit-touch-callout: none)': {
134
+ minHeight: '-webkit-fill-available',
135
+ },
136
+ },
137
+ overlayVariantSmBottom: {
138
+ [theme.breakpoints.down('sm')]: {
139
+ marginTop: `calc(${theme.appShell.headerHeightSm} * 0.5 * -1)`,
140
+ paddingTop: `calc(${theme.appShell.headerHeightSm} * 0.5)`,
141
+ scrollSnapStop: 'always',
142
+ display: 'grid',
143
+ },
144
+ },
145
+ overlayVariantMdBottom: {
146
+ [theme.breakpoints.up('md')]: {
147
+ marginTop: `calc(${theme.appShell.headerHeightMd} + (${theme.appShell.appBarHeightMd} - ${theme.appShell.appBarInnerHeightMd}) * 0.5)`,
148
+ paddingTop: `calc(${theme.appShell.headerHeightMd} + (${theme.appShell.appBarHeightMd} - ${theme.appShell.appBarInnerHeightMd}) * -0.5)`,
149
+ scrollSnapAlign: 'start',
150
+ scrollSnapStop: 'always',
151
+ display: 'grid',
152
+ },
153
+ },
154
+ overlayPane: {
155
+ pointerEvents: 'all',
156
+ backgroundColor: theme.palette.background.paper,
157
+ boxShadow: theme.shadows[24],
158
+ minWidth: 'min(800px, 90vw)',
159
+ },
160
+ overlayPaneVariantSmBottom: {
161
+ [theme.breakpoints.down('sm')]: {
162
+ width: '100vw',
163
+ borderRadius: theme.shape.borderRadius * 3,
164
+ },
165
+ },
166
+ overlayPaneVariantMdBottom: {
167
+ [theme.breakpoints.up('md')]: {
168
+ width: '100vw',
169
+ borderRadius: theme.shape.borderRadius * 4,
170
+ },
171
+ },
172
+ overlayPaneVariantSmLeft: {
173
+ [theme.breakpoints.down('sm')]: {
174
+ paddingBottom: 1,
175
+ minHeight: '100vh',
176
+ '@supports (-webkit-touch-callout: none)': {
177
+ minHeight: '-webkit-fill-available',
178
+ },
179
+ },
180
+ },
181
+ overlayPaneVariantMdLeft: {
182
+ [theme.breakpoints.up('md')]: {
183
+ paddingBottom: 1,
184
+ minHeight: '100vh',
185
+ '@supports (-webkit-touch-callout: none)': {
186
+ minHeight: '-webkit-fill-available',
187
+ },
188
+ },
189
+ },
190
+ overlayPaneVariantSmRight: {
191
+ [theme.breakpoints.down('sm')]: {
192
+ paddingBottom: 1,
193
+ minHeight: '100vh',
194
+ '@supports (-webkit-touch-callout: none)': {
195
+ minHeight: '-webkit-fill-available',
196
+ },
197
+ },
198
+ },
199
+ overlayPaneVariantMdRight: {
200
+ [theme.breakpoints.up('md')]: {
201
+ paddingBottom: 1,
202
+ minHeight: '100vh',
203
+ '@supports (-webkit-touch-callout: none)': {
204
+ minHeight: '-webkit-fill-available',
205
+ },
206
+ },
207
+ },
208
+ afterOverlay: {
209
+ gridArea: 'afterOverlay',
210
+ scrollSnapAlign: 'start',
211
+ },
212
+ backdrop: {
213
+ zIndex: -1,
214
+ position: 'fixed',
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ right: 0,
219
+ bottom: 0,
220
+ top: 0,
221
+ left: 0,
222
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
223
+ WebkitTapHighlightColor: 'transparent',
224
+ },
225
+ }),
226
+ { name: 'Overlay' },
227
+ )
228
+
229
+ export type LayoutOverlayVariant = 'left' | 'bottom' | 'right'
230
+
231
+ export type LayoutOverlayBaseProps = {
232
+ children?: React.ReactNode
233
+ variantSm: LayoutOverlayVariant
234
+ variantMd: LayoutOverlayVariant
235
+ } & UseStyles<typeof useStyles>
236
+
237
+ export enum OverlayPosition {
238
+ UNOPENED = -1,
239
+ OPENED = 1,
240
+ CLOSED = 0,
241
+ }
242
+
243
+ export function LayoutOverlayBase(props: LayoutOverlayBaseProps) {
244
+ const { children, variantSm, variantMd } = props
245
+ const { scrollerRef } = useScrollerContext()
246
+ const positions = useOverlayPosition()
247
+ const scrollTo = useScrollTo()
248
+ const [isPresent, safeToRemove] = usePresence()
249
+
250
+ const { closeSteps, active, direction } = usePageContext()
251
+ const pageRouter = usePageRouter()
252
+
253
+ const position = useMotionValue<OverlayPosition>(OverlayPosition.UNOPENED)
254
+
255
+ const classes = useStyles(props)
256
+ const className = classesPicker(classes, { variantSm, variantMd })
257
+
258
+ const overlayRef = useRef<HTMLDivElement>(null)
259
+
260
+ const scroll = useElementScroll(scrollerRef)
261
+
262
+ useIsomorphicLayoutEffect(() => {
263
+ const scroller = scrollerRef.current
264
+ if (!scroller || !isPresent) return
265
+
266
+ const open = { x: positions.open.x.get(), y: positions.open.y.get() }
267
+
268
+ if (direction === 1 && position.get() !== OverlayPosition.OPENED) {
269
+ scroller.scrollLeft = positions.closed.x.get()
270
+ scroller.scrollTop = positions.closed.y.get()
271
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
272
+ scrollTo(open).then(() => {
273
+ scroller.scrollLeft = positions.open.x.get()
274
+ scroller.scrollTop = positions.open.y.get()
275
+ position.set(OverlayPosition.OPENED)
276
+ })
277
+ } else {
278
+ scroller.scrollLeft = open.x
279
+ scroller.scrollTop = open.y
280
+ }
281
+ }, [direction, isPresent, position, positions, scrollTo, scrollerRef])
282
+
283
+ // Make sure the overlay stays open when resizing the window.
284
+ useEffect(() => {
285
+ const scroller = scrollerRef.current
286
+ if (!scroller) return () => {}
287
+
288
+ const resize = () => {
289
+ if (positions.open.visible.get() !== 1) return
290
+ scroller.scrollLeft = positions.open.x.get()
291
+ scroller.scrollTop = positions.open.y.get()
292
+ }
293
+
294
+ window.addEventListener('resize', resize)
295
+ return () => window.removeEventListener('resize', resize)
296
+ // We're not checking for all deps, because that will cause rerenders.
297
+ // The scroller context shouldn't be changing, but at the moment it is.
298
+ }, [positions, scrollerRef])
299
+
300
+ useEffect(() => {
301
+ if (isPresent) return
302
+
303
+ position.set(OverlayPosition.CLOSED)
304
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
305
+ scrollTo({
306
+ x: positions.closed.x.get(),
307
+ y: positions.closed.y.get(),
308
+ }).then(() => safeToRemove?.())
309
+ }, [isPresent, position, positions, safeToRemove, scrollTo])
310
+
311
+ // Only go back to a previous page if the overlay isn't closed.
312
+ const closeOverlay = useCallback(() => {
313
+ if (position.get() !== OverlayPosition.OPENED) return
314
+ position.set(OverlayPosition.CLOSED)
315
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
316
+ pageRouter.go(closeSteps * -1)
317
+ }, [closeSteps, pageRouter, position])
318
+
319
+ // Handle escape key
320
+ const windowRef = useRef(typeof window !== 'undefined' ? window : null)
321
+ const handleEscape = (e: KeyboardEvent | Event) => {
322
+ if (active && (e as KeyboardEvent)?.key === 'Escape') closeOverlay()
323
+ }
324
+ useDomEvent(windowRef, 'keyup', handleEscape, { passive: true })
325
+
326
+ // When the overlay isn't visible anymore, we navigate back.
327
+ useEffect(() => positions.open.visible.onChange((o) => o === 0 && closeOverlay()))
328
+
329
+ // Measure the offset of the overlay in the scroller.
330
+
331
+ const offsetY = useMotionValue(0)
332
+ useEffect(() => {
333
+ if (!overlayRef.current) return () => {}
334
+ const ro = new ResizeObserver(([entry]) => offsetY.set(entry.contentRect.top))
335
+ ro.observe(overlayRef.current)
336
+ return () => ro.disconnect()
337
+ }, [offsetY])
338
+
339
+ // Create the exact position for the LayoutProvider which offsets the top of the overlay
340
+ const offsetPageY = useScrollOffset().y
341
+ const scrollWithoffset = useTransform(
342
+ [scroll.y, positions.open.y, offsetY] as MotionValue<number | string>[],
343
+ ([y, openY, offsetYv]: number[]) => Math.max(0, y - openY - offsetYv + offsetPageY),
344
+ )
345
+
346
+ return (
347
+ <>
348
+ <m.div {...className('backdrop')} style={{ opacity: positions.open.visible }} />
349
+ <Scroller {...className('root')} grid={false} hideScrollbar>
350
+ <div {...className('beforeOverlay')} />
351
+ <div {...className('overlay')} ref={overlayRef}>
352
+ <ClickAwayListener onClickAway={closeOverlay}>
353
+ <div {...className('overlayPane')}>
354
+ <LayoutProvider scroll={scrollWithoffset}>{children}</LayoutProvider>
355
+ </div>
356
+ </ClickAwayListener>
357
+ </div>
358
+ <div {...className('afterOverlay')} />
359
+ </Scroller>
360
+ </>
361
+ )
362
+ }
@@ -0,0 +1,5 @@
1
+ import { LayoutHeaderProps, LayoutHeader } from '../../Layout'
2
+
3
+ export function LayoutOverlayHeader(props: LayoutHeaderProps) {
4
+ return <LayoutHeader {...props} noAlign />
5
+ }
@@ -0,0 +1,70 @@
1
+ import { useScrollerContext } from '@graphcommerce/framer-scroller'
2
+ import {
3
+ useConstant,
4
+ useElementScroll,
5
+ useIsomorphicLayoutEffect,
6
+ } from '@graphcommerce/framer-utils'
7
+ import { motionValue } from 'framer-motion'
8
+ import { useEffect } from 'react'
9
+
10
+ export function useOverlayPosition() {
11
+ const { getScrollSnapPositions, scrollerRef } = useScrollerContext()
12
+
13
+ const state = useConstant(() => ({
14
+ open: {
15
+ x: motionValue(0),
16
+ y: motionValue(0),
17
+ visible: motionValue(0),
18
+ },
19
+ closed: { x: motionValue(0), y: motionValue(0) },
20
+ }))
21
+
22
+ const scroll = useElementScroll(scrollerRef)
23
+
24
+ useIsomorphicLayoutEffect(() => {
25
+ if (!scrollerRef.current) return () => {}
26
+
27
+ const measure = () => {
28
+ const positions = getScrollSnapPositions()
29
+ state.open.x.set(positions.x[1] ?? 0)
30
+ state.closed.x.set(positions.x[0])
31
+ state.open.y.set(positions.y[1] ?? 0)
32
+ state.closed.y.set(positions.y[0])
33
+ }
34
+ const ro = new ResizeObserver(measure)
35
+ measure()
36
+
37
+ ro.observe(scrollerRef.current)
38
+ return () => ro.disconnect()
39
+ })
40
+
41
+ // sets a float between 0 and 1 for the visibility of the overlay
42
+ useEffect(() => {
43
+ const calc = () => {
44
+ const x = scroll.x.get()
45
+ const y = scroll.y.get()
46
+
47
+ const yC = state.closed.y.get()
48
+ const yO = state.open.y.get()
49
+ const visY = yC === yO ? 1 : Math.max(0, Math.min(1, (y - yC) / (yO - yC)))
50
+
51
+ const xC = state.closed.x.get()
52
+ const xO = state.open.x.get()
53
+ const visX = xO === xC ? 1 : Math.max(0, Math.min(1, (x - xC) / (xO - xC)))
54
+
55
+ // todo: visibility sometimes flickers
56
+ state.open.visible.set(visY * visX)
57
+ }
58
+
59
+ const cancelY = scroll.y.onChange(calc)
60
+ const cancelX = scroll.x.onChange(calc)
61
+ calc()
62
+
63
+ return () => {
64
+ cancelY()
65
+ cancelX()
66
+ }
67
+ }, [state, scroll])
68
+
69
+ return state
70
+ }
@@ -0,0 +1,2 @@
1
+ export * from './components/LayoutOverlay'
2
+ export * from './components/LayoutOverlayHeader'
package/Page/App.tsx CHANGED
@@ -2,6 +2,7 @@ import { FramerNextPages } from '@graphcommerce/framer-next-pages'
2
2
  import { LazyMotion } from 'framer-motion'
3
3
  import { AppPropsType } from 'next/dist/shared/lib/utils'
4
4
  import React, { useEffect } from 'react'
5
+ import PageLoadIndicator from '../PageLoadIndicator'
5
6
  import { AppProps } from './types'
6
7
 
7
8
  export default function App(props: AppProps & AppPropsType) {
@@ -9,6 +10,7 @@ export default function App(props: AppProps & AppPropsType) {
9
10
 
10
11
  return (
11
12
  <LazyMotion features={async () => (await import('./framerFeatures')).default} strict>
13
+ <PageLoadIndicator />
12
14
  <FramerNextPages {...props} />
13
15
  </LazyMotion>
14
16
  )