@sanity/ui 2.6.7-canary.0 → 2.6.7

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 (59) hide show
  1. package/dist/index.d.mts +15 -51
  2. package/dist/index.d.ts +15 -51
  3. package/dist/index.esm.js +186 -120
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +186 -119
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +186 -120
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme.d.mts +14 -14
  10. package/dist/theme.d.ts +14 -14
  11. package/dist/theme.esm.js.map +1 -1
  12. package/dist/theme.js.map +1 -1
  13. package/dist/theme.mjs.map +1 -1
  14. package/package.json +53 -46
  15. package/src/core/__workshop__/constants.ts +36 -36
  16. package/src/core/components/autocomplete/autocomplete.tsx +1 -1
  17. package/src/core/components/breadcrumbs/breadcrumbs.tsx +6 -15
  18. package/src/core/components/dialog/dialog.tsx +21 -12
  19. package/src/core/components/dialog/styles.ts +8 -8
  20. package/src/core/components/menu/menu.tsx +18 -11
  21. package/src/core/components/menu/menuButton.tsx +11 -11
  22. package/src/core/components/menu/menuContext.ts +1 -1
  23. package/src/core/components/menu/useMenuController.ts +17 -11
  24. package/src/core/components/tab/tab.tsx +8 -8
  25. package/src/core/components/tab/tabPanel.tsx +1 -1
  26. package/src/core/components/toast/styles.ts +1 -2
  27. package/src/core/components/toast/useToast.ts +0 -1
  28. package/src/core/components/tree/tree.tsx +0 -1
  29. package/src/core/components/tree/treeItem.tsx +0 -1
  30. package/src/core/helpers/focus.ts +0 -1
  31. package/src/core/helpers/scroll.ts +0 -1
  32. package/src/core/hooks/_internal/index.ts +1 -0
  33. package/src/core/hooks/_internal/useUnique.ts +34 -0
  34. package/src/core/hooks/index.ts +2 -3
  35. package/src/core/hooks/useClickOutside.ts +72 -38
  36. package/src/core/hooks/useElementSize.ts +0 -1
  37. package/src/core/hooks/useMediaIndex/useMediaIndex.ts +10 -2
  38. package/src/core/hooks/usePrefersDark.ts +55 -10
  39. package/src/core/hooks/usePrefersReducedMotion.ts +56 -10
  40. package/src/core/primitives/avatar/styles.ts +42 -42
  41. package/src/core/primitives/badge/styles.ts +2 -2
  42. package/src/core/primitives/button/styles.ts +4 -4
  43. package/src/core/primitives/inline/styles.ts +2 -2
  44. package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +7 -9
  45. package/src/core/primitives/popover/constants.ts +4 -4
  46. package/src/core/primitives/stack/styles.ts +2 -2
  47. package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +7 -9
  48. package/src/core/primitives/tooltip/constants.ts +4 -4
  49. package/src/core/primitives/tooltip/tooltip.tsx +49 -68
  50. package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +0 -1
  51. package/src/core/styles/font/responsiveFont.ts +8 -8
  52. package/src/core/theme/__workshop__/debug/story.tsx +11 -11
  53. package/src/core/utils/layer/layerProvider.tsx +0 -1
  54. package/src/core/utils/portal/__workshop__/named.tsx +8 -10
  55. package/src/core/utils/portal/portalProvider.tsx +13 -7
  56. package/src/theme/build/buildColorTheme.test.ts +13 -13
  57. package/src/theme/config/tokens/color/types.ts +14 -14
  58. package/src/theme/defaults/colorTokens.ts +36 -36
  59. package/src/core/hooks/useMatchMedia.ts +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.6.7-canary.0",
3
+ "version": "2.6.7",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -51,6 +51,32 @@
51
51
  "src",
52
52
  "theme.js"
53
53
  ],
