@sanity/ui 4.0.0-static.33 → 4.0.0-static.35

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 (32) hide show
  1. package/dist/index.d.ts +6 -20
  2. package/dist/index.js +284 -230
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/core/components/dialog/Dialog.tsx +16 -7
  6. package/src/core/components/dialog/DialogCard.tsx +4 -4
  7. package/src/core/components/dialog/DialogProvider.tsx +1 -1
  8. package/src/core/components/menu/Menu.test.tsx +6 -6
  9. package/src/core/components/menu/Menu.tsx +2 -2
  10. package/src/core/components/toast/Toast.test.tsx +6 -6
  11. package/src/core/components/toast/ToastProvider.tsx +2 -2
  12. package/src/core/components/tree/Tree.tsx +2 -2
  13. package/src/core/components/tree/TreeItem.tsx +4 -4
  14. package/src/core/helpers/props.ts +0 -9
  15. package/src/core/hooks/useResponsiveProp.ts +4 -17
  16. package/src/core/index.ts +4 -5
  17. package/src/core/primitives/avatar/Avatar.tsx +2 -2
  18. package/src/core/primitives/card/CardProvider.tsx +2 -11
  19. package/src/core/primitives/card/useCard.ts +8 -3
  20. package/src/core/primitives/layer/Layer.test.tsx +6 -6
  21. package/src/core/primitives/layer/LayerProvider.tsx +3 -3
  22. package/src/core/primitives/popover/Popover.tsx +22 -17
  23. package/src/core/primitives/textInput/TextInput.tsx +73 -53
  24. package/src/core/primitives/tooltip/Tooltip.tsx +30 -20
  25. package/src/core/primitives/tooltip/tooltipDelayGroup/TooltipDelayGroupProvider.tsx +1 -3
  26. package/src/core/utils/boundaryElement/BoundaryElementContext.ts +2 -5
  27. package/src/core/utils/boundaryElement/BoundaryElementProvider.tsx +2 -4
  28. package/src/core/utils/boundaryElement/types.ts +1 -4
  29. package/src/core/utils/boundaryElement/useBoundaryElement.ts +6 -18
  30. package/src/core/utils/portal/Portal.tsx +6 -3
  31. package/src/core/utils/portal/usePortal.ts +9 -2
  32. package/src/core/utils/boundaryElement/BoundaryElement.test.tsx +0 -102
@@ -23,8 +23,8 @@ import {
23
23
  import {isValidElementType} from 'react-is'
24
24
 
25
25
  import {EMPTY_RECORD} from '../../constants'
26
+ import {_getResponsiveProp} from '../../helpers/props'
26
27
  import {useCustomValidity} from '../../hooks/useCustomValidity'
27
- import {useResponsiveProp} from '../../hooks/useResponsiveProp'
28
28
  import {isRecord} from '../../lib/isRecord'
29
29
  import type {ComponentType, Props} from '../../types'
30
30
  import {Box} from '../box/Box'
@@ -107,7 +107,7 @@ export function TextInput<E extends TextInputElementType = typeof DEFAULT_TEXT_I
107
107
 
108
108
  const ref = useRef<HTMLInputElement | null>(null)
109
109
 
110
- const responsivePadding = useResponsiveProp(padding)
110
+ const responsivePadding = useMemo(() => _getResponsiveProp(padding), [padding])
111
111
 
112
112
  const withClearButton = Boolean(clearButton)
113
113
 
@@ -137,35 +137,6 @@ export function TextInput<E extends TextInputElementType = typeof DEFAULT_TEXT_I
137
137
  [onClear, ref],
138
138
  )
139
139
 
