@graphcommerce/next-ui 8.1.0-canary.8 → 9.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 (83) hide show
  1. package/ActionCard/ActionCard.tsx +66 -39
  2. package/ActionCard/ActionCardAccordion.tsx +2 -1
  3. package/ActionCard/ActionCardLayout.tsx +1 -1
  4. package/ActionCard/ActionCardList.tsx +11 -10
  5. package/ActionCard/index.ts +0 -1
  6. package/Blog/BlogAuthor/BlogAuthor.tsx +2 -4
  7. package/Blog/BlogListItem/BlogListItem.tsx +2 -4
  8. package/Blog/BlogTags/BlogTag.tsx +4 -1
  9. package/Breadcrumbs/Breadcrumbs.tsx +195 -0
  10. package/Breadcrumbs/BreadcrumbsJsonLd.tsx +13 -0
  11. package/Breadcrumbs/BreadcrumbsList.tsx +101 -0
  12. package/Breadcrumbs/BreadcrumbsPopper.tsx +48 -0
  13. package/Breadcrumbs/index.ts +4 -0
  14. package/Breadcrumbs/jsonLdBreadcrumb.tsx +19 -0
  15. package/Breadcrumbs/types.ts +11 -0
  16. package/CHANGELOG.md +341 -98
  17. package/Config.graphqls +5 -0
  18. package/Document/DocumentBodyEnd.tsx +7 -0
  19. package/Document/DocumentBodyStart.tsx +7 -0
  20. package/Document/DocumentHeadEnd.tsx +7 -0
  21. package/Document/DocumentHeadStart.tsx +7 -0
  22. package/Document/index.ts +4 -0
  23. package/Footer/Footer.tsx +1 -1
  24. package/FramerScroller/SidebarGallery.tsx +54 -27
  25. package/FullPageMessage/FullPageMessage.tsx +1 -0
  26. package/Intl/DateTimeFormat/DateFormat.tsx +10 -0
  27. package/Intl/DateTimeFormat/DateTimeFormat.tsx +20 -0
  28. package/Intl/DateTimeFormat/TimeFormat.tsx +10 -0
  29. package/Intl/DateTimeFormat/index.ts +3 -0
  30. package/Intl/DateTimeFormat/toDate.ts +16 -0
  31. package/Intl/DisplayNames/DisplayNames.tsx +22 -0
  32. package/Intl/DisplayNames/index.ts +1 -0
  33. package/Intl/ListFormat.tsx +33 -0
  34. package/Intl/NumberFormat/CurrencyFormat.tsx +191 -0
  35. package/Intl/NumberFormat/NumberFormat.tsx +24 -0
  36. package/Intl/NumberFormat/PercentFormat.tsx +5 -0
  37. package/Intl/NumberFormat/UnitFormat.tsx +59 -0
  38. package/Intl/NumberFormat/index.ts +4 -0
  39. package/Intl/RelativeTimeFormat/RelativeTimeFormat.tsx +34 -0
  40. package/Intl/RelativeTimeFormat/RelativeToTimeFormat.tsx +35 -0
  41. package/Intl/RelativeTimeFormat/index.ts +2 -0
  42. package/Intl/RelativeTimeFormat/relativeTimeFormatAutoUnit.ts +23 -0
  43. package/Intl/index.ts +11 -0
  44. package/JsonLd/JsonLd.tsx +3 -2
  45. package/Layout/components/LayoutHeader.tsx +30 -10
  46. package/Layout/components/LayoutHeaderBack.tsx +20 -9
  47. package/Layout/components/LayoutHeaderClose.tsx +8 -2
  48. package/Layout/components/LayoutTitle.tsx +0 -5
  49. package/LayoutDefault/components/LayoutDefault.tsx +15 -13
  50. package/LazyHydrate/LazyHydrate.tsx +19 -3
  51. package/Overlay/components/OverlayBase.tsx +31 -6
  52. package/Overlay/components/OverlayHeader.tsx +27 -0
  53. package/Overlay/components/index.ts +1 -0
  54. package/PageLoadIndicator/PageLoadIndicator.tsx +25 -14
  55. package/PageMeta/PageMeta.tsx +1 -71
  56. package/PageMeta/canonicalize.ts +78 -0
  57. package/Pagination/PaginationExtended.tsx +103 -0
  58. package/Snackbar/MessageSnackbarImpl.tsx +60 -19
  59. package/Styles/responsiveVal.tsx +4 -5
  60. package/Styles/withEmotionCache.tsx +2 -2
  61. package/Theme/DarkLightModeThemeProvider.tsx +41 -19
  62. package/Theme/MuiButton.ts +11 -3
  63. package/TimeAgo/TimeAgo.tsx +3 -0
  64. package/hooks/index.ts +2 -1
  65. package/hooks/memoDeep.ts +38 -0
  66. package/hooks/useDateTimeFormat.ts +4 -4
  67. package/hooks/{useSsr.ts → useIsSsr.ts} +3 -0
  68. package/hooks/useLocale.ts +2 -2
  69. package/hooks/useNumberFormat.ts +4 -3
  70. package/hooks/useStorefrontConfig.ts +1 -9
  71. package/hooks/useUrlQuery.ts +6 -4
  72. package/icons.ts +55 -0
  73. package/index.ts +11 -2
  74. package/package.json +14 -14
  75. package/server.ts +9 -0
  76. package/utils/cssFlags.tsx +53 -0
  77. package/utils/getCssFlagInitScript.tsx +20 -0
  78. package/utils/normalizeLocale.ts +26 -0
  79. package/utils/robots.ts +41 -0
  80. package/utils/sitemap.ts +47 -0
  81. package/utils/storefrontConfig.ts +8 -0
  82. package/ActionCard/ActionCardListForm.tsx +0 -88
  83. package/icons/index.ts +0 -48