54
+ "scripts": {
55
+ "build": "run-s clean pkg:build pkg:check figma:pkg:build",
56
+ "clean": "rimraf .workshop dist",
57
+ "commit": "cz",
58
+ "cypress:dev": "run-p dev cypress:open",
59
+ "cypress:open": "cypress open",
60
+ "cypress:run": "cypress run",
61
+ "dev": "run-p storybook:dev workshop:dev",
62
+ "figma:pkg:build": "cd figma && pnpm build",
63
+ "format": "prettier --write --cache --ignore-unknown .",
64
+ "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
65
+ "pkg:build": "pkg build --strict",
66
+ "pkg:check": "pkg --strict",
67
+ "prepare": "husky install",
68
+ "prepublishOnly": "pnpm build",
69
+ "release": "semantic-release",
70
+ "storybook:build": "storybook build",
71
+ "storybook:dev": "storybook dev -p 6006",
72
+ "test": "jest",
73
+ "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
74
+ "ts:check": "tsc",
75
+ "watch": "pkg watch --strict",
76
+ "workshop:build": "workshop build",
77
+ "workshop:dev": "workshop dev",
78
+ "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
79
+ },
54
80
  "commitlint": {
55
81
  "extends": [
56
82
  "@commitlint/config-conventional"
@@ -61,7 +87,6 @@
61
87
  "prettier --write --cache --ignore-unknown"
62
88
  ]
63
89
  },
