@sanity/ui 4.0.0-static.30 → 4.0.0-static.32

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 (47) hide show
  1. package/dist/index.d.ts +8 -1
  2. package/dist/index.js +1076 -510
  3. package/dist/index.js.map +1 -1
  4. package/package.json +10 -11
  5. package/src/core/components/autocomplete/Autocomplete.tsx +80 -48
  6. package/src/core/components/breadcrumbs/Breadcrumbs.tsx +74 -41
  7. package/src/core/components/dialog/DialogContext.ts +1 -1
  8. package/src/core/components/dialog/__workshop__/Panes.tsx +5 -21
  9. package/src/core/components/dialog/__workshop__/styles.css.ts +18 -0
  10. package/src/core/components/menu/Menu.test.tsx +2 -6
  11. package/src/core/components/menu/Menu.tsx +13 -14
  12. package/src/core/components/menu/MenuContext.ts +2 -3
  13. package/src/core/components/menu/MenuGroup.tsx +2 -0
  14. package/src/core/components/menu/useMenu.ts +2 -2
  15. package/src/core/components/menu/useMenuController.ts +5 -5
  16. package/src/core/components/toast/ToastContext.ts +1 -1
  17. package/src/core/components/tree/Tree.tsx +1 -0
  18. package/src/core/components/tree/TreeContext.ts +1 -1
  19. package/src/core/components/tree/TreeItem.tsx +17 -14
  20. package/src/core/components/virtualList/VirtualList.tsx +86 -44
  21. package/src/core/hooks/useGlobalKeyDown.ts +11 -4
  22. package/src/core/lib/createGlobalScopedContext.ts +2 -2
  23. package/src/core/primitives/avatar/Avatar.tsx +1 -0
  24. package/src/core/primitives/button/Button.tsx +2 -1
  25. package/src/core/primitives/button/__workshop__/SanityUploadButton.tsx +10 -25
  26. package/src/core/primitives/button/__workshop__/Styled1.tsx +2 -8
  27. package/src/core/primitives/button/__workshop__/index.ts +0 -5
  28. package/src/core/primitives/button/__workshop__/styles.css.ts +30 -0
  29. package/src/core/primitives/card/CardContext.ts +1 -1
  30. package/src/core/primitives/card/__workshop__/Styled.tsx +2 -5
  31. package/src/core/primitives/flex/__workshop__/Example.tsx +7 -15
  32. package/src/core/primitives/flex/__workshop__/styles.css.ts +12 -0
  33. package/src/core/primitives/inline/Inline.tsx +1 -0
  34. package/src/core/primitives/layer/LayerContext.ts +1 -1
  35. package/src/core/primitives/popover/Popover.tsx +146 -130
  36. package/src/core/primitives/popover/floating-ui/size.ts +17 -16
  37. package/src/core/primitives/skeleton/Skeleton.tsx +2 -2
  38. package/src/core/primitives/text/__workshop__/Colored.tsx +5 -10
  39. package/src/core/primitives/tooltip/Tooltip.tsx +69 -41
  40. package/src/core/primitives/tooltip/tooltipDelayGroup/TooltipDelayGroupContext.tsx +1 -1
  41. package/src/core/utils/boundaryElement/BoundaryElementContext.ts +1 -1
  42. package/src/core/utils/elementQuery/__workshop__/CustomMedia.tsx +3 -25
  43. package/src/core/utils/elementQuery/__workshop__/styles.css.ts +31 -0
  44. package/src/core/utils/portal/Portal.test.tsx +6 -6
  45. package/src/core/utils/portal/PortalContext.ts +1 -1
  46. package/src/core/utils/portal/PortalProvider.tsx +5 -37
  47. package/src/core/primitives/button/__workshop__/Styled2.tsx +0 -19
@@ -7,12 +7,12 @@ import {
7
7
  type MouseEvent,
8
8
  type MouseEventHandler,
9
9
  type ReactNode,
10
- type Ref,
11
10
  useCallback,
12
11
  useEffect,
13
12
  useId,
14
13
  useMemo,
15
14
  useRef,
15
+ useState,
16
16
  } from 'react'
17
17
 
18
18
  import {Box, type BoxProps} from '../../primitives/box/Box'
