@sanity/ui 2.6.4-canary.0 → 2.6.4-canary.2

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 (55) hide show
  1. package/dist/index.d.mts +51 -15
  2. package/dist/index.d.ts +51 -15
  3. package/dist/index.esm.js +103 -167
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +102 -167
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +103 -167
  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 +28 -27
  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 +15 -6
  18. package/src/core/components/dialog/dialog.tsx +12 -21
  19. package/src/core/components/dialog/styles.ts +8 -8
  20. package/src/core/components/menu/menu.tsx +11 -18
  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 +11 -18
  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/useToast.ts +1 -0
  27. package/src/core/components/tree/tree.tsx +1 -0
  28. package/src/core/components/tree/treeItem.tsx +1 -0
  29. package/src/core/hooks/index.ts +3 -2
  30. package/src/core/hooks/useClickOutside.ts +37 -74
  31. package/src/core/hooks/useMatchMedia.ts +46 -0
  32. package/src/core/hooks/useMediaIndex/useMediaIndex.ts +2 -11
  33. package/src/core/hooks/usePrefersDark.ts +10 -58
  34. package/src/core/hooks/usePrefersReducedMotion.ts +10 -58
  35. package/src/core/primitives/avatar/styles.ts +42 -42
  36. package/src/core/primitives/badge/styles.ts +2 -2
  37. package/src/core/primitives/button/styles.ts +4 -4
  38. package/src/core/primitives/inline/styles.ts +2 -2
  39. package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
  40. package/src/core/primitives/popover/constants.ts +4 -4
  41. package/src/core/primitives/stack/styles.ts +2 -2
  42. package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +9 -7
  43. package/src/core/primitives/tooltip/constants.ts +4 -4
  44. package/src/core/primitives/tooltip/tooltip.tsx +45 -27
  45. package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +1 -0
  46. package/src/core/styles/font/responsiveFont.ts +8 -8
  47. package/src/core/theme/__workshop__/debug/story.tsx +11 -11
  48. package/src/core/utils/layer/layerProvider.tsx +1 -0
  49. package/src/core/utils/portal/__workshop__/named.tsx +10 -8
  50. package/src/core/utils/portal/portalProvider.tsx +7 -14
  51. package/src/theme/build/buildColorTheme.test.ts +13 -13
  52. package/src/theme/config/tokens/color/types.ts +14 -14
  53. package/src/theme/defaults/colorTokens.ts +36 -36
  54. package/src/core/hooks/_internal/index.ts +0 -1
  55. package/src/core/hooks/_internal/useUnique.ts +0 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.6.4-canary.0",
3
+ "version": "2.6.4-canary.2",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -61,6 +61,7 @@
61
61
  "prettier --write --cache --ignore-unknown"
62
62
  ]
63
63
  },
