@sanity/ui 2.6.4-canary.2 → 2.6.4

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 (37) hide show
  1. package/dist/index.d.mts +6 -42
  2. package/dist/index.d.ts +6 -42
  3. package/dist/index.esm.js +156 -90
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +154 -88
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +156 -90
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +34 -26
  10. package/src/core/components/breadcrumbs/breadcrumbs.tsx +6 -15
  11. package/src/core/components/dialog/dialog.tsx +21 -12
  12. package/src/core/components/menu/menu.tsx +18 -11
  13. package/src/core/components/menu/menuButton.tsx +1 -1
  14. package/src/core/components/menu/menuContext.ts +1 -1
  15. package/src/core/components/menu/useMenuController.ts +17 -11
  16. package/src/core/components/toast/styles.ts +1 -2
  17. package/src/core/components/toast/useToast.ts +0 -1
  18. package/src/core/components/tree/tree.tsx +0 -1
  19. package/src/core/components/tree/treeItem.tsx +0 -1
  20. package/src/core/helpers/focus.ts +0 -1
  21. package/src/core/helpers/scroll.ts +0 -1
  22. package/src/core/hooks/_internal/index.ts +1 -0
  23. package/src/core/hooks/_internal/useUnique.ts +34 -0
  24. package/src/core/hooks/index.ts +2 -3
  25. package/src/core/hooks/useClickOutside.ts +72 -38
  26. package/src/core/hooks/useElementSize.ts +0 -1
  27. package/src/core/hooks/useMediaIndex/useMediaIndex.ts +10 -2
  28. package/src/core/hooks/usePrefersDark.ts +55 -10
  29. package/src/core/hooks/usePrefersReducedMotion.ts +56 -10
  30. package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +7 -9
  31. package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +6 -8
  32. package/src/core/primitives/tooltip/tooltip.tsx +21 -25
  33. package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +0 -1
  34. package/src/core/utils/layer/layerProvider.tsx +0 -1
  35. package/src/core/utils/portal/__workshop__/named.tsx +8 -10
  36. package/src/core/utils/portal/portalProvider.tsx +3 -2
  37. 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.4-canary.2",
3
+ "version": "2.6.4",
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"
@@ -168,37 +194,19 @@
168
194
  "react-is": "^18",
169
195
  "styled-components": "^5.2 || ^6"
170
196
  },
197
+ "packageManager": "pnpm@9.5.0",
171
198
  "engines": {
172
199
  "node": ">=14.0.0"
173
200
  },
174
201
  "publishConfig": {
175
202
  "access": "public"
176
203
  },
204
+ "pnpm": {
205
+ "overrides": {
206
+ "conventional-changelog-conventionalcommits": ">= 8.0.0"
207
+ }
208
+ },
177
209
  "esm.sh": {
178
210
  "bundle": false
179
- },
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"
203
211
  }
204
- }
212
+ }
@@ -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>
@@ -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)) {
@@ -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
  }
@@ -14,12 +14,11 @@ const loadingAnimation = keyframes`
14
14
  }
15
15
  100% {
16
16
  width: 100%;
17
- }
17
+ }
18
18
  `
19
19
 
20
20
  const LOADING_BAR_HEIGHT = 2
21
21
 
22
- // @TODO get rid of $duration modifier, set data attribute instead and use stable selector
23
22
  export function rootStyles(
24
23
  props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
25
24
  ): ReturnType<typeof css> {
@@ -13,7 +13,6 @@ 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
17
16
  // NOTE: This check is for future-compatiblity
18
17
  // - If the value is not an object, it’s not compatible with the current version
19
18
  // - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
@@ -84,7 +84,6 @@ export const Tree = memo(
84
84
  })
85
85
  }, [])
86
86
 