64
- "prettier": "@sanity/prettier-config",
65
90
  "release": {
66
91
  "branches": [
67
92
  "+([0-9])?(.{+([0-9]),x}).x",
@@ -84,8 +109,7 @@
84
109
  "@sanity/icons": "^3.3.0",
85
110
  "csstype": "^3.1.3",
86
111
  "framer-motion": "11.0.8",
87
- "react-refractor": "^2.2.0",
88
- "use-effect-event": "^1.0.2"
112
+ "react-refractor": "^2.2.0"
89
113
  },
90
114
  "devDependencies": {
91
115
  "@babel/core": "^7.24.7",
@@ -96,25 +120,24 @@
96
120
  "@commitlint/config-conventional": "^19.2.2",
97
121
  "@juggle/resize-observer": "^3.4.0",
98
122
  "@sanity/pkg-utils": "^6.10.3",
99
- "@sanity/prettier-config": "^1.0.2",
100
- "@sanity/semantic-release-preset": "^5.0.0",
123
+ "@sanity/semantic-release-preset": "^4.1.8",
101
124
  "@sanity/ui-workshop": "^2.0.15",
102
- "@storybook/addon-a11y": "^8.2.1",
103
- "@storybook/addon-docs": "^8.2.1",
104
- "@storybook/addon-essentials": "^8.2.1",
105
- "@storybook/addon-interactions": "^8.2.1",
106
- "@storybook/addon-links": "^8.2.1",
107
- "@storybook/addon-mdx-gfm": "^8.2.1",
108
- "@storybook/addon-storysource": "^8.2.1",
109
- "@storybook/addon-themes": "^8.2.1",
110
- "@storybook/blocks": "^8.2.1",
111
- "@storybook/manager-api": "^8.2.1",
112
- "@storybook/react": "^8.2.1",
113
- "@storybook/react-vite": "^8.2.1",
114
- "@storybook/test": "^8.2.1",
115
- "@storybook/theming": "^8.2.1",
125
+ "@storybook/addon-a11y": "^8.0.8",
126
+ "@storybook/addon-docs": "^8.0.8",
127
+ "@storybook/addon-essentials": "^8.0.8",
128
+ "@storybook/addon-interactions": "^8.0.8",
129
+ "@storybook/addon-links": "^8.0.8",
130
+ "@storybook/addon-storysource": "^8.0.8",
131
+ "@storybook/addon-themes": "^8.0.8",
132
+ "@storybook/blocks": "^8.0.8",
133
+ "@storybook/jest": "^0.2.3",
134
+ "@storybook/manager-api": "^8.0.8",
135
+ "@storybook/react": "^8.0.8",
136
+ "@storybook/react-vite": "^8.0.8",
137
+ "@storybook/testing-library": "^0.2.2",
138
+ "@storybook/theming": "^8.0.8",
116
139
  "@testing-library/dom": "^10.3.1",
117
- "@testing-library/jest-dom": "^6.4.6",
140
+ "@testing-library/jest-dom": "^5.17.0",
118
141
  "@testing-library/react": "^16.0.0",
119
142
  "@types/jest": "^29.5.12",
120
143
  "@types/jest-axe": "^3.5.9",
@@ -123,6 +146,7 @@
123
146
  "@types/react-dom": "^18.3.0",
124
147
  "@types/react-is": "^18.3.0",
125
148
  "@types/refractor": "^3.4.1",
149
+ "@types/testing-library__jest-dom": "^5.14.9",
126
150
  "@typescript-eslint/eslint-plugin": "^7.16.0",
127
151
  "@typescript-eslint/parser": "^7.16.0",
128
152
  "commitizen": "^4.3.0",
@@ -148,6 +172,7 @@
148
172
  "module-alias": "^2.2.3",
149
173
  "npm-run-all2": "^5.0.2",
150
174
  "prettier": "^3.3.2",
175
+ "prettier-plugin-packagejson": "^2.5.0",
151
176
  "react": "^18.3.1",
152
177
  "react-dom": "^18.3.1",
153
178
  "react-is": "^18.3.1",
@@ -155,7 +180,7 @@
155
180
  "rimraf": "^5.0.5",
156
181
  "semantic-release": "^24.0.0",
157
182
  "start-server-and-test": "^2.0.4",
158
- "storybook": "^8.2.1",
183
+ "storybook": "^8.0.8",
159
184
  "styled-components": "^6.1.11",
160
185
  "tsconfig-paths": "^4.2.0",
161
186
  "typescript": "5.5.3",
@@ -168,6 +193,7 @@
168
193
  "react-is": "^18",
169
194
  "styled-components": "^5.2 || ^6"
170
195
  },
196
+ "packageManager": "pnpm@9.5.0",
171
197
  "engines": {
172
198
  "node": ">=14.0.0"
173
199
  },
@@ -177,28 +203,9 @@
177
203
  "esm.sh": {
178
204
  "bundle": false
179
205
  },
180
- "scripts": {
181
- "build": "run-s clean pkg:build pkg:check figma:pkg:build",
182
- "clean": "rimraf .workshop dist",
183
- "commit": "cz",
184
- "cypress:dev": "run-p dev cypress:open",
185
- "cypress:open": "cypress open",
186
- "cypress:run": "cypress run",
187
- "dev": "run-p storybook:dev workshop:dev",
188
- "figma:pkg:build": "cd figma && pnpm build",
189
- "format": "prettier --write --cache --ignore-unknown .",
190
- "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
191
- "pkg:build": "pkg build --strict",
192
- "pkg:check": "pkg --strict",
193
- "release": "semantic-release",
194
- "storybook:build": "storybook build",
195
- "storybook:dev": "storybook dev -p 6006",
196
- "test": "jest",
197
- "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
198
- "ts:check": "tsc",
199
- "watch": "pkg watch --strict",
200
- "workshop:build": "workshop build",
201
- "workshop:dev": "workshop dev",
202
- "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
206
+ "pnpm": {
207
+ "overrides": {
208
+ "conventional-changelog-conventionalcommits": ">= 8.0.0"
209
+ }
203
210
  }
204
- }
211
+ }
@@ -44,9 +44,9 @@ export const WORKSHOP_BUTTON_MODE_OPTIONS: {[key: string]: ButtonMode} = {
44
44
 
45
45
  export const WORKSHOP_BUTTON_TEXT_ALIGN_OPTIONS: {[key: string]: ButtonTextAlign | ''} = {
46
46
  '(none)': '',
47
- 'Left': 'left',
48
- 'Right': 'right',
49
- 'Center': 'center',
47
+ Left: 'left',
48
+ Right: 'right',
49
+ Center: 'center',
50
50
  }
51
51
 
52
52
  export const WORKSHOP_BUTTON_TONE_OPTIONS: {[key: string]: ButtonTone} = {
@@ -61,11 +61,11 @@ export const WORKSHOP_CARD_AS_OPTIONS: {
61
61
  [key: string]: 'div' | 'button' | 'span' | 'ol' | 'pre' | 'ul'
62
62
  } = {
63
63
  'DIV (default)': 'div',
64
- 'BUTTON': 'button',
65
- 'SPAN': 'span',
66
- 'OL': 'ol',
67
- 'PRE': 'pre',
68
- 'UL': 'ul',
64
+ BUTTON: 'button',
65
+ SPAN: 'span',
66
+ OL: 'ol',
67
+ PRE: 'pre',
68
+ UL: 'ul',
69
69
  }
70
70
 
71
71
  export const WORKSHOP_CARD_TONE_OPTIONS: {[key: string]: CardTone} = {
@@ -92,7 +92,7 @@ export const WORKSHOP_CONTAINER_WIDTH_OPTIONS: {[key: string]: number | 'auto'}
92
92
  '3': 3,
93
93
  '4': 4,
94
94
  '5': 5,
95
- 'auto': 'auto',
95
+ auto: 'auto',
96
96
  }
97
97
 
98
98
  export const WORKSHOP_DIALOG_POSITION_OPTIONS: {[key: string]: DialogPosition} = {
@@ -106,15 +106,15 @@ export const WORKSHOP_FLEX_DIRECTION_OPTIONS: {[key: string]: 'row' | 'column'}
106
106
  }
107
107
 
108
108
  export const WORKSHOP_FLEX_ALIGN_OPTIONS: Record<FlexAlign, FlexAlign> = {
109
- 'baseline': 'baseline',
110
- 'center': 'center',
109
+ baseline: 'baseline',
110
+ center: 'center',
111
111
  'flex-end': 'flex-end',
112
112
  'flex-start': 'flex-start',
113
- 'stretch': 'stretch',
113
+ stretch: 'stretch',
114
114
  }
115
115
 
116
116
  export const WORKSHOP_FLEX_JUSTIFY_OPTIONS: Record<FlexJustify, FlexJustify> = {
117
- 'center': 'center',
117
+ center: 'center',
118
118
  'flex-end': 'flex-end',
119
119
  'flex-start': 'flex-start',
120
120
  'space-around': 'space-around',
@@ -157,16 +157,16 @@ export const WORKSHOP_LABEL_FONT_SIZE_OPTIONS = {
157
157
  }
158
158
 
159
159
  export const WORKSHOP_PLACEMENT_OPTIONS: Record<Placement, Placement> = {
160
- 'top': 'top',
160
+ top: 'top',
161
161
  'top-start': 'top-start',
162
162
  'top-end': 'top-end',
163
- 'right': 'right',
163
+ right: 'right',
164
164
  'right-start': 'right-start',
165
165
  'right-end': 'right-end',
166
- 'left': 'left',
166
+ left: 'left',
167
167
  'left-start': 'left-start',
168
168
  'left-end': 'left-end',
169
- 'bottom': 'bottom',
169
+ bottom: 'bottom',
170
170
  'bottom-start': 'bottom-start',
171
171
  'bottom-end': 'bottom-end',
172
172
  }
@@ -217,11 +217,11 @@ export const WORKSHOP_SPOT_COLOR_OPTIONS: {[key: string]: ThemeColorSpotKey} = {
217
217
 
218
218
  export const WORKSHOP_TEXT_ALIGN_OPTIONS: {[key: string]: TextAlign | ''} = {
219
219
  '(none)': '',
220
- 'Initial': 'initial',
221
- 'Left': 'left',
222
- 'Right': 'right',
223
- 'Center': 'center',
224
- 'Justify': 'justify',
220
+ Initial: 'initial',
221
+ Left: 'left',
222
+ Right: 'right',
223
+ Center: 'center',
224
+ Justify: 'justify',
225
225
  }
226
226
 
227
227
  export const WORKSHOP_TEXT_FONT_SIZE_OPTIONS = {
@@ -233,17 +233,17 @@ export const WORKSHOP_TEXT_FONT_SIZE_OPTIONS = {
233
233
  }
234
234
 
235
235
  export const WORKSHOP_TEXT_INPUT_TYPE_OPTIONS: {[key: string]: TextInputType} = {
236
- 'date': 'date',
236
+ date: 'date',
237
237
  'datetime-local': 'datetime-local',
238
- 'email': 'email',
239
- 'month': 'month',
240
- 'number': 'number',
241
- 'password': 'password',
242
- 'tel': 'tel',
243
- 'time': 'time',
244
- 'text': 'text',
245
- 'week': 'week',
246
- 'color': 'color',
238
+ email: 'email',
239
+ month: 'month',
240
+ number: 'number',
241
+ password: 'password',
242
+ tel: 'tel',
243
+ time: 'time',
244
+ text: 'text',
245
+ week: 'week',
246
+ color: 'color',
247
247
  }
248
248
 
249
249
  export const WORKSHOP_TEXT_OVERFLOW_OPTIONS: {[key: string]: 'ellipsis' | ''} = {
@@ -261,9 +261,9 @@ export const WORKSHOP_TEXT_SIZE_OPTIONS = {
261
261
 
262
262
  export const WORKSHOP_TEXT_WEIGHT_OPTIONS: {[key: string]: ThemeFontWeightKey | ''} = {
263
263
  'Regular (default)': '',
264
- 'Medium': 'medium',
265
- 'Semibold': 'semibold',
266
- 'Bold': 'bold',
264
+ Medium: 'medium',
265
+ Semibold: 'semibold',
266
+ Bold: 'bold',
267
267
  }
268
268
 
269
269
  export const WORKSHOP_THEME_COLOR_TONE_OPTIONS: {[key: string]: ThemeColorToneKey} = {
@@ -286,7 +286,7 @@ export const WORKSHOP_TOAST_STATUS_OPTIONS: {
286
286
  }
287
287
 
288
288
  export const WORKSHOP_WIDTH_OPTIONS: {[key: string]: number | 'auto'} = {
289
- 'Auto': 'auto',
289
+ Auto: 'auto',
290
290
  '0': 0,
291
291
  '1': 1,
292
292
  '2': 2,
@@ -452,7 +452,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
452
452
  if (!loading && !disabled && value) {
453
453
  return {
454
454
  'aria-label': 'Clear',
455
- 'onFocus': handleClearButtonFocus,
455
+ onFocus: handleClearButtonFocus,
456
456
  }
457
457
  }
458
458
 
@@ -1,13 +1,4 @@
1
- import {
2
- Children,
3
- forwardRef,
4
- Fragment,
5
- isValidElement,
6
- useCallback,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react'
1
+ import {Children, forwardRef, Fragment, isValidElement, useCallback, useMemo, useState} from 'react'
11
2
  import {useArrayProp, useClickOutside} from '../../hooks'
12
3
  import {Box, Popover, Stack, Text} from '../../primitives'
13
4
  import {ExpandButton, Root} from './breadcrumbs.styles'
@@ -31,13 +22,13 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
31
22
  const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
32
23
  const space = useArrayProp(spaceRaw)
33
24
  const [open, setOpen] = useState(false)
34
- const expandElementRef = useRef<HTMLButtonElement | null>(null)
35
- const popoverElementRef = useRef<HTMLDivElement | null>(null)
25
+ const [expandElement, setExpandElement] = useState<HTMLButtonElement | null>(null)
26
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
36
27
 
37
28
  const collapse = useCallback(() => setOpen(false), [])
38
29
  const expand = useCallback(() => setOpen(true), [])
39
30
 
40
- useClickOutside(collapse, () => [expandElementRef.current, popoverElementRef.current])
31
+ useClickOutside(collapse, [expandElement, popoverElement])
41
32
 
42
33
  const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
43
34
 
@@ -61,14 +52,14 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
61
52
  open={open}
62
53
  placement="top"
63
54
  portal
64
- ref={popoverElementRef}
55
+ ref={setPopoverElement}
65
56
  >
66
57
  <ExpandButton
67
58
  fontSize={1}
68
59
  mode="bleed"
69
60
  onClick={open ? collapse : expand}
70
61
  padding={1}
71
- ref={expandElementRef}
62
+ ref={setExpandElement}
72
63
  selected={open}
73
64
  text="…"
74
65
  />
@@ -1,6 +1,6 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
2
  import {ThemeColorSchemeKey} from '@sanity/ui/theme'
3
- import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from 'react'
3
+ import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
  import {
6
6
  containsOrEqualsElement,
@@ -170,6 +170,7 @@ const DialogCard = forwardRef(function DialogCard(
170
170
  const shadow = useArrayProp(shadowProp)
171
171
  const width = useArrayProp(widthProp)
172
172
  const ref = useRef<HTMLDivElement | null>(null)
173
+ const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
173
174
  const contentRef = useRef<HTMLDivElement | null>(null)
174
175
  const layer = useLayer()
175
176
  const {isTopLayer} = layer
@@ -215,24 +216,32 @@ const DialogCard = forwardRef(function DialogCard(
215
216
  )
216
217
 
217
218
  useClickOutside(
218
- (event: MouseEvent | TouchEvent) => {
219
- if (!isTopLayer || !onClickOutside) return
219
+ useCallback(
220
+ (event: MouseEvent) => {
221
+ if (!isTopLayer || !onClickOutside) return
220
222
 
221
- const target = event.target as Node | null
223
+ const target = event.target as Node | null
222
224
 
223
- if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
224
- // Ignore clicks outside of the scope
225
- return
226
- }
225
+ if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
226
+ // Ignore clicks outside of the scope
227
+ return
228
+ }
227
229
 
228
- onClickOutside()
229
- },
230
- () => [ref.current],
230
+ onClickOutside()
231
+ },
232
+ [boundaryElement, isTopLayer, onClickOutside, portalElement],
233
+ ),
234
+ [rootElement],
231
235
  )
232
236
 
237
+ const setRef = useCallback((el: HTMLDivElement | null) => {
238
+ setRootElement(el)
239
+ ref.current = el
240
+ }, [])
241
+
233
242
  return (
234
243
  <DialogContainer data-ui="DialogCard" width={width}>
235
- <DialogCardRoot radius={radius} ref={ref} scheme={scheme} shadow={shadow}>
244
+ <DialogCardRoot radius={radius} ref={setRef} scheme={scheme} shadow={shadow}>
236
245
  <DialogLayout direction="column">
237
246
  {showHeader && (
238
247
  <DialogHeader>
@@ -18,14 +18,14 @@ export function dialogStyle({theme}: ThemeProps): CSSObject {
18
18
  display: 'flex',
19
19
  },
20
20
 
21
- 'top': 0,
22
- 'left': 0,
23
- 'right': 0,
24
- 'bottom': 0,
25
- 'alignItems': 'center',
26
- 'justifyContent': 'center',
27
- 'outline': 'none',
28
- 'background': color.backdrop,
21
+ top: 0,
22
+ left: 0,
23
+ right: 0,
24
+ bottom: 0,
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ outline: 'none',
28
+ background: color.backdrop,
29
29
  }
30
30
  }
31
31
 
@@ -19,7 +19,7 @@ export interface MenuProps extends ResponsivePaddingProps {
19
19
  * @deprecated Use `shouldFocus="last"` instead.
20
20
  */
21
21
  focusLast?: boolean
22
- onClickOutside?: (event: MouseEvent | TouchEvent) => void
22
+ onClickOutside?: (event: MouseEvent) => void
23
23
  onEscape?: () => void
24
24
  onItemClick?: () => void
25
25
  onItemSelect?: (index: number) => void
@@ -75,18 +75,16 @@ export const Menu = forwardRef(function Menu(
75
75
  handleItemMouseLeave,
76
76
  handleKeyDown,
77
77
  mount,
78
- } = useMenuController({onKeyDown, originElement, shouldFocus, rootElementRef: ref})
78
+ rootElement,
79
+ setRootElement,
80
+ } = useMenuController({onKeyDown, originElement, shouldFocus})
79
81
 
80
82
  const handleRefChange = useCallback(
81
83
  (el: HTMLDivElement | null) => {
84
+ setRootElement(el)
82
85
  ref.current = el
83
-
84
- // Register root element (for nested menus)
85
- if (ref.current && registerElement) {
86
- registerElement(ref.current)
87
- }
88
86
  },
89
- [registerElement],
87
+ [setRootElement],
90
88
  )
91
89
 
92
90
  // Trigger `onItemSelect` when active index changes
@@ -96,8 +94,11 @@ export const Menu = forwardRef(function Menu(
96
94
 
97
95
  // Close menu when clicking outside
98
96
  useClickOutside(
99
- (event) => isTopLayer && onClickOutside && onClickOutside(event),
100
- () => [ref.current],
97
+ useCallback(
98
+ (event) => isTopLayer && onClickOutside && onClickOutside(event),
99
+ [isTopLayer, onClickOutside],
100
+ ),
101
+ [rootElement],
101
102
  )
102
103
 
103
104
  // Close menu when pressing Escape
@@ -115,7 +116,13 @@ export const Menu = forwardRef(function Menu(
115
116
  ),
116
117
  )
117
118
 
118
- // @TODO split out into separate contexts
119
+ // Register root element (for nested menus)
120
+ useEffect(() => {
121
+ if (!rootElement || !registerElement) return
122
+
123
+ return registerElement(rootElement)
124
+ }, [registerElement, rootElement])
125
+
119
126
  const value: MenuContextValue = useMemo(
120
127
  () => ({
121
128
  version: 0.0,
@@ -140,7 +140,7 @@ export const MenuButton = forwardRef(function MenuButton(
140
140
  }, [])
141
141
 
142
142
  const handleMenuClickOutside = useCallback(
143
- (event: MouseEvent | TouchEvent) => {
143
+ (event: MouseEvent) => {
144
144
  const target = event.target
145
145
 
146
146
  if (!(target instanceof Node)) {
@@ -206,11 +206,11 @@ export const MenuButton = forwardRef(function MenuButton(
206
206
  const menuProps: MenuProps = useMemo(
207
207
  () => ({
208
208
  'aria-labelledby': id,
209
- 'onBlurCapture': handleBlur,
210
- 'onClickOutside': handleMenuClickOutside,
211
- 'onEscape': handleMenuEscape,
212
- 'onItemClick': handleItemClick,
213
- 'originElement': buttonElement,
209
+ onBlurCapture: handleBlur,
210
+ onClickOutside: handleMenuClickOutside,
211
+ onEscape: handleMenuEscape,
212
+ onItemClick: handleItemClick,
213
+ originElement: buttonElement,
214
214
  registerElement,
215
215
  shouldFocus,
216
216
  }),
@@ -235,13 +235,13 @@ export const MenuButton = forwardRef(function MenuButton(
235
235
  cloneElement(buttonProp, {
236
236
  'data-ui': 'MenuButton',
237
237
  id,
238
- 'onClick': handleButtonClick,
239
- 'onKeyDown': handleButtonKeyDown,
240
- 'onMouseDown': handleMouseDown,
238
+ onClick: handleButtonClick,
239
+ onKeyDown: handleButtonKeyDown,
240
+ onMouseDown: handleMouseDown,
241
241
  'aria-haspopup': true,
242
242
  'aria-expanded': open,
243
- 'ref': ref,
244
- 'selected': buttonProp.props.selected ?? open,
243
+ ref: ref,
244
+ selected: buttonProp.props.selected ?? open,
245
245
  }),
246
246
  [buttonProp, handleButtonClick, handleButtonKeyDown, handleMouseDown, id, open],
247
247
  )
@@ -5,7 +5,7 @@ export interface MenuContextValue {
5
5
  activeElement: HTMLElement | null
6
6
  activeIndex: number
7
7
  mount: (element: HTMLElement | null, selected?: boolean) => () => void
8
- onClickOutside?: (event: MouseEvent | TouchEvent) => void
8
+ onClickOutside?: (event: MouseEvent) => void
9
9
  onEscape?: () => void
10
10
  onItemClick?: () => void
11
11
  onItemMouseEnter?: (event: React.MouseEvent<HTMLElement>) => void
@@ -1,4 +1,4 @@
1
- import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
1
+ import {useCallback, useEffect, useRef, useState} from 'react'
2
2
  import {_getFocusableElements, _sortElements} from './helpers'
3
3
 
4
4
  /**
@@ -11,6 +11,8 @@ export interface MenuController {
11
11
  handleItemMouseLeave: () => void
12
12
  handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
13
13
  mount: (element: HTMLElement | null, selected?: boolean) => () => void
14
+ rootElement: HTMLDivElement | null
15
+ setRootElement: (el: HTMLDivElement | null) => void
14
16
  }
15
17
 
16
18
  /**
@@ -22,14 +24,14 @@ export function useMenuController(props: {
22
24
  onKeyDown?: React.KeyboardEventHandler
23
25
  originElement?: HTMLElement | null
24
26
  shouldFocus: 'first' | 'last' | null
25
- rootElementRef: React.MutableRefObject<HTMLDivElement | null>
26
27
  }): MenuController {
27
- const {onKeyDown, originElement, shouldFocus, rootElementRef} = props
28
+ const {onKeyDown, originElement, shouldFocus} = props
28
29
  const elementsRef = useRef<HTMLElement[]>([])
30
+ const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
29
31
  const [activeIndex, _setActiveIndex] = useState(-1)
30
32
  const activeIndexRef = useRef(activeIndex)
31
- const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
32
- const mounted = Boolean(rootElementRef.current)
33
+ const activeElement = elementsRef.current[activeIndex] || null
34
+ const mounted = Boolean(rootElement)
33
35
 
34
36
  const setActiveIndex = useCallback((nextActiveIndex: number) => {
35
37
  _setActiveIndex(nextActiveIndex)
@@ -42,7 +44,7 @@ export function useMenuController(props: {
42
44
 
43
45
  if (elementsRef.current.indexOf(element) === -1) {
44
46
  elementsRef.current.push(element)
45
- _sortElements(rootElementRef.current, elementsRef.current)
47
+ _sortElements(rootElement, elementsRef.current)
46
48
  }
47
49
 
48
50
  if (selected) {
@@ -59,7 +61,7 @@ export function useMenuController(props: {
59
61
  }
60
62
  }
61
63
  },
62
- [rootElementRef, setActiveIndex],
64
+ [rootElement, setActiveIndex],
63
65
  )
64
66
 
65
67
  const handleKeyDown = useCallback(
@@ -177,15 +179,17 @@ export function useMenuController(props: {
177
179
  // which would be incorrect when the user hovers over a gap
178
180
  // between two menu items or a menu divider.
179
181
  setActiveIndex(-2)
180
- rootElementRef.current?.focus()
181
- }, [rootElementRef, setActiveIndex])
182
+ rootElement?.focus()
183
+ }, [setActiveIndex, rootElement])
182
184
 
183
185
  // Set focus on the currently active element
184
186
  useEffect(() => {
185
187
  if (!mounted) return
186
188
 
187
189
  const rafId = window.requestAnimationFrame(() => {
188
- if (activeIndex === -1) {
190
+ const _activeIndex = activeIndexRef.current
191
+
192
+ if (_activeIndex === -1) {
189
193
  if (shouldFocus === 'first') {
190
194
  const focusableElements = _getFocusableElements(elementsRef.current)
191
195
  const el = focusableElements[0]
@@ -213,7 +217,7 @@ export function useMenuController(props: {
213
217
  return
214
218
  }
215
219
 
216
- const element = elementsRef.current[activeIndex] || null
220
+ const element = elementsRef.current[_activeIndex] || null
217
221
 
218
222
  element?.focus()
219
223
  })
@@ -230,5 +234,7 @@ export function useMenuController(props: {
230
234
  handleItemMouseLeave,
231
235
  handleKeyDown,
232
236
  mount,
237
+ rootElement,
238
+ setRootElement,
233
239
  }
234
240
  }