64
+ "prettier": "@sanity/prettier-config",
64
65
  "release": {
65
66
  "branches": [
66
67
  "+([0-9])?(.{+([0-9]),x}).x",
@@ -83,7 +84,8 @@
83
84
  "@sanity/icons": "^3.3.0",
84
85
  "csstype": "^3.1.3",
85
86
  "framer-motion": "11.0.8",
86
- "react-refractor": "^2.2.0"
87
+ "react-refractor": "^2.2.0",
88
+ "use-effect-event": "^1.0.2"
87
89
  },
88
90
  "devDependencies": {
89
91
  "@babel/core": "^7.24.7",
@@ -93,25 +95,26 @@
93
95
  "@commitlint/cli": "^19.3.0",
94
96
  "@commitlint/config-conventional": "^19.2.2",
95
97
  "@juggle/resize-observer": "^3.4.0",
96
- "@sanity/pkg-utils": "^6.10.2",
97
- "@sanity/semantic-release-preset": "^4.1.8",
98
+ "@sanity/pkg-utils": "^6.10.3",
99
+ "@sanity/prettier-config": "^1.0.2",
100
+ "@sanity/semantic-release-preset": "^5.0.0",
98
101
  "@sanity/ui-workshop": "^2.0.15",
99
- "@storybook/addon-a11y": "^8.0.8",
100
- "@storybook/addon-docs": "^8.0.8",
101
- "@storybook/addon-essentials": "^8.0.8",
102
- "@storybook/addon-interactions": "^8.0.8",
103
- "@storybook/addon-links": "^8.0.8",
104
- "@storybook/addon-storysource": "^8.0.8",
105
- "@storybook/addon-themes": "^8.0.8",
106
- "@storybook/blocks": "^8.0.8",
107
- "@storybook/jest": "^0.2.3",
108
- "@storybook/manager-api": "^8.0.8",
109
- "@storybook/react": "^8.0.8",
110
- "@storybook/react-vite": "^8.0.8",
111
- "@storybook/testing-library": "^0.2.2",
112
- "@storybook/theming": "^8.0.8",
113
- "@testing-library/dom": "^10.1.0",
114
- "@testing-library/jest-dom": "^5.17.0",
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",
116
+ "@testing-library/dom": "^10.3.1",
117
+ "@testing-library/jest-dom": "^6.4.6",
115
118
  "@testing-library/react": "^16.0.0",
116
119
  "@types/jest": "^29.5.12",
117
120
  "@types/jest-axe": "^3.5.9",
@@ -120,12 +123,11 @@
120
123
  "@types/react-dom": "^18.3.0",
121
124
  "@types/react-is": "^18.3.0",
122
125
  "@types/refractor": "^3.4.1",
123
- "@types/testing-library__jest-dom": "^5.14.9",
124
126
  "@typescript-eslint/eslint-plugin": "^7.16.0",
125
127
  "@typescript-eslint/parser": "^7.16.0",
126
128
  "commitizen": "^4.3.0",
127
- "cypress": "^13.11.0",
128
- "cypress-real-events": "^1.12.0",
129
+ "cypress": "^13.13.0",
130
+ "cypress-real-events": "^1.13.0",
129
131
  "cz-conventional-changelog": "^3.3.0",
130
132
  "eslint": "^8.57.0",
131
133
  "eslint-config-prettier": "^9.1.0",
@@ -140,13 +142,12 @@
140
142
  "http-server": "^14.1.1",
141
143
  "husky": "^8.0.3",
142
144
  "jest": "^29.7.0",
143
- "jest-axe": "^8.0.0",
145
+ "jest-axe": "^9.0.0",
144
146
  "jest-environment-jsdom": "^29.7.0",
145
147
  "lint-staged": "^14.0.1",
146
148
  "module-alias": "^2.2.3",
147
149
  "npm-run-all2": "^5.0.2",
148
150
  "prettier": "^3.3.2",
149
- "prettier-plugin-packagejson": "^2.5.0",
150
151
  "react": "^18.3.1",
151
152
  "react-dom": "^18.3.1",
152
153
  "react-is": "^18.3.1",
@@ -154,11 +155,11 @@
154
155
  "rimraf": "^5.0.5",
155
156
  "semantic-release": "^24.0.0",
156
157
  "start-server-and-test": "^2.0.4",
157
- "storybook": "^8.0.8",
158
+ "storybook": "^8.2.1",
158
159
  "styled-components": "^6.1.11",
159
160
  "tsconfig-paths": "^4.2.0",
160
161
  "typescript": "5.5.3",
161
- "vite": "^5.2.13",
162
+ "vite": "^5.3.3",
162
163
  "vite-tsconfig-paths": "^4.3.2"
163
164
  },
164
165
  "peerDependencies": {
@@ -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,4 +1,13 @@
1
- import {Children, forwardRef, Fragment, isValidElement, useCallback, useMemo, useState} from 'react'
1
+ import {
2
+ Children,
3
+ forwardRef,
4
+ Fragment,
5
+ isValidElement,
6
+ useCallback,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react'
2
11
  import {useArrayProp, useClickOutside} from '../../hooks'
3
12
  import {Box, Popover, Stack, Text} from '../../primitives'
4
13
  import {ExpandButton, Root} from './breadcrumbs.styles'
@@ -22,13 +31,13 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
22
31
  const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
23
32
  const space = useArrayProp(spaceRaw)
24
33
  const [open, setOpen] = useState(false)
25
- const [expandElement, setExpandElement] = useState<HTMLButtonElement | null>(null)
26
- const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
34
+ const expandElementRef = useRef<HTMLButtonElement | null>(null)
35
+ const popoverElementRef = useRef<HTMLDivElement | null>(null)
27
36
 
28
37
  const collapse = useCallback(() => setOpen(false), [])
29
38
  const expand = useCallback(() => setOpen(true), [])
30
39
 
31
- useClickOutside(collapse, [expandElement, popoverElement])
40
+ useClickOutside(collapse, () => [expandElementRef.current, popoverElementRef.current])
32
41
 
33
42
  const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
34
43
 
@@ -52,14 +61,14 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
52
61
  open={open}
53
62
  placement="top"
54
63
  portal
55
- ref={setPopoverElement}
64
+ ref={popoverElementRef}
56
65
  >
57
66
  <ExpandButton
58
67
  fontSize={1}
59
68
  mode="bleed"
60
69
  onClick={open ? collapse : expand}
61
70
  padding={1}
62
- ref={setExpandElement}
71
+ ref={expandElementRef}
63
72
  selected={open}
64
73
  text="…"
65
74
  />
@@ -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, useState} from 'react'
3
+ import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
  import {
6
6
  containsOrEqualsElement,
@@ -170,7 +170,6 @@ 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)
174
173
  const contentRef = useRef<HTMLDivElement | null>(null)
175
174
  const layer = useLayer()
176
175
  const {isTopLayer} = layer
@@ -216,32 +215,24 @@ const DialogCard = forwardRef(function DialogCard(
216
215
  )
217
216
 
218
217
  useClickOutside(
219
- useCallback(
220
- (event: MouseEvent) => {
221
- if (!isTopLayer || !onClickOutside) return
218
+ (event: MouseEvent | TouchEvent) => {
219
+ if (!isTopLayer || !onClickOutside) return
222
220
 
223
- const target = event.target as Node | null
221
+ const target = event.target as Node | null
224
222
 
225
- if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
226
- // Ignore clicks outside of the scope
227
- return
228
- }
223
+ if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
224
+ // Ignore clicks outside of the scope
225
+ return
226
+ }
229
227
 
230
- onClickOutside()
231
- },
232
- [boundaryElement, isTopLayer, onClickOutside, portalElement],
233
- ),
234
- [rootElement],
228
+ onClickOutside()
229
+ },
230
+ () => [ref.current],
235
231
  )
236
232
 
237
- const setRef = useCallback((el: HTMLDivElement | null) => {
238
- setRootElement(el)
239
- ref.current = el
240
- }, [])
241
-
242
233
  return (
243
234
  <DialogContainer data-ui="DialogCard" width={width}>
244
- <DialogCardRoot radius={radius} ref={setRef} scheme={scheme} shadow={shadow}>
235
+ <DialogCardRoot radius={radius} ref={ref} scheme={scheme} shadow={shadow}>
245
236
  <DialogLayout direction="column">
246
237
  {showHeader && (
247
238
  <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) => void
22
+ onClickOutside?: (event: MouseEvent | TouchEvent) => void
23
23
  onEscape?: () => void
24
24
  onItemClick?: () => void
25
25
  onItemSelect?: (index: number) => void
@@ -75,16 +75,18 @@ export const Menu = forwardRef(function Menu(
75
75
  handleItemMouseLeave,
76
76
  handleKeyDown,
77
77
  mount,
78
- rootElement,
79
- setRootElement,
80
- } = useMenuController({onKeyDown, originElement, shouldFocus})
78
+ } = useMenuController({onKeyDown, originElement, shouldFocus, rootElementRef: ref})
81
79
 
82
80
  const handleRefChange = useCallback(
83
81
  (el: HTMLDivElement | null) => {
84
- setRootElement(el)
85
82
  ref.current = el
83
+
84
+ // Register root element (for nested menus)
85
+ if (ref.current && registerElement) {
86
+ registerElement(ref.current)
87
+ }
86
88
  },
87
- [setRootElement],
89
+ [registerElement],
88
90
  )
89
91
 
90
92
  // Trigger `onItemSelect` when active index changes
@@ -94,11 +96,8 @@ export const Menu = forwardRef(function Menu(
94
96
 
95
97
  // Close menu when clicking outside
96
98
  useClickOutside(
97
- useCallback(
98
- (event) => isTopLayer && onClickOutside && onClickOutside(event),
99
- [isTopLayer, onClickOutside],
100
- ),
101
- [rootElement],
99
+ (event) => isTopLayer && onClickOutside && onClickOutside(event),
100
+ () => [ref.current],
102
101
  )
103
102
 
104
103
  // Close menu when pressing Escape
@@ -116,13 +115,7 @@ export const Menu = forwardRef(function Menu(
116
115
  ),
117
116
  )
118
117
 
119
- // Register root element (for nested menus)
120
- useEffect(() => {
121
- if (!rootElement || !registerElement) return
122
-
123
- return registerElement(rootElement)
124
- }, [registerElement, rootElement])
125
-
118
+ // @TODO split out into separate contexts
126
119
  const value: MenuContextValue = useMemo(
127
120
  () => ({
128
121
  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) => {
143
+ (event: MouseEvent | TouchEvent) => {
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) => void
8
+ onClickOutside?: (event: MouseEvent | TouchEvent) => 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, useRef, useState} from 'react'
1
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
2
2
  import {_getFocusableElements, _sortElements} from './helpers'
3
3
 
4
4
  /**
@@ -11,8 +11,6 @@ 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
16
14
  }
17
15
 
18
16
  /**
@@ -24,14 +22,14 @@ export function useMenuController(props: {
24
22
  onKeyDown?: React.KeyboardEventHandler
25
23
  originElement?: HTMLElement | null
26
24
  shouldFocus: 'first' | 'last' | null
25
+ rootElementRef: React.MutableRefObject<HTMLDivElement | null>
27
26
  }): MenuController {
28
- const {onKeyDown, originElement, shouldFocus} = props
27
+ const {onKeyDown, originElement, shouldFocus, rootElementRef} = props
29
28
  const elementsRef = useRef<HTMLElement[]>([])
30
- const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
31
29
  const [activeIndex, _setActiveIndex] = useState(-1)
32
30
  const activeIndexRef = useRef(activeIndex)
33
- const activeElement = elementsRef.current[activeIndex] || null
34
- const mounted = Boolean(rootElement)
31
+ const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
32
+ const mounted = Boolean(rootElementRef.current)
35
33
 
36
34
  const setActiveIndex = useCallback((nextActiveIndex: number) => {
37
35
  _setActiveIndex(nextActiveIndex)
@@ -44,7 +42,7 @@ export function useMenuController(props: {
44
42
 
45
43
  if (elementsRef.current.indexOf(element) === -1) {
46
44
  elementsRef.current.push(element)
47
- _sortElements(rootElement, elementsRef.current)
45
+ _sortElements(rootElementRef.current, elementsRef.current)
48
46
  }
49
47
 
50
48
  if (selected) {
@@ -61,7 +59,7 @@ export function useMenuController(props: {
61
59
  }
62
60
  }
63
61
  },
64
- [rootElement, setActiveIndex],
62
+ [rootElementRef, setActiveIndex],
65
63
  )
66
64
 
67
65
  const handleKeyDown = useCallback(
@@ -179,18 +177,15 @@ export function useMenuController(props: {
179
177
  // which would be incorrect when the user hovers over a gap
180
178
  // between two menu items or a menu divider.
181
179
  setActiveIndex(-2)
182
- rootElement?.focus()
183
- }, [setActiveIndex, rootElement])
180
+ rootElementRef.current?.focus()
181
+ }, [rootElementRef, setActiveIndex])
184
182
 
185
183
  // Set focus on the currently active element
186
- // @TODO verify if this is the best way, compare with adobe-spectrum
187
184
  useEffect(() => {
188
185
  if (!mounted) return
189
186
 
190
187
  const rafId = window.requestAnimationFrame(() => {
191
- const _activeIndex = activeIndexRef.current
192
-
193
- if (_activeIndex === -1) {
188
+ if (activeIndex === -1) {
194
189
  if (shouldFocus === 'first') {
195
190
  const focusableElements = _getFocusableElements(elementsRef.current)
196
191
  const el = focusableElements[0]
@@ -218,7 +213,7 @@ export function useMenuController(props: {
218
213
  return
219
214
  }
220
215
 
221
- const element = elementsRef.current[_activeIndex] || null
216
+ const element = elementsRef.current[activeIndex] || null
222
217
 
223
218
  element?.focus()
224
219
  })
@@ -235,7 +230,5 @@ export function useMenuController(props: {
235
230
  handleItemMouseLeave,
236
231
  handleKeyDown,
237
232
  mount,
238
- rootElement,
239
- setRootElement,
240
233
  }
241
234
  }
@@ -11,14 +11,14 @@ export interface TabProps {
11
11
  * The `id` of the correlating `TabPanel` component.
12
12
  */
13
13
  'aria-controls': string
14
- id: string
15
- icon?: React.ElementType | React.ReactNode
16
- focused?: boolean
17
- fontSize?: number | number[]
18
- label?: React.ReactNode
19
- padding?: number | number[]
20
- selected?: boolean
21
- tone?: ButtonTone
14
+ 'id': string
15
+ 'icon'?: React.ElementType | React.ReactNode
16
+ 'focused'?: boolean
17
+ 'fontSize'?: number | number[]
18
+ 'label'?: React.ReactNode
19
+ 'padding'?: number | number[]
20
+ 'selected'?: boolean
21
+ 'tone'?: ButtonTone
22
22
  }
23
23
 
24
24
  const CustomButton = styled(Button)`
@@ -9,7 +9,7 @@ export interface TabPanelProps extends BoxProps {
9
9
  * The `id` of the correlating `Tab` component.
10
10
  */
11
11
  'aria-labelledby': string
12
- id: string
12
+ 'id': string
13
13
  }
14
14
 
15
15
  /**
@@ -13,6 +13,7 @@ export function useToast(): ToastContextValue {
13
13
  throw new Error('useToast(): missing context value')
14
14
  }
15
15
 
16
+ // @TODO context and hooks doesn't really work like this, there will never be a mismatch between the provider and the consumer, we can remove these version specifiers
16
17
  // NOTE: This check is for future-compatiblity
17
18
  // - If the value is not an object, it’s not compatible with the current version
18
19
  // - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
@@ -84,6 +84,7 @@ export const Tree = memo(
84
84
  })
85
85
  }, [])
86
86
 
87
+ // @TODO split out into separate contexts
87
88
  const contextValue: TreeContextValue = useMemo(
88
89
  () => ({
89
90
  version: 0.0,