@@ -81,17 +81,19 @@ export function TreeItem<E extends TreeItemElementType = typeof DEFAULT_TREE_ITE
81
81
  ...rest
82
82
  } = props as TreeItemProps<typeof DEFAULT_TREE_ITEM_ELEMENT>
83
83
 
84
- const rootRef = useRef<HTMLElement | null>(null)
84
+ const [rootElement, setRootElement] = useState<HTMLLIElement | null>(null)
85
85
  const treeitemRef = useRef<HTMLDivElement | null>(null)
86
86
  const tree = useTree()
87
87
  const {path, registerItem, setExpanded, setFocusedElement} = tree
88
88
  const _id = useId()
89
89
  const id = idProp || _id
90
- const itemPath = useMemo(() => path.concat([id || '']), [id, path])
91
- const itemKey = itemPath.join('/')
90
+ const [itemPath, itemKey] = useMemo(() => {
91
+ const itemPath = path.concat([id || ''])
92
+ return [itemPath, itemPath.join('/')]
93
+ }, [id, path])
92
94
  const itemState = tree.state[itemKey]
93
95
  const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
94
- const focused = tree.focusedElement && tree.focusedElement === rootRef.current
96
+ const focused = tree.focusedElement && tree.focusedElement === rootElement
95
97
  const tabIndex = focused ? 0 : -1
96
98
  const contextValue = useMemo(
97
99
  () => ({...tree, level: tree.level + 1, path: itemPath}),
@@ -111,28 +113,28 @@ export function TreeItem<E extends TreeItemElementType = typeof DEFAULT_TREE_ITE
111
113
  ) {
112
114
  event.stopPropagation()
113
115
  setExpanded(itemKey, !expanded)
114
- setFocusedElement(rootRef.current)
116
+ setFocusedElement(rootElement)
115
117
  }
116
118
  },
117
- [expanded, itemKey, onClick, setExpanded, setFocusedElement],
119
+ [expanded, itemKey, onClick, rootElement, setExpanded, setFocusedElement],
118
120
  )
119
121
 
120
122
  const handleKeyDown = useCallback(
121
123
  (event: KeyboardEvent<HTMLElement>) => {
122
124
  if (focused && event.key === 'Enter') {
123
- const el = treeitemRef.current || rootRef.current
125
+ const el = treeitemRef.current || rootElement
124
126
 
125
127
  el?.click()
126
128
  }
127
129
  },
128
- [focused],
130
+ [focused, rootElement],
129
131
  )
130
132
 
131
133
  useEffect(() => {
132
- if (!rootRef.current) return
134
+ if (!rootElement) return
133
135
 
134
- return registerItem(rootRef.current, itemPath.join('/'), expanded, selected)
135
- }, [expanded, itemPath, registerItem, selected])
136
+ return registerItem(rootElement, itemKey, expanded, selected)
137
+ }, [expanded, itemKey, registerItem, rootElement, selected])
136
138
 