87
- // @TODO split out into separate contexts
88
87
  const contextValue: TreeContextValue = useMemo(
89
88
  () => ({
90
89
  version: 0.0,
@@ -76,7 +76,6 @@ export const TreeItem = memo(function TreeItem(
76
76
  const focused = tree.focusedElement === rootRef.current
77
77
  const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
78
78
  const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
79
- // @TODO split out into separate contexts
80
79
  const contextValue = useMemo(
81
80
  () => ({...tree, level: tree.level + 1, path: itemPath}),
82
81
  [itemPath, tree],
@@ -15,7 +15,6 @@ import {
15
15
  * @internal
16
16
  */
17
17
  export function _hasFocus(element: HTMLElement): boolean {
18
- // @TODO verify this is not called during render
19
18
  return Boolean(document.activeElement) && element.contains(document.activeElement)
20
19
  }
21
20
 
@@ -4,7 +4,6 @@
4
4
  export function _isScrollable(el: Node): boolean {
5
5
  if (!(el instanceof Element)) return false
6
6
 
7
- // @TODO check if this is called during render
8
7
  const style = window.getComputedStyle(el)
9
8
 
10
9
  return (
@@ -0,0 +1 @@
1
+ export * from './useUnique'
@@ -0,0 +1,34 @@
1
+ import {useRef} from 'react'
2
+
3
+ /**
4
+ * This is a React hook to make sure that a record identity is the same on every render. Uses strict
5
+ * equality comparison (eg by identity), and only goes one level deep.
6
+ *
7
+ * @internal
8
+ */
9
+ type Comparable = Record<string | number | symbol, unknown> | undefined | null
10
+
11
+ export function useUnique<ValueType extends Comparable = Comparable>(value: ValueType): ValueType {
12
+ const valueRef = useRef<ValueType>(value)
13
+
14
+ if (!_isEqual(valueRef.current, value)) {
15
+ valueRef.current = value
16
+ }
17
+
18
+ return valueRef.current
19
+ }
20
+
21
+ function _isEqual(objA: Comparable, objB: Comparable): boolean {
22
+ if (!objA || !objB) {
23
+ return objA === objB
24
+ }
25
+
26
+ const keysA = Object.keys(objA)
27
+ const keysB = Object.keys(objB)
28
+
29
+ if (keysA.length !== keysB.length) {
30
+ return false
31
+ }
32
+
33
+ return keysA.every((key) => objA[key] === objB[key])
34
+ }
@@ -1,11 +1,10 @@
1
1
  export * from './useArrayProp'
2
2
  export * from './useClickOutside'
3
- export * from './useCustomValidity'
4
3
  export * from './useElementRect'
5
4
  export * from './useElementSize'
6
- export * from './useForwardedRef'
7
5
  export * from './useGlobalKeyDown'
8
- export * from './useMatchMedia'
9
6
  export * from './useMediaIndex'
10
7
  export * from './usePrefersDark'
11
8
  export * from './usePrefersReducedMotion'
9
+ export * from './useForwardedRef'
10
+ export * from './useCustomValidity'
@@ -1,65 +1,99 @@
1
- import {useEffect} from 'react'
2
- import {useEffectEvent} from 'use-effect-event'
1
+ import {useEffect, useRef, useState} from 'react'
3
2
  import {EMPTY_ARRAY} from '../constants'
4
3
 
5
4
  /**
6
5
  * @public
7
6
  */
8
- export type ClickOutsideListener = (event: MouseEvent | TouchEvent) => void
7
+ export type ClickOutsideListener = (event: MouseEvent) => void
8
+
9
+ function _getElements(
10
+ element: HTMLElement | null,
11
+ elementsArg: Array<HTMLElement | HTMLElement[] | null>,
12
+ ): HTMLElement[] {
13
+ const ret = [element]
14
+
15
+ for (const el of elementsArg) {
16
+ if (Array.isArray(el)) {
17
+ ret.push(...el)
18
+ } else {
19
+ ret.push(el)
20
+ }
21
+ }
22
+
23
+ return ret.filter(Boolean) as HTMLElement[]
24
+ }
9
25
 
10
26
  /**
11
- * Use the callback version of `elementsArg` if you're using `useRef` to handle elements:
12
- * ```tsx
13
- * useClickOutside(
14
- * () => {},
15
- * () => [ref.current],
16
- * )
17
- *
18
27
  * @public
19
28
  */
20
29
  export function useClickOutside(
21
30
  listener: ClickOutsideListener,
22
- elementsArg:
23
- | Array<HTMLElement | HTMLElement[] | null>
24
- | (() => Array<HTMLElement | HTMLElement[] | null>) = EMPTY_ARRAY,
31
+ elementsArg: Array<HTMLElement | HTMLElement[] | null> = EMPTY_ARRAY,
25
32
  boundaryElement?: HTMLElement | null,
26
- ): void {
27
- /**
28
- * The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
29
- * juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
30
- */
31
- const eventHandler = useEffectEvent((evt: MouseEvent | TouchEvent) => {
32
- const target = evt.target
33
-
34
- if (!(target instanceof Node)) {
35
- return
36
- }
33
+ ): (el: HTMLElement | null) => void {
34
+ const [element, setElement] = useState<HTMLElement | null>(null)
35
+ const [elements, setElements] = useState(() => _getElements(element, elementsArg))
36
+ const elementsRef = useRef(elements)
37
+
38
+ useEffect(() => {
39
+ const prevElements = elementsRef.current
40
+ const nextElements = _getElements(element, elementsArg)
41
+
42
+ if (prevElements.length !== nextElements.length) {
43
+ setElements(nextElements)
44
+ elementsRef.current = nextElements
37
45
 
38
- if (boundaryElement && !boundaryElement.contains(target)) {
39
46
  return
40
47
  }
41
48
 
42
- const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
43
- const elements = resolvedElements.flat()
44
-
45
- for (const el of elements) {
46
- if (!el) continue
49
+ for (const el of prevElements) {
50
+ if (!nextElements.includes(el)) {
51
+ setElements(nextElements)
52
+ elementsRef.current = nextElements
47
53
 
48
- if (target === el || el.contains(target)) {
49
54
  return
50
55
  }
51
56
  }
52
57
 
53
- listener(evt)
54
- })
58
+ for (const el of nextElements) {
59
+ if (!prevElements.includes(el)) {
60
+ setElements(nextElements)
61
+ elementsRef.current = nextElements
62
+
63
+ return
64
+ }
65
+ }
66
+ }, [element, elementsArg])
55
67
 
56
68
  useEffect(() => {
57
- document.addEventListener('mousedown', eventHandler)
58
- document.addEventListener('touchstart', eventHandler)
69
+ if (!listener) return undefined
70
+
71
+ const handleWindowMouseDown = (evt: MouseEvent) => {
72
+ const target = evt.target
73
+
74
+ if (!(target instanceof Node)) {
75
+ return
76
+ }
77
+
78
+ if (boundaryElement && !boundaryElement.contains(target)) {
79
+ return
80
+ }
81
+
82
+ for (const el of elements) {
83
+ if (target === el || el.contains(target)) {
84
+ return
85
+ }
86
+ }
87
+
88
+ listener(evt)
89
+ }
90
+
91
+ window.addEventListener('mousedown', handleWindowMouseDown)
59
92
 
60
93
  return () => {
61
- document.removeEventListener('mousedown', eventHandler)
62
- document.removeEventListener('touchstart', eventHandler)
94
+ window.removeEventListener('mousedown', handleWindowMouseDown)
63
95
  }
64
- }, [eventHandler])
96
+ }, [boundaryElement, listener, elements])
97
+
98
+ return setElement
65
99
  }
@@ -6,7 +6,6 @@ import {ElementSize, _elementSizeObserver} from '../observers/elementSizeObserve
6
6
  * @beta
7
7
  */
8
8
  export function useElementSize(element: HTMLElement | null): ElementSize | null {
9
- // @TODO we can probably use something in framer-motion or @floating-ui instead of rolling our own
10
9
  const [size, setSize] = useState<ElementSize | null>(null)
11
10
 
12
11
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import {useMemo, useSyncExternalStore} from 'react'
1
+ import {useSyncExternalStore} from 'react'
2
2
  import {useTheme_v2} from '../../theme'
3
3
 
4
4
  /**
@@ -9,6 +9,8 @@ export interface _MediaStore {
9
9
  getSnapshot: () => number
10
10
  }
11
11
 
12
+ const MEDIA_STORE_CACHE = new WeakMap<number[], _MediaStore>()
13
+
12
14
  type MediaQueryMinWidth = `(min-width: ${number}px)`
13
15
  type MediaQueryMaxWidth = `(max-width: ${number}px)`
14
16
  type MediaQueryMinMaxWidth = `${MediaQueryMinWidth} and ${MediaQueryMaxWidth}`
@@ -95,7 +97,13 @@ function getServerSnapshot() {
95
97
  */
96
98
  export function useMediaIndex(): number {
97
99
  const {media} = useTheme_v2()
98
- const store = useMemo(() => _createMediaStore(media), [media])
100
+
101
+ let store = MEDIA_STORE_CACHE.get(media)
102
+
103
+ if (!store) {
104
+ store = _createMediaStore(media)
105
+ MEDIA_STORE_CACHE.set(media, store)
106
+ }
99
107
 
100
108
  return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot)
101
109
  }