@@ -13,7 +13,7 @@ import { extendableComponent, responsiveVal } from '../Styles'
13
13
  import { breakpointVal } from '../Styles/breakpointVal'
14
14
 
15
15
  type Variants = 'outlined' | 'default'
16
- type Size = 'large' | 'medium' | 'small'
16
+ type Size = 'large' | 'medium' | 'small' | 'responsive'
17
17
  type Color = 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
18
18
  type Layout = 'inline' | 'grid' | 'list' | 'stack'
19
19
 
@@ -37,9 +37,9 @@ export type ActionCardProps = {
37
37
  price?: React.ReactNode
38
38
  after?: React.ReactNode
39
39
  secondaryAction?: React.ReactNode
40
- onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number | null) => void
40
+ onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number | boolean | null) => void
41
41
  selected?: boolean
42
- value: string | number | null
42
+ value: string | number | boolean | null
43
43
  reset?: React.ReactNode
44
44
  disabled?: boolean
45
45
  error?: boolean
@@ -47,6 +47,7 @@ export type ActionCardProps = {
47
47
 
48
48
  const parts = [
49
49
  'root',
50
+ 'rootInner',
50
51
  'image',
51
52
  'title',
52
53
  'action',
@@ -77,6 +78,13 @@ const { withState, selectors } = extendableComponent<StateProps, typeof name, ty
77
78
 
78
79
  export const actionCardSelectors = selectors
79
80
 
81
+ export const actionCardImageSizes = {
82
+ small: responsiveVal(60, 80),
83
+ medium: responsiveVal(60, 80),
84
+ large: responsiveVal(100, 120),
85
+ responsive: responsiveVal(60, 120),
86
+ }
87
+
80
88
  export function ActionCard(props: ActionCardProps) {
81
89
  const {
82
90
  title,
@@ -92,7 +100,7 @@ export function ActionCard(props: ActionCardProps) {
92
100
  selected = false,
93
101
  reset,
94
102
  disabled = false,
95
- size = 'medium',
103
+ size = 'responsive',
96
104
  color = 'primary',
97
105
  variant = 'outlined',
98
106
  layout = 'list',
@@ -140,18 +148,24 @@ export function ActionCard(props: ActionCardProps) {
140
148
  py: responsiveVal(12, 14),
141
149
  display: 'block',
142
150
  },
151
+ '&.sizeResponsive': {
152
+ px: responsiveVal(8, 16),
153
+ py: responsiveVal(4, 14),
154
+ display: { xs: 'flex', md: 'block', lg: 'block' },
155
+ [theme.breakpoints.down('md')]: { typography: 'body2' },
156
+ },
143
157
 
144
158
  '&.variantDefault': {
145
159
  position: 'relative',
146
- '&::after': {
147
- content: '""',
148
- position: 'absolute',
149
- width: '100%',
150
- left: 0,
151
- bottom: '-1px',
152
- borderBottom: `1px solid ${theme.palette.divider}`,
153
- display: 'block',
154
- },
160
+ // '&::after': {
161
+ // content: '""',
162
+ // position: 'absolute',
163
+ // width: '100%',
164
+ // left: 0,
165
+ // bottom: '-1px',
166
+ // borderBottom: `1px solid ${theme.palette.divider}`,
167
+ // display: 'block',
168
+ // },
155
169
  '&.selected': {
156
170
  backgroundColor:
157
171
  theme.palette.mode === 'light'
@@ -165,34 +179,41 @@ export function ActionCard(props: ActionCardProps) {
165
179
  )}`,
166
180
  },
167
181
 
168
- '&.sizeSmall': {
169
- mt: { xs: '2px', sm: '3px', md: '5px' },
170
- mb: { xs: '3px', sm: '4px', md: '6px' },
171
- '&::after': {
172
- mb: { xs: '-2px', sm: '-3px', md: '-5px' },
173
- },
174
- },
175
- '&.sizeMedium': {
176
- mt: { xs: '4px', sm: '5px', md: '6px' },
177
- mb: { xs: '5px', sm: '6px', md: '7px' },
178
- '&::after': {
179
- mb: { xs: '-4px', sm: '-5px', md: '-6px' },
180
- },
181
- },
182
- '&.sizeLarge': {
183
- mt: { xs: '5px', sm: '7px', md: '8px' },
184
- mb: { xs: '6px', sm: '8px', md: '9px' },
185
- '&::after': {
186
- mb: { xs: '-5px', sm: '-7px', md: '-8px' },
187
- },
188
- },
182
+ // '&.sizeSmall': {
183
+ // mt: { xs: '2px', sm: '3px', md: '5px' },
184
+ // mb: { xs: '3px', sm: '4px', md: '6px' },
185
+ // '&::after': {
186
+ // mb: { xs: '-2px', sm: '-3px', md: '-5px' },
187
+ // },
188
+ // },
189
+ // '&.sizeMedium': {
190
+ // mt: { xs: '4px', sm: '5px', md: '6px' },
191
+ // mb: { xs: '5px', sm: '6px', md: '7px' },
192
+ // '&::after': {
193
+ // mb: { xs: '-4px', sm: '-5px', md: '-6px' },
194
+ // },
195
+ // },
196
+ // '&.sizeLarge': {
197
+ // mt: { xs: '5px', sm: '7px', md: '8px' },
198
+ // mb: { xs: '6px', sm: '8px', md: '9px' },
199
+ // '&::after': {
200
+ // mb: { xs: '-5px', sm: '-7px', md: '-8px' },
201
+ // },
202
+ // },
203
+ // '&.sizeResponsive': {
204
+ // mt: responsiveVal(2, 8),
205
+ // mb: responsiveVal(3, 9),
206
+ // '&::after': {
207
+ // mb: responsiveVal(-2, -8),
208
+ // },
209
+ // },
189
210
  },
190
211
 
191
212
  '&.variantOutlined': {
192
213
  backgroundColor: theme.palette.background.paper,
193
214
  boxShadow: `inset 0 0 0 1px ${theme.palette.divider}`,
194
215
  '&:not(:last-of-type)': {
195
- marginBottom: '-1px',
216
+ marginBottom: '-2px',
196
217
  },
197
218
  '&.layoutList': {
198
219
  borderRadius: 0,
@@ -230,7 +251,7 @@ export function ActionCard(props: ActionCardProps) {
230
251
  borderColor: 'transparent',
231
252
  boxShadow: `inset 0 0 0 2px ${theme.palette[color].main}`,
232
253
  },
233
- '&.selected:focus': {
254
+ '&.selected:focus, &.error:focus': {
234
255
  borderColor: 'transparent',
235
256
  boxShadow: `inset 0 0 0 2px ${theme.palette[color].main}, 0 0 0 4px ${alpha(
236
257
  theme.palette[color].main,
@@ -245,12 +266,15 @@ export function ActionCard(props: ActionCardProps) {
245
266
  },
246
267
 
247
268
  '&.error': {
248
- boxShadow: `0 0 0 2px ${theme.palette.error.main}`,
269
+ boxShadow: `inset 0 0 0 2px ${theme.palette.error.main}`,
249
270
  },
250
271
  },
251
272
  '&.selected': {
252
273
  zIndex: 1,
253
274
  },
275
+ '&:focus, &.selected:focus, &.error:focus': {
276
+ zIndex: 2,
277
+ },
254
278
  '&.disabled': {
255
279
  background: theme.palette.action.disabledBackground,
256
280
  opacity: theme.palette.action.disabledOpacity,
@@ -261,6 +285,7 @@ export function ActionCard(props: ActionCardProps) {
261
285
  ]}
262
286
  >
263
287
  <Box
288
+ className={classes.rootInner}
264
289
  sx={{
265
290
  display: 'flex',
266
291
  flexDirection: 'row',
@@ -298,9 +323,10 @@ export function ActionCard(props: ActionCardProps) {
298
323
  <Box
299
324
  className={classes.title}
300
325
  sx={{
301
- '&.sizeSmall': { typography: 'body2' },
326
+ '&.sizeSmall': { typography: 'body1' },
302
327
  '&.sizeMedium': { typography: 'body1' },
303
328
  '&.sizeLarge': { typography: 'h6' },
329
+ '&.sizeResponsive': { typography: { xs: 'body1', md: 'body1', lg: 'body1' } },
304
330
  }}
305
331
  >
306
332
  {title}
@@ -341,8 +367,9 @@ export function ActionCard(props: ActionCardProps) {
341
367
  sx={{
342
368
  textAlign: 'right',
343
369
  typography: 'body1',
344
- '&.sizeMedium': { typographty: 'subtitle1' },
370
+ '&.sizeMedium': { typography: 'subtitle1' },
345
371
  '&.sizeLarge': { typography: 'h6' },
372
+ '&.sizeResponsive': { typography: { xs: 'body1', md: 'subtitle1', lg: 'h6' } },
346
373
  }}
347
374
  >
348
375
  {price}
@@ -29,6 +29,7 @@ export function ActionCardAccordion(props: ActionCardAccordionProps) {
29
29
  '&.Mui-expanded': { my: 0 },
30
30
  '::before': { display: 'none' },
31
31
  border: 'none',
32
+ borderBottom: `1px solid ${theme.palette.divider}`,
32
33
  '&:not(.Mui-expanded)': { borderBottom: `1px solid ${theme.palette.divider}` },
33
34
  }),
34
35
  ...(Array.isArray(sx) ? sx : [sx]),
@@ -39,7 +40,7 @@ export function ActionCardAccordion(props: ActionCardAccordionProps) {
39
40
  expandIcon={<IconSvg src={iconChevronDown} />}
40
41
  sx={{
41
42
  px: 0,
42
- typography: 'h6',
43
+ typography: 'subtitle1',
43
44
  minHeight: 54,
44
45
  '& .MuiAccordionSummary-content': {
45
46
  display: 'flex',
@@ -6,7 +6,7 @@ import { ActionCardProps } from './ActionCard'
6
6
  export type ActionCardLayoutProps = {
7
7
  children?: React.ReactNode
8
8
  } & Pick<ActionCardProps, 'layout'> &
9
- Pick<BoxProps, 'sx' | 'className'>
9
+ Pick<BoxProps, 'sx' | 'className' | 'tabIndex'>
10
10
 
11
11
  const parts = ['root'] as const
12
12
  const name = 'ActionCardLayout'
@@ -12,13 +12,13 @@ import { ActionCardLayout } from './ActionCardLayout'
12
12
  type MultiSelect = {
13
13
  multiple: true
14
14
  collapse?: false
15
- value: (string | number | null)[]
15
+ value: (string | boolean | number | null)[]
16
16
 
17
17
  onChange?: (event: React.MouseEvent<HTMLElement>, value: MultiSelect['value']) => void
18
18
  }
19
19
  type Select = {
20
20
  multiple?: boolean
21
- value: string | number | null
21
+ value: string | boolean | number | null
22
22
  collapse?: boolean
23
23
 
24
24
  /** Value is null when deselected when not required */
@@ -44,6 +44,7 @@ function isValueSelected(
44
44
  candidate?: Select['value'] | MultiSelect['value'],
45
45
  ) {
46
46
  if (candidate === undefined) return false
47
+ if (typeof value === 'boolean') return value
47
48
  if (Array.isArray(candidate)) return candidate.indexOf(value) >= 0
48
49
  return value === candidate
49
50
  }
@@ -57,6 +58,8 @@ const { withState } = extendableComponent<HoistedActionCardProps, typeof name, t
57
58
  parts,
58
59
  )
59
60
 
61
+ const isNotFrag = <T extends React.ReactElement>(el: T): el is T => !isFragment(el)
62
+
60
63
  export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListProps>(
61
64
  (props, ref) => {
62
65
  const {
@@ -115,17 +118,15 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
115
118
  .filter(React.isValidElement)
116
119
  .filter(isActionCardLike)
117
120
  .filter((child) => {
121
+ const notFrag = isNotFrag(child)
118
122
  if (process.env.NODE_ENV !== 'production') {
119
- if (isFragment(child))
123
+ if (!notFrag)
120
124
  console.error(
121
- [
122
- "@graphcommerce/next-ui: The ActionCardList component doesn't accept a Fragment as a child.",
123
- 'Consider providing an array instead',
124
- ].join('\n'),
125
+ "@graphcommerce/next-ui: The ActionCardList component doesn't accept a Fragment as a child. Consider providing an array instead",
125
126
  )
126
127
  }
127
128
 
128
- return !isFragment(child)
129
+ return notFrag
129
130
  })
130
131
 
131
132
  // Make sure the selected values is in the list of all possible values
@@ -137,8 +138,8 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
137
138
  const classes = withState({ size, color, variant, layout })
138
139
 
139
140
  return (
140
- <div ref={ref}>
141
- <ActionCardLayout sx={sx} className={classes.root} layout={layout}>
141
+ <div>
142
+ <ActionCardLayout sx={sx} className={classes.root} layout={layout} ref={ref} tabIndex={0}>
142
143
  {childActionCards.map((child, index) => {
143
144
  if (collapse && Boolean(value) && !isValueSelected(child.props.value, value))
144
145
  return null
@@ -2,4 +2,3 @@ export * from './ActionCard'
2
2
  export * from './ActionCardAccordion'
3
3
  export * from './ActionCardLayout'
4
4
  export * from './ActionCardList'
5
- export * from './ActionCardListForm'
@@ -1,6 +1,6 @@
1
1
  import { Avatar, Box, Chip, SxProps, Theme } from '@mui/material'
2
2
  import { responsiveVal } from '../../Styles/responsiveVal'
3
- import { useDateTimeFormat } from '../../hooks'
3
+ import { DateFormat } from '../../Intl/DateTimeFormat'
4
4
 
5
5
  export type BlogAuthorProps = {
6
6
  author: string
@@ -11,8 +11,6 @@ export type BlogAuthorProps = {
11
11
  export function BlogAuthor(props: BlogAuthorProps) {
12
12
  const { author, date, sx = [] } = props
13
13
 
14
- const formatter = useDateTimeFormat({ month: 'long', day: 'numeric' })
15
-
16
14
  return (
17
15
  <Box
18
16
  maxWidth='md'
@@ -48,7 +46,7 @@ export function BlogAuthor(props: BlogAuthorProps) {
48
46
  {author}
49
47
  </Box>
50
48
  <Box sx={(theme) => ({ lineHeight: 1.4, color: theme.palette.text.disabled })}>
51
- {formatter.format(new Date(date))}
49
+ <DateFormat dateStyle='long'>{date}</DateFormat>
52
50
  </Box>
53
51
  </section>
54
52
  }
@@ -2,7 +2,7 @@ import { Box, Link, SxProps, Theme, Typography } from '@mui/material'
2
2
  import React from 'react'
3
3
  import { extendableComponent } from '../../Styles'
4
4
  import { breakpointVal } from '../../Styles/breakpointVal'
5
- import { useDateTimeFormat } from '../../hooks'
5
+ import { DateFormat } from '../../Intl'
6
6
 
7
7
  export type BlogListItemProps = {
8
8
  asset: React.ReactNode
@@ -19,8 +19,6 @@ const { classes } = extendableComponent(name, parts)
19
19
  export function BlogListItem(props: BlogListItemProps) {
20
20
  const { asset, url, date, title, sx = [] } = props
21
21
 
22
- const formatter = useDateTimeFormat({ dateStyle: 'long' })
23
-
24
22
  return (
25
23
  <Box
26
24
  className={classes.item}
@@ -67,7 +65,7 @@ export function BlogListItem(props: BlogListItemProps) {
67
65
  color: theme.palette.text.secondary,
68
66
  })}
69
67
  >
70
- {formatter.format(new Date(date))}
68
+ <DateFormat dateStyle='long'>{date}</DateFormat>
71
69
  </Box>
72
70
  )}
73
71
 
@@ -14,7 +14,10 @@ export function BlogTag(props: BlogTagsProps) {
14
14
  href={`/${url}`}
15
15
  component={NextLink}
16
16
  label={title}
17
- sx={[{ marginRight: 3, borderRadius: 2 }, ...(Array.isArray(sx) ? sx : [sx])]}
17
+ sx={[
18
+ { marginRight: 3, borderRadius: 2, cursor: 'pointer' },
19
+ ...(Array.isArray(sx) ? sx : [sx]),
20
+ ]}
18
21
  />
19
22
  )
20
23
  }
@@ -0,0 +1,195 @@
1
+ import { Trans } from '@lingui/react'
2
+ import {
3
+ Box,
4
+ Breadcrumbs as MuiBreadcrumbs,
5
+ BreadcrumbsProps as MuiBreadcrumbProps,
6
+ ClickAwayListener,
7
+ IconButton,
8
+ Link,
9
+ Typography,
10
+ useEventCallback,
11
+ useTheme,
12
+ SxProps,
13
+ Theme,
14
+ LinkProps,
15
+ } from '@mui/material'
16
+ import dynamic from 'next/dynamic'
17
+ import { useState, MouseEvent } from 'react'
18
+ import { IconSvg } from '../IconSvg'
19
+ import { iconClose, iconEllypsis } from '../icons'
20
+ import type { BreadcrumbsType } from './types'
21
+ import { i18n } from '@lingui/core'
22
+ import { Button } from '../Button'
23
+
24
+ const BreadcrumbsPopper = dynamic(
25
+ async () => (await import('./BreadcrumbsPopper')).BreadcrumbsPopper,
26
+ )
27
+
28
+ export type BreadcrumbsProps = BreadcrumbsType &
29
+ Omit<MuiBreadcrumbProps, 'children'> & {
30
+ maxItems?: number
31
+ lastIsLink?: boolean
32
+ breadcrumbsAmountDesktop?: number
33
+ breadcrumbsAmountMobile?: number
34
+ itemSx?: SxProps<Theme>
35
+ linkProps?: Omit<LinkProps, 'href'>
36
+ }
37
+
38
+ export function Breadcrumbs(props: BreadcrumbsProps) {
39
+ const {
40
+ breadcrumbs,
41
+ sx,
42
+ breadcrumbsAmountDesktop = 4,
43
+ breadcrumbsAmountMobile = 2,
44
+ lastIsLink = false,
45
+ maxItems,
46
+ itemSx = [],
47
+ linkProps,
48
+ ...rest
49
+ } = props
50
+ const [anchorElement, setAnchorElement] = useState<HTMLButtonElement | null>(null)
51
+ const theme = useTheme()
52
+
53
+ const isDefaultMobile = breadcrumbsAmountMobile === 0
54
+ const showButtonMobile = breadcrumbs.length > breadcrumbsAmountMobile && !isDefaultMobile
55
+ const isDefaultDesktop = breadcrumbsAmountDesktop === 0
56
+ const showButtonDesktop = breadcrumbs.length > breadcrumbsAmountDesktop && !isDefaultDesktop
57
+
58
+ const handleClick = useEventCallback((event: MouseEvent<HTMLButtonElement>) => {
59
+ setAnchorElement((el) => (el !== event.currentTarget ? event.currentTarget : null))
60
+ })
61
+
62
+ const handleClose = () => setAnchorElement(null)
63
+
64
+ const breadcrumbLinks = [...(lastIsLink ? breadcrumbs : breadcrumbs.slice(0, -1))]
65
+ const last = lastIsLink ? null : breadcrumbs[breadcrumbs.length - 1]
66
+
67
+ return (
68
+ <MuiBreadcrumbs
69
+ {...rest}
70
+ aria-label={i18n._(/* i18n*/ `Breadcrumbs`)}
71
+ maxItems={maxItems}
72
+ color='inherit'
73
+ sx={[
74
+ {},
75
+ !maxItems && {
76
+ '& .MuiBreadcrumbs-ol': {
77
+ flexWrap: 'nowrap',
78
+ '& .MuiBreadcrumbs-li': {
79
+ '&:nth-of-type(1)': {
80
+ display: {
81
+ xs: showButtonMobile ? 'flex' : 'none',
82
+ md: showButtonDesktop ? 'flex' : 'none',
83
+ },
84
+ },
85
+ '&:nth-last-of-type(1)': {
86
+ display: 'inline-flex',
87
+ overflowX: 'hidden',
88
+ },
89
+ },
90
+ },
91
+ '& .MuiBreadcrumbs-separator': {
92
+ '&:nth-of-type(2)': {
93
+ display: {
94
+ xs: !showButtonMobile && 'none',
95
+ md: !showButtonDesktop && 'none',
96
+ },
97
+ },
98
+ },
99
+
100
+ [theme.breakpoints.down('md')]: showButtonMobile && {
101
+ '& .MuiBreadcrumbs-li, & .MuiBreadcrumbs-separator': {
102
+ display: 'none',
103
+ [`&:nth-last-of-type(-n+${breadcrumbsAmountMobile * 2})`]: {
104
+ display: 'flex',
105
+ },
106
+ },
107
+ },
108
+
109
+ [theme.breakpoints.up('md')]: showButtonDesktop && {
110
+ '& .MuiBreadcrumbs-li, & .MuiBreadcrumbs-separator': {
111
+ display: 'none',
112
+ [`&:nth-last-of-type(-n+${breadcrumbsAmountDesktop * 2})`]: {
113
+ display: 'flex',
114
+ },
115
+ },
116
+ },
117
+ },
118
+ ...(Array.isArray(sx) ? sx : [sx]),
119
+ ]}
120
+ >
121
+ {!maxItems && (
122
+ <ClickAwayListener
123
+ mouseEvent='onMouseDown'
124
+ touchEvent='onTouchStart'
125
+ onClickAway={handleClose}
126
+ >
127
+ <Box sx={{ position: 'relative', display: 'flex' }}>
128
+ <Button
129
+ aria-describedby={anchorElement ? 'breadcrumb-list' : undefined}
130
+ color='inherit'
131
+ variant='pill'
132
+ size='small'
133
+ onClick={handleClick}
134
+ sx={{
135
+ minWidth: 0,
136
+ // borderRadius: 2,
137
+ // boxShadow: 6,
138
+ // color: 'text.primary',
139
+ // px: 1,
140
+ // py: { xs: 0.3, md: 0.5 },
141
+ // typography: 'caption',
142
+ // backgroundColor: 'background.paper',
143
+ }}
144
+ >
145
+ <IconSvg src={anchorElement ? iconClose : iconEllypsis} />
146
+ </Button>
147
+ <BreadcrumbsPopper
148
+ breadcrumbs={breadcrumbs}
149
+ anchorElement={anchorElement}
150
+ onClose={handleClose}
151
+ showDesktopAmount={breadcrumbsAmountDesktop}
152
+ showMobileAmount={breadcrumbsAmountMobile}
153
+ />
154
+ </Box>
155
+ </ClickAwayListener>
156
+ )}
157
+
158
+ <Link
159
+ href='/'
160
+ color='inherit'
161
+ underline='hover'
162
+ {...linkProps}
163
+ sx={[...(Array.isArray(itemSx) ? itemSx : [itemSx])]}
164
+ >
165
+ <Trans id='Home' />
166
+ </Link>
167
+ {breadcrumbLinks.map((breadcrumb) => (
168
+ <Link
169
+ key={breadcrumb.href}
170
+ color='inherit'
171
+ underline='hover'
172
+ {...breadcrumb}
173
+ {...linkProps}
174
+ sx={[
175
+ ...(Array.isArray(breadcrumb.sx) ? breadcrumb.sx : [breadcrumb.sx]),
176
+ ...(Array.isArray(itemSx) ? itemSx : [itemSx]),
177
+ ]}
178
+ >
179
+ {breadcrumb.name}
180
+ </Link>
181
+ ))}
182
+
183
+ {last && (
184
+ <Typography
185
+ component='span'
186
+ noWrap
187
+ color='inherit'
188
+ sx={[{ fontWeight: '600' }, ...(Array.isArray(itemSx) ? itemSx : [itemSx])]}
189
+ >
190
+ {last.name}
191
+ </Typography>
192
+ )}
193
+ </MuiBreadcrumbs>
194
+ )
195
+ }
@@ -0,0 +1,13 @@
1
+ import { JsonLd } from '../JsonLd/JsonLd'
2
+ import type { BreadcrumbsType } from './types'
3
+
4
+ type BreadcrumbsJsonLdProps<T extends { '@type': string }> = BreadcrumbsType & {
5
+ render: (breadcrumbs: BreadcrumbsType['breadcrumbs']) => T & { '@context': 'https://schema.org' }
6
+ }
7
+
8
+ export function BreadcrumbsJsonLd<T extends { '@type': string }>(props: BreadcrumbsJsonLdProps<T>) {
9
+ const { render, breadcrumbs } = props
10
+
11
+ if (!breadcrumbs.length) return null
12
+ return <JsonLd<T> item={render(breadcrumbs)} keyVal='breadcrumb-jsonld' />
13
+ }
@@ -0,0 +1,101 @@
1
+ import { Trans } from '@lingui/react'
2
+ import { Box, Link, alpha, useTheme } from '@mui/material'
3
+ import { useEffect, useRef, KeyboardEvent } from 'react'
4
+ import type { BreadcrumbsType } from './types'
5
+
6
+ type PopperBreadcrumbsListProps = {
7
+ autoFocus: boolean
8
+ breadcrumbs: BreadcrumbsType['breadcrumbs']
9
+ showDesktopAmount?: number
10
+ showMobileAmount?: number
11
+ onClose: () => void
12
+ }
13
+
14
+ export function BreadcrumbsList(props: PopperBreadcrumbsListProps) {
15
+ const { autoFocus, breadcrumbs, showDesktopAmount = 4, showMobileAmount = 3, onClose } = props
16
+ const listRef = useRef<HTMLDivElement | null>(null)
17
+ const theme = useTheme()
18
+
19
+ const handleKeyDown = (event: KeyboardEvent) => {
20
+ if (event.key === 'Escape') {
21
+ onClose()
22
+ }
23
+ }
24
+
25
+ useEffect(() => {
26
+ if (autoFocus) {
27
+ listRef.current?.focus()
28
+ }
29
+ }, [autoFocus])
30
+
31
+ return (
32
+ <Box
33
+ ref={listRef}
34
+ onKeyDown={handleKeyDown}
35
+ tabIndex={-1}
36
+ sx={{
37
+ backgroundColor: 'background.paper',
38
+ borderRadius: 3,
39
+ boxShadow: 12,
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ overflow: 'hidden auto',
43
+ py: `calc(${theme.spacings.xxs} / 2)`,
44
+ [theme.breakpoints.up('md')]: {
45
+ '& .MuiLink-root': {
46
+ [`:nth-last-of-type(-n+${showDesktopAmount})`]: {
47
+ display: 'none',
48
+ },
49
+ },
50
+ },
51
+ [theme.breakpoints.down('md')]: {
52
+ '& .MuiLink-root': {
53
+ [`:nth-last-of-type(-n+${showMobileAmount})`]: {
54
+ display: 'none',
55
+ },
56
+ },
57
+ },
58
+ }}
59
+ >
60
+ <Link
61
+ href='/'
62
+ underline='none'
63
+ color='text.primary'
64
+ variant='body1'
65
+ noWrap
66
+ onClick={onClose}
67
+ tabIndex={0}
68
+ sx={{
69
+ flex: 1,
70
+ padding: `calc(${theme.spacings.xxs} / 2) ${theme.spacings.xs}`,
71
+ '&:hover': {
72
+ backgroundColor: alpha(theme.palette.action.hover, 0.025),
73
+ },
74
+ }}
75
+ >
76
+ <Trans id='Home' />
77
+ </Link>
78
+ {breadcrumbs.slice(0, breadcrumbs.length).map((breadcrumb) => (
79
+ <Link
80
+ {...breadcrumb}
81
+ key={breadcrumb.href}
82
+ underline='none'
83
+ color='text.primary'
84
+ variant='body1'
85
+ noWrap
86
+ onClick={onClose}
87
+ tabIndex={0}
88
+ sx={{
89
+ flex: 1,
90
+ padding: `calc(${theme.spacings.xxs} / 2) ${theme.spacings.xs}`,
91
+ '&:hover': {
92
+ backgroundColor: alpha(theme.palette.action.hover, 0.025),
93
+ },
94
+ }}
95
+ >
96
+ {breadcrumb.name}
97
+ </Link>
98
+ ))}
99
+ </Box>
100
+ )
101
+ }