137
139
  const content = (
138
140
  <Flex gap={space} padding={padding}>
@@ -172,7 +174,7 @@ export function TreeItem<E extends TreeItemElementType = typeof DEFAULT_TREE_ITE
172
174
  as="li"
173
175
  className={treeItem({className})}
174
176
  onClick={handleClick}
175
- ref={rootRef as Ref<HTMLLIElement>}
177
+ ref={setRootElement}
176
178
  role="none"
177
179
  >
178
180
  <Selectable
@@ -211,7 +213,8 @@ export function TreeItem<E extends TreeItemElementType = typeof DEFAULT_TREE_ITE
211
213
  className={treeItem({className})}
212
214
  onClick={handleClick}
213
215
  onKeyDown={handleKeyDown}
214
- ref={rootRef as Ref<HTMLAnchorElement>}
216
+ // @ts-expect-error - TODO: fix this
217
+ ref={setRootElement}
215
218
  role="treeitem"
216
219
  tabIndex={tabIndex}
217
220
  >
@@ -3,6 +3,7 @@ import type {Space} from '@sanity/ui/theme'
3
3
  import {
4
4
  type CSSProperties,
5
5
  type ReactNode,
6
+ startTransition,
6
7
  useEffect,
7
8
  useImperativeHandle,
8
9
  useMemo,
@@ -29,7 +30,10 @@ export interface VirtualListChangeOpts {
29
30
  }
30
31
 
31
32
  /** @beta */
32
- export type VirtualListOwnProps<Item = any> = {
33
+ export type VirtualListOwnProps<
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ Item = any,
36
+ > = {
33
37
  gap?: Space
34
38
  getItemKey?: (item: Item, itemIndex: number) => string
35
39
  items?: Item[]
@@ -113,6 +117,7 @@ export function VirtualList<E extends VirtualListElementType = typeof DEFAULT_VI
113
117
  window.addEventListener('scroll', handleScroll, {passive: true})
114
118
  window.addEventListener('resize', handleResize)
115
119
 
120
+ // eslint-disable-next-line react-hooks/set-state-in-effect
116
121
  setScrollHeight(window.innerHeight)
117
122
 
118
123
  handleScroll()
@@ -141,51 +146,21 @@ export function VirtualList<E extends VirtualListElementType = typeof DEFAULT_VI
141
146
 
142
147
  useEffect(() => {
143
148
  // console.log('renderItem', renderItem)
144
- setItemHeight(-1)
149
+ startTransition(() => setItemHeight(-1))
145
150
  }, [renderItem])
146
151
 
147
- const children = useMemo(() => {
148
- if (!renderItem || items.length === 0) return null
149
-
150
- if (itemHeight === -1) {
151
- return [
152
- <>
153
- <Box
154
- ref={(el) => (el ? setItemHeight(el.offsetHeight) : undefined)}
155
- insetTop={0}
156
- insetLeft={0}
157
- insetRight={0}
158
- key={0}
159
- position="absolute"
160
- >
161
- {renderItem(items[0])}
162
- </Box>
163
- <div
164
- ref={(el) => (el ? setGapValue(el.offsetHeight) : undefined)}
165
- style={{height: vars.space[gap]}}
166
- />
167
- </>,
168
- ]
169
- }
170
-
171
- return items.slice(fromIndex, toIndex).map((item, _itemIndex) => {
172
- const itemIndex = fromIndex + _itemIndex
173
- const node = renderItem(item)
174
- const key = getItemKey ? getItemKey(item, itemIndex) : itemIndex
175
-
176
- return (
177
- <Box
178
- insetLeft={0}
179
- insetRight={0}
180
- key={key}
181
- position="absolute"
182
- style={{top: itemIndex * (itemHeight + gapValue)}}
183
- >
184
- {node}
185
- </Box>
186
- )
187
- })
188
- }, [fromIndex, gap, gapValue, getItemKey, itemHeight, items, renderItem, toIndex])
152
+ const children = useChildren({
153
+ fromIndex,
154
+ gap,
155
+ getItemKey,
156
+ itemHeight,
157
+ items,
158
+ renderItem,
159
+ toIndex,
160
+ gapValue,
161
+ setGapValue,
162
+ setItemHeight,
163
+ })
189
164
 
190
165
  const wrapperStyle: CSSProperties = useMemo(() => ({height}), [height])
191
166
 
@@ -198,6 +173,73 @@ export function VirtualList<E extends VirtualListElementType = typeof DEFAULT_VI
198
173
  )
199
174
  }
200
175
 
176
+ function useChildren({
177
+ fromIndex,
178
+ gap,
179
+ getItemKey,
180
+ itemHeight,
181
+ items,
182
+ renderItem,
183
+ toIndex,
184
+ gapValue,
185
+ setGapValue,
186
+ setItemHeight,
187
+ }: Pick<VirtualListProps, 'getItemKey' | 'renderItem'> &
188
+ Required<Pick<VirtualListProps, 'items'>> & {
189
+ fromIndex: number
190
+ gap: Space
191
+ itemHeight: number
192
+ toIndex: number
193
+ gapValue: number
194
+ setGapValue: React.Dispatch<React.SetStateAction<number>>
195
+ setItemHeight: React.Dispatch<React.SetStateAction<number>>
196
+ }) {
197
+ if (!renderItem || items.length === 0) return null
198
+
199
+ if (itemHeight === -1) {
200
+ return [
201
+ <>
202
+ <Box
203
+ ref={(el) => {
204
+ if (el) setItemHeight(el.offsetHeight)
205
+ }}
206
+ insetTop={0}
207
+ insetLeft={0}
208
+ insetRight={0}
209
+ key={0}
210
+ position="absolute"
211
+ >
212
+ {renderItem(items[0])}
213
+ </Box>
214
+ <div
215
+ ref={(el) => {
216
+ if (el) setGapValue(el.offsetHeight)
217
+ }}
218
+ style={{height: vars.space[gap]}}
219
+ />
220
+ </>,
221
+ ]
222
+ }
223
+
224
+ return items.slice(fromIndex, toIndex).map((item, _itemIndex) => {
225
+ const itemIndex = fromIndex + _itemIndex
226
+ const node = renderItem(item)
227
+ const key = getItemKey ? getItemKey(item, itemIndex) : itemIndex
228
+
229
+ return (
230
+ <Box
231
+ insetLeft={0}
232
+ insetRight={0}
233
+ key={key}
234
+ position="absolute"
235
+ style={{top: itemIndex * (itemHeight + gapValue)}}
236
+ >
237
+ {node}
238
+ </Box>
239
+ )
240
+ })
241
+ }
242
+
201
243
  function findScrollable(parentNode: ParentNode | null) {
202
244
  let _scrollEl = parentNode
203
245
 
@@ -2,16 +2,23 @@ import {useEffect} from 'react'
2
2
  import {useEffectEvent} from 'use-effect-event'
3
3
 
4
4
  /**
5
+ * Adds global keydown event listener to the window.
6
+ *
7
+ * @param onKeyDown - The function to call when a keydown event is triggered.
8
+ * @param options - The options to pass to the addEventListener function (example, capture: true)
5
9
  * @beta
6
10
  */
7
- export function useGlobalKeyDown(onKeyDown: (event: KeyboardEvent) => void): void {
11
+ export function useGlobalKeyDown(
12
+ onKeyDown: (event: KeyboardEvent) => void,
13
+ options?: AddEventListenerOptions,
14
+ ): void {
8
15
  const handleKeyDown = useEffectEvent((event: KeyboardEvent) => onKeyDown(event))
9
16
 
10
17
  useEffect(() => {
11
18
  const handler = (event: KeyboardEvent) => handleKeyDown(event)
12
19
 
13
- window.addEventListener('keydown', handler)
20
+ window.addEventListener('keydown', handler, options)
14
21
 
15
- return () => window.removeEventListener('keydown', handler)
16
- }, [])
22
+ return () => window.removeEventListener('keydown', handler, options)
23
+ }, [options])
17
24
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * As `@sanity/ui` is declared as a dependency, and may be duplicated, sometimes across major versions
3
3
  * it's critical that vital react contexts are shared even when there is a duplicate.
4
- * If we used a model similar to `sanity` itself, or `styled-components`, this would be unnecessary as
4
+ * If we used a model similar to `sanity` itself this would be unnecessary as
5
5
  * those libraries enforce single instances.
6
6
  * Since we don't enforce it we have to support a sanity plugin being able to call hooks like `useToast`, and then
7
7
  * read the context setup by `sanity`, which calls `ToastProvider`, even if the provider and hook are different instances in memory.
@@ -16,7 +16,7 @@ export function createGlobalScopedContext<ContextType, const T extends ContextTy
16
16
  /**
17
17
  * Enforce that all Symbol.for keys used for globally scoped contexts have a predictable prefix
18
18
  */
19
- key: `@sanity/ui/v3/${string}`,
19
+ key: `@sanity/ui/v4/${string}`,
20
20
  defaultValue: T,
21
21
  ): Context<ContextType> {
22
22
  const symbol = Symbol.for(key)
@@ -79,6 +79,7 @@ export function Avatar<E extends AvatarElementType = typeof DEFAULT_AVATAR_ELEME
79
79
  }, [arrowPosition, arrowPositionProp])
80
80
 
81
81
  useEffect(() => {
82
+ // eslint-disable-next-line react-hooks/set-state-in-effect
82
83
  if (src) setImageError(false)
83
84
  }, [src])
84
85
 
@@ -63,7 +63,7 @@ export function Button<E extends ButtonElementType = typeof DEFAULT_BUTTON_ELEME
63
63
  disabled,
64
64
  flex,
65
65
  fontSize = 1,
66
- gap = props.padding ?? 3,
66
+ gap: _gap,
67
67
  gapX,
68
68
  gapY,
69
69
  icon: IconComponent,
@@ -90,6 +90,7 @@ export function Button<E extends ButtonElementType = typeof DEFAULT_BUTTON_ELEME
90
90
  width,
91
91
  ...rest
92
92
  } = props as ButtonProps<typeof DEFAULT_BUTTON_ELEMENT>