140
- const clearButtonBoxPadding = useMemo(() => {
141
- return Object.fromEntries(
142
- Object.entries(responsivePadding).map(([key, value]) => {
143
- if (value === 0) return [key, 0]
144
- if (value === 1) return [key, 1]
145
- if (value === 2) return [key, 1]
146
-
147
- return [key, typeof value === 'number' ? value - 2 : 0]
148
- }),
149
- ) as ResponsiveProp<Space>
150
- }, [responsivePadding])
151
-
152
- const clearButtonPadding = useMemo(() => {
153
- return Object.fromEntries(
154
- Object.entries(responsivePadding).map(([key, value]) => {
155
- if (value === 0) return [key, 0]
156
- if (value === 1) return [key, 0]
157
- if (value === 2) return [key, 1]
158
-
159
- return [key, typeof value === 'number' ? value - 1 : 0]
160
- }),
161
- ) as ResponsiveProp<Space>
162
- }, [responsivePadding])
163
-
164
- const clearButtonProps: TextInputClearButtonProps = useMemo(
165
- () => (isRecord(clearButton) ? clearButton : EMPTY_RECORD),
166
- [clearButton],
167
- )
168
-
169
140
  return (
170
141
  <span
171
142
  className={textInput({
@@ -224,28 +195,14 @@ export function TextInput<E extends TextInputElementType = typeof DEFAULT_TEXT_I
224
195
  </span>
225
196
 
226
197
  {!disabled && !readOnly && clearButton && (
227
- <Box
228
- as="span"
229
- insetTop={0}
230
- insetRight={0}
231
- padding={clearButtonBoxPadding}
232
- position="absolute"
233
- style={{zIndex: 2}}
234
- >
235
- <Button
236
- aria-label="Clear"
237
- data-qa="clear-button"
238
- display="block"
239
- fontSize={fontSize}
240
- icon={CloseIcon}
241
- mode="bleed"
242
- padding={clearButtonPadding}
243
- radius={radius}
244
- {...clearButtonProps}
245
- onClick={handleClearClick}
246
- onMouseDown={handleClearMouseDown}
247
- />
248
- </Box>
198
+ <ClearButton
199
+ fontSize={fontSize}
200
+ radius={radius}
201
+ clearButtonProps={isRecord(clearButton) ? clearButton : EMPTY_RECORD}
202
+ handleClearClick={handleClearClick}
203
+ handleClearMouseDown={handleClearMouseDown}
204
+ padding={responsivePadding}
205
+ />
249
206
  )}
250
207
  </span>
251
208
 
@@ -257,3 +214,66 @@ export function TextInput<E extends TextInputElementType = typeof DEFAULT_TEXT_I
257
214
  </span>
258
215
  )
259
216
  }
217
+
218
+ function ClearButton({
219
+ fontSize,
220
+ radius,
221
+ clearButtonProps,
222
+ handleClearClick,
223
+ handleClearMouseDown,
224
+ padding,
225
+ }: {
226
+ clearButtonProps: TextInputClearButtonProps
227
+ handleClearClick: (event: MouseEvent<HTMLButtonElement>) => void
228
+ handleClearMouseDown: (event: MouseEvent<HTMLButtonElement>) => void
229
+ padding: ResponsiveProp<Space>
230
+ } & Pick<TextInputOwnProps, 'fontSize' | 'radius'>) {
231
+ const clearButtonBoxPadding = useMemo(() => {
232
+ return Object.fromEntries(
233
+ Object.entries(padding).map(([key, value]) => {
234
+ if (value === 0) return [key, 0]
235
+ if (value === 1) return [key, 1]
236
+ if (value === 2) return [key, 1]
237
+
238
+ return [key, typeof value === 'number' ? value - 2 : 0]
239
+ }),
240
+ ) as ResponsiveProp<Space>
241
+ }, [padding])
242
+
243
+ const clearButtonPadding = useMemo(() => {
244
+ return Object.fromEntries(
245
+ Object.entries(padding).map(([key, value]) => {
246
+ if (value === 0) return [key, 0]
247
+ if (value === 1) return [key, 0]
248
+ if (value === 2) return [key, 1]
249
+
250
+ return [key, typeof value === 'number' ? value - 1 : 0]
251
+ }),
252
+ ) as ResponsiveProp<Space>
253
+ }, [padding])
254
+
255
+ return (
256
+ <Box
257
+ as="span"
258
+ insetTop={0}
259
+ insetRight={0}
260
+ padding={clearButtonBoxPadding}
261
+ position="absolute"
262
+ style={{zIndex: 2}}
263
+ >
264
+ <Button
265
+ aria-label="Clear"
266
+ data-qa="clear-button"
267
+ display="block"
268
+ fontSize={fontSize}
269
+ icon={CloseIcon}
270
+ mode="bleed"
271
+ padding={clearButtonPadding}
272
+ radius={radius}
273
+ {...clearButtonProps}
274
+ onClick={handleClearClick}
275
+ onMouseDown={handleClearMouseDown}
276
+ />
277
+ </Box>
278
+ )
279
+ }
@@ -19,6 +19,7 @@ import {
19
19
  type ReactElement,
20
20
  type ReactNode,
21
21
  type RefAttributes,
22
+ use,
22
23
  useCallback,
23
24
  useEffect,
24
25
  useId,
@@ -35,20 +36,21 @@ import {useDelayedState} from '../../hooks/useDelayedState'
35
36
  import {usePrefersReducedMotion} from '../../hooks/usePrefersReducedMotion'
36
37
  import {origin} from '../../middleware/origin'
37
38
  import type {ComponentType, Delay, Placement, Props} from '../../types'
38
- import {useBoundaryElement} from '../../utils/boundaryElement/useBoundaryElement'
39
+ import {BoundaryElementContext} from '../../utils/boundaryElement/BoundaryElementContext'
39
40
  import {getElementRef} from '../../utils/getElementRef'
40
41
  import {Portal} from '../../utils/portal/Portal'
41
- import {usePortal} from '../../utils/portal/usePortal'
42
+ import {PortalContext} from '../../utils/portal/PortalContext'
43
+ import {assertPortalContext} from '../../utils/portal/usePortal'
44
+ import {CardContext} from '../card/CardContext'
42
45
  import {CardProvider} from '../card/CardProvider'
43
- import {useCard} from '../card/useCard'
46
+ import {assertCardContext} from '../card/useCard'
44
47
  import type {LayerOwnProps} from '../layer/Layer'
45
48
  import {
46
49
  DEFAULT_FALLBACK_PLACEMENTS,
47
50
  DEFAULT_TOOLTIP_DISTANCE,
48
51
  DEFAULT_TOOLTIP_PADDING,
49
52
  } from './constants'
50
- import {useTooltipDelayGroup} from './tooltipDelayGroup/useTooltipDelayGroup'
51
- // import {useTooltipDelayGroup} from './TooltipDelayGroup'
53
+ import {TooltipDelayGroupContext} from './tooltipDelayGroup/TooltipDelayGroupContext'
52
54
  import {TooltipLayer} from './TooltipLayer'
53
55
 
54
56
  /** @public */
@@ -106,8 +108,6 @@ export type TooltipProps<E extends TooltipElementType = TooltipElementType> = Pr
106
108
  export function Tooltip<E extends TooltipElementType = typeof DEFAULT_TOOLTIP_ELEMENT>(
107
109
  props: TooltipProps<E>,
108
110
  ): React.JSX.Element {
109
- const boundaryElementContext = useBoundaryElement()
110
-
111
111
  const {
112
112
  animate: _animate = false,
113
113
  arrow: arrowProp = false,
@@ -130,11 +130,10 @@ export function Tooltip<E extends TooltipElementType = typeof DEFAULT_TOOLTIP_EL
130
130
  tone = 'inherit',
131
131
  ...rest
132
132
  } = props as TooltipProps<typeof DEFAULT_TOOLTIP_ELEMENT>
133
- const boundaryElement = _boundaryElement ?? boundaryElementContext?.element
133
+ const boundaryElement = _boundaryElement ?? use(BoundaryElementContext)
134
134
  const fallbackPlacements =
135
135
  _fallbackPlacements ?? DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom']
136
136
 
137
- const card = useCard()
138
137
  const prefersReducedMotion = usePrefersReducedMotion()
139
138
  const animate = prefersReducedMotion ? false : _animate
140
139
  const ref = useRef<HTMLDivElement | null>(null)
@@ -145,9 +144,13 @@ export function Tooltip<E extends TooltipElementType = typeof DEFAULT_TOOLTIP_EL
145
144
 
146
145
  useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
147
146
 
148
- const portal = usePortal()
149
- const portalElement =
150
- typeof portalProp === 'string' ? portal.elements?.[portalProp] || null : portal.element
147
+ let portalElement: HTMLElement | null = null
148
+ if (portalProp) {
149
+ const portal = use(PortalContext)
150
+ assertPortalContext(portal)
151
+ portalElement =
152
+ typeof portalProp === 'string' ? portal.elements?.[portalProp] || null : portal.element
153
+ }
151
154
 
152
155
  const middleware = useMiddleware({
153
156
  animate,
@@ -174,7 +177,7 @@ export function Tooltip<E extends TooltipElementType = typeof DEFAULT_TOOLTIP_EL
174
177
 
175
178
  const tooltipId = useId()
176
179
  const [isOpen, setIsOpen] = useDelayedState(false)
177
- const delayGroupContext = useTooltipDelayGroup()
180
+ const delayGroupContext = use(TooltipDelayGroupContext)
178
181
  const {setIsGroupActive, setOpenTooltipId} = delayGroupContext || {}
179
182
  const showTooltip = isOpen || delayGroupContext?.openTooltipId === tooltipId
180
183
 
@@ -385,17 +388,24 @@ export function Tooltip<E extends TooltipElementType = typeof DEFAULT_TOOLTIP_EL
385
388
  </TooltipLayer>
386
389
  )
387
390
 
388
- const children =
389
- showTooltip &&
390
- (portalProp ? (
391
+ let children = showTooltip ? node : null
392
+
393
+ if (showTooltip && portalProp) {
394
+ let resolvedTone = tone as Exclude<typeof tone, 'inherit'>
395
+ if (tone === 'inherit') {
396
+ const card = use(CardContext)
397
+ assertCardContext(card)
398
+ resolvedTone = card.tone
399
+ }
400
+
401
+ children = (
391
402
  <Portal __unstable_name={typeof portalProp === 'string' ? portalProp : undefined}>
392
- <CardProvider tone={tone === 'inherit' ? card.tone : tone} scheme={scheme ?? 'light'}>
403
+ <CardProvider tone={resolvedTone} scheme={scheme ?? 'light'}>
393
404
  {node}
394
405
  </CardProvider>
395
406
  </Portal>
396
- ) : (
397
- node
398
- ))
407
+ )
408
+ }
399
409
 
400
410
  return (
401
411
  <>
@@ -49,7 +49,5 @@ export function TooltipDelayGroupProvider(
49
49
  [closeDelay, isGroupActive, openDelay, openTooltipId, setIsGroupActive, setOpenTooltipId],
50
50
  )
51
51
 
52
- return (
53
- <TooltipDelayGroupContext.Provider value={value}>{children}</TooltipDelayGroupContext.Provider>
54
- )
52
+ return <TooltipDelayGroupContext value={value}>{children}</TooltipDelayGroupContext>
55
53
  }
@@ -3,8 +3,5 @@ import type {Context} from 'react'
3
3
  import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
4
4
  import type {BoundaryElementContextValue} from './types'
5
5
 
6
- export const BoundaryElementContext: Context<BoundaryElementContextValue | null> =
7
- createGlobalScopedContext<BoundaryElementContextValue | null>(
8
- '@sanity/ui/v4/boundaryElement',
9
- null,
10
- )
6
+ export const BoundaryElementContext: Context<BoundaryElementContextValue> =
7
+ createGlobalScopedContext<BoundaryElementContextValue>('@sanity/ui/v4/boundaryElement', null)
@@ -1,7 +1,6 @@
1
- import {type ReactNode, useMemo} from 'react'
1
+ import {type ReactNode} from 'react'
2
2
 
3
3
  import {BoundaryElementContext} from './BoundaryElementContext'
4
- import type {BoundaryElementContextValue} from './types'
5
4
 
6
5
  /** @public */
7
6
  export interface BoundaryElementProviderProps {
@@ -12,7 +11,6 @@ export interface BoundaryElementProviderProps {
12
11
  /** @public */
13
12
  export function BoundaryElementProvider(props: BoundaryElementProviderProps): React.JSX.Element {
14
13
  const {children, element} = props
15
- const value: BoundaryElementContextValue = useMemo(() => ({version: 0.0, element}), [element])
16
14
 
17
- return <BoundaryElementContext.Provider value={value}>{children}</BoundaryElementContext.Provider>
15
+ return <BoundaryElementContext value={element}>{children}</BoundaryElementContext>
18
16
  }
@@ -1,5 +1,2 @@
1
1
  /** @public */
2
- export interface BoundaryElementContextValue {
3
- version: 0.0
4
- element: HTMLElement | null
5
- }
2
+ export type BoundaryElementContextValue = HTMLElement | null
@@ -1,24 +1,12 @@
1
- import {use} from 'react'
1
+ import {use, useMemo} from 'react'
2
2
 
3
- import {isRecord} from '../../lib/isRecord'
4
3
  import {BoundaryElementContext} from './BoundaryElementContext'
5
4
  import type {BoundaryElementContextValue} from './types'
6
5
 
7
- const DEFAULT_VALUE: BoundaryElementContextValue = {
8
- version: 0.0,
9
- element: null,
10
- }
11
-
12
6
  /** @public */
13
- export function useBoundaryElement(): BoundaryElementContextValue {
14
- const value = use(BoundaryElementContext)
15
-
16
- // NOTE: This check is for future-compatiblity
17
- // - If the value is not an object, it’s not compatible with the current version
18
- // - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
19
- if (value && (!isRecord(value) || value.version !== 0.0)) {
20
- throw new Error('useBoundaryElement(): the context value is not compatible')
21
- }
22
-
23
- return value || DEFAULT_VALUE
7
+ export function useBoundaryElement(): {version: 0.0; element: BoundaryElementContextValue} {
8
+ // eslint-disable-next-line no-warning-comments
9
+ // @TODO this wrapping is for BC and should be removed
10
+ const element = use(BoundaryElementContext)
11
+ return useMemo(() => ({version: 0.0, element}), [element])
24
12
  }
@@ -1,8 +1,9 @@
1
- import type {ReactNode, ReactPortal} from 'react'
1
+ import {type ReactNode, type ReactPortal, use} from 'react'
2
2
  import {createPortal} from 'react-dom'
3
3
 
4
+ import {CardContext} from '../../primitives/card/CardContext'
4
5
  import {CardProvider} from '../../primitives/card/CardProvider'
5
- import {useCard} from '../../primitives/card/useCard'
6
+ import {assertCardContext} from '../../primitives/card/useCard'
6
7
  import {usePortal} from './usePortal'
7
8
 
8
9
  /** @public */
@@ -17,7 +18,6 @@ export interface PortalProps {
17
18
  /** @public */
18
19
  export function Portal(props: PortalProps): ReactPortal | null {
19
20
  const {children, __unstable_name: name} = props
20
- const card = useCard()
21
21
  const portal = usePortal()
22
22
  const portalElement =
23
23
  (name ? portal.elements && portal.elements[name] : portal.element) ||
@@ -27,6 +27,9 @@ export function Portal(props: PortalProps): ReactPortal | null {
27
27
  return null
28
28
  }
29
29
 
30
+ const card = use(CardContext)
31
+ assertCardContext(card)
32
+
30
33
  return createPortal(
31
34
  <CardProvider tone="transparent" scheme={card.scheme}>
32
35
  {children}
@@ -8,6 +8,15 @@ import type {PortalContextValue} from './types'
8
8
  export function usePortal(): PortalContextValue {
9
9
  const value = use(PortalContext)
10
10
 
11
+ assertPortalContext(value)
12
+
13
+ return value
14
+ }
15
+
16
+ /** @internal */
17
+ export function assertPortalContext(
18
+ value: PortalContextValue | null,
19
+ ): asserts value is PortalContextValue {
11
20
  if (!value) {
12
21
  throw new Error('usePortal(): missing context value')
13
22
  }
@@ -18,6 +27,4 @@ export function usePortal(): PortalContextValue {
18
27
  if (!isRecord(value) || value.version !== 0.0) {
19
28
  throw new Error('usePortal(): the context value is not compatible')
20
29
  }
21
-
22
- return value
23
30
  }
@@ -1,102 +0,0 @@
1
- import {describe, expect, it, vi} from 'vitest'
2
-
3
- import {render} from '$test/utils'
4
-
5
- import {BoundaryElementContext} from './BoundaryElementContext'
6
- import type {BoundaryElementContextValue} from './types'
7
- import {useBoundaryElement} from './useBoundaryElement'
8
-
9
- describe('utils/boundaryElement', () => {
10
- describe('useBoundaryElement', () => {
11
- it('should get context value', async () => {
12
- const log = vi.fn()
13
-
14
- function Debug() {
15
- const rootBoundaryElement = useBoundaryElement()
16
-
17
- log(rootBoundaryElement)
18
-
19
- return <>debug</>
20
- }
21
-
22
- function Root() {
23
- const value: BoundaryElementContextValue = {
24
- version: 0.0,
25
- element: null,
26
- }
27
-
28
- return (
29
- <BoundaryElementContext.Provider value={value}>
30
- <Debug />
31
- </BoundaryElementContext.Provider>
32
- )
33
- }
34
-
35
- render(<Root />)
36
-
37
- expect(log.mock.calls[0][0].version).toBe(0.0)
38
- expect(log.mock.calls[0][0].element).toBe(null)
39
- })
40
-
41
- it('should provide default value when no context value is provided', async () => {
42
- const log = vi.fn()
43
-
44
- function Debug() {
45
- const rootBoundaryElement = useBoundaryElement()
46
-
47
- log(rootBoundaryElement)
48
-
49
- return null
50
- }
51
-
52
- function Root() {
53
- return (
54
- <BoundaryElementContext.Provider
55
- // @ts-expect-error - we want to test the error case
56
- value={undefined}
57
- >
58
- <Debug />
59
- </BoundaryElementContext.Provider>
60
- )
61
- }
62
-
63
- render(<Root />)
64
-
65
- expect(log.mock.calls[0][0].version).toBe(0.0)
66
- expect(log.mock.calls[0][0].element).toBe(null)
67
- })
68
-
69
- it('should fail when context value is not compatible', async () => {
70
- const log = vi.fn()
71
-
72
- function Debug() {
73
- try {
74
- useBoundaryElement()
75
- } catch (err) {
76
- log(err)
77
- }
78
-
79
- return null
80
- }
81
-
82
- function Root() {
83
- // NOTE: we’re testing this because the context value may be a function in the future
84
-
85
- return (
86
- <BoundaryElementContext.Provider
87
- // @ts-expect-error - we want to test the error case
88
- value={() => ({version: 1})}
89
- >
90
- <Debug />
91
- </BoundaryElementContext.Provider>
92
- )
93
- }
94
-
95
- render(<Root />)
96
-
97
- expect(log.mock.calls[0][0].message).toEqual(
98
- 'useBoundaryElement(): the context value is not compatible',
99
- )
100
- })
101
- })
102
- })