93
+ const gap = _gap ?? props.padding ?? 3
93
94
 
94
95
  let href: string | undefined = undefined
95
96
 
@@ -1,36 +1,21 @@
1
1
  import {UploadIcon} from '@sanity/icons'
2
2
  import {Button, Flex} from '@sanity/ui'
3
- import {styled} from 'styled-components'
4
3
 
5
- const SanityUploadButton = styled(Button).attrs({forwardedAs: 'label'})`
6
- & input {
7
- appearance: none;
8
- overflow: hidden;
9
- overflow: clip;
10
- top: 0;
11
- left: 0;
12
- width: 100%;
13
- height: 100%;
14
- opacity: 0;
15
- position: absolute;
16
- max-width: 0;
17
- width: -webkit-fill-available;
18
- width: stretch;
19
- }
20
-
21
- & > span:nth-child(2) {
22
- width: 0;
23
- flex: none;
24
- padding: 0;
25
- }
26
- `
4
+ import {sanityUploadButton} from './styles.css'
27
5
 
28
6
  export default function SanityUploadButtonWorkaroundStory(): React.JSX.Element {
29
7
  return (
30
8
  <Flex align="center" height="fill" justify="center">
31
- <SanityUploadButton htmlFor="file" icon={UploadIcon} tabIndex={0} text="Upload">
9
+ <Button
10
+ className={sanityUploadButton}
11
+ as="label"
12
+ htmlFor="file"
13
+ icon={UploadIcon}
14
+ tabIndex={0}
15
+ text="Upload"
16
+ >
32
17
  <input type="file" />
33
- </SanityUploadButton>
18
+ </Button>
34
19
  </Flex>
35
20
  )
36
21
  }
@@ -1,17 +1,11 @@
1
1
  import {Button, Flex} from '@sanity/ui'
2
- import {styled} from 'styled-components'
3
2
 
4
- const StyledButton1 = styled.a`
5
- &:hover {
6
- background-color: red;
7
- box-shadow: none;
8
- }
9
- `
3
+ import {styledButton1} from './styles.css'
10
4
 
11
5
  export default function StyledButton1Story(): React.JSX.Element {
12
6
  return (
13
7
  <Flex align="center" height="fill" justify="center">
14
- <Button as={StyledButton1} href="#" text="Test" />
8
+ <Button className={styledButton1} as="a" href="#" text="Test" />
15
9
  </Flex>
16
10
  )
17
11
  }
@@ -15,11 +15,6 @@ const scope: WorkshopScope = {
15
15
  title: 'Styled #1',
16
16
  component: lazy(() => import('./Styled1')),
17
17
  },
18
- {
19
- name: 'styled-2',
20
- title: 'Styled #2',
21
- component: lazy(() => import('./Styled2')),
22
- },
23
18
  {
24
19
  name: 'stacked',
25
20
  title: 'Stacked',
@@ -0,0 +1,30 @@
1
+ import {globalStyle, style} from '@vanilla-extract/css'
2
+
3
+ export const sanityUploadButton: string = style({})
4
+
5
+ globalStyle(`${sanityUploadButton} input`, {
6
+ appearance: 'none',
7
+ overflow: ['hidden', 'clip'],
8
+ top: 0,
9
+ left: 0,
10
+ height: '100%',
11
+ opacity: 0,
12
+ position: 'absolute',
13
+ maxWidth: 0,
14
+ width: ['100%', '-moz-available', '-webkit-fill-available', 'stretch'],
15
+ })
16
+
17
+ globalStyle(`${sanityUploadButton} > span:nth-child(2)`, {
18
+ width: 0,
19
+ flex: 'none',
20
+ padding: 0,
21
+ })
22
+
23
+ export const styledButton1: string = style({
24
+ selectors: {
25
+ '&:hover': {
26
+ backgroundColor: 'red',
27
+ boxShadow: 'none',
28
+ },
29
+ },
30
+ })
@@ -7,4 +7,4 @@ import type {CardContextValue} from './types'
7
7
  * @internal
8
8
  */
9
9
  export const CardContext: Context<CardContextValue | null> =
10
- createGlobalScopedContext<CardContextValue | null>('@sanity/ui/v3/card', null)
10
+ createGlobalScopedContext<CardContextValue | null>('@sanity/ui/v4/card', null)
@@ -1,14 +1,11 @@
1
1
  import {Card, Flex, Text} from '@sanity/ui'
2
- import {styled} from 'styled-components'
3
-
4
- const StyledCard = styled(Card).attrs({forwardedAs: 'ol'})``
5
2
 
6
3
  export default function StyledCardStory(): React.JSX.Element {
7
4
  return (
8
5
  <Flex align="center" height="fill" justify="center">
9
- <StyledCard>
6
+ <Card as="ol">
10
7
  <Text as="li">Styled</Text>
11
- </StyledCard>
8
+ </Card>
12
9
  </Flex>
13
10
  )
14
11
  }
@@ -1,17 +1,9 @@
1
1
  import {Card, Code, Flex} from '@sanity/ui'
2
2
  import {useSelect} from '@sanity/ui-workshop'
3
- import {styled} from 'styled-components'
4
3
 
5
4
  import {WORKSHOP_FLEX_DIRECTION_OPTIONS} from '$workshop'
6
5
 
7
- const DebugCard = styled(Card)`
8
- outline: 1px solid red;
9
- &:not([hidden]) {
10
- display: flex;
11
- }
12
- align-items: center;
13
- justify-content: center;
14
- `
6
+ import {debugCard} from './styles.css'
15
7
 
16
8
  export default function ExampleStory(): React.JSX.Element {
17
9
  // @ts-expect-error - TODO: fix this
@@ -24,17 +16,17 @@ export default function ExampleStory(): React.JSX.Element {
24
16
  height="fill"
25
17
  style={{width: '100%'}}
26
18
  >
27
- <DebugCard flex={1}>
19
+ <Card className={debugCard} flex={1}>
28
20
  <Code size={1}>1</Code>
29
- </DebugCard>
21
+ </Card>
30
22
 
31
- <DebugCard flex={[1, 2, 3]}>
23
+ <Card className={debugCard} flex={[1, 2, 3]}>
32
24
  <Code size={1}>[1,2,3]</Code>
33
- </DebugCard>
25
+ </Card>
34
26
 
35
- <DebugCard flex={['none', 'none', 1]}>
27
+ <Card className={debugCard} flex={['none', 'none', 1]}>
36
28
  <Code size={1}>['none', 'none', 1]</Code>
37
- </DebugCard>
29
+ </Card>
38
30
  </Flex>
39
31
  )
40
32
  }
@@ -0,0 +1,12 @@
1
+ import {style} from '@vanilla-extract/css'
2
+
3
+ export const debugCard: string = style({
4
+ outline: '1px solid red',
5
+ alignItems: 'center',
6
+ justifyContent: 'center',
7
+ selectors: {
8
+ '&:not([hidden])': {
9
+ display: 'flex',
10
+ },
11
+ },
12
+ })
@@ -22,6 +22,7 @@ export type InlineOwnProps = Omit<
22
22
  | 'gridRow'
23
23
  | 'gridTemplateColumns'
24
24
  | 'gridTemplateRows'
25
+ // eslint-disable-next-line no-warning-comments
25
26
  // todo: omit deprecated flex and grid props
26
27
  >
27
28
 
@@ -4,4 +4,4 @@ import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
4
4
  import type {LayerContextValue} from './types'
5
5
 
6
6
  export const LayerContext: Context<LayerContextValue | null> =
7
- createGlobalScopedContext<LayerContextValue | null>('@sanity/ui/v3/layer', null)
7
+ createGlobalScopedContext<LayerContextValue | null>('@sanity/ui/v4/layer', null)