@sanity/ui 3.1.14 → 3.3.0

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 (78) hide show
  1. package/dist/_chunks-cjs/_visual-editing.js +323 -292
  2. package/dist/_chunks-cjs/_visual-editing.js.map +1 -1
  3. package/dist/_chunks-es/_visual-editing.mjs +323 -292
  4. package/dist/_chunks-es/_visual-editing.mjs.map +1 -1
  5. package/dist/_visual-editing.d.mts +195 -76
  6. package/dist/_visual-editing.d.ts +195 -76
  7. package/dist/index.d.mts +308 -140
  8. package/dist/index.d.ts +308 -140
  9. package/dist/index.js +97 -93
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +97 -93
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/theme.d.mts +10 -4
  14. package/dist/theme.d.ts +10 -4
  15. package/package.json +1 -1
  16. package/src/core/components/autocomplete/autocomplete.tsx +1 -1
  17. package/src/core/components/autocomplete/types.ts +2 -2
  18. package/src/core/components/breadcrumbs/breadcrumbs.test.tsx +51 -0
  19. package/src/core/components/breadcrumbs/breadcrumbs.tsx +7 -3
  20. package/src/core/components/hotkeys/hotkeys.test.tsx +42 -0
  21. package/src/core/components/hotkeys/hotkeys.tsx +7 -3
  22. package/src/core/components/menu/__workshop__/asComponent.tsx +1 -1
  23. package/src/core/components/menu/menu.spacing.test.tsx +63 -0
  24. package/src/core/components/menu/menu.tsx +8 -2
  25. package/src/core/components/menu/menuDivider.ts +16 -5
  26. package/src/core/components/menu/menuGroup.spacing.test.tsx +63 -0
  27. package/src/core/components/menu/menuGroup.tsx +22 -8
  28. package/src/core/components/menu/menuItem.spacing.test.tsx +59 -0
  29. package/src/core/components/menu/menuItem.tsx +25 -8
  30. package/src/core/components/skeleton/skeleton.tsx +2 -2
  31. package/src/core/components/tab/tabList.test.tsx +48 -0
  32. package/src/core/components/tab/tabList.tsx +2 -2
  33. package/src/core/components/tab/tabPanel.tsx +2 -2
  34. package/src/core/components/toast/styles.ts +2 -2
  35. package/src/core/components/tree/__workshop__/basic.perf.ts +1 -1
  36. package/src/core/components/tree/__workshop__/tabFromElement.tsx +1 -1
  37. package/src/core/components/tree/tree.test.tsx +55 -0
  38. package/src/core/components/tree/tree.tsx +19 -10
  39. package/src/core/components/tree/treeGroup.tsx +3 -3
  40. package/src/core/components/tree/treeItem.test.tsx +65 -0
  41. package/src/core/components/tree/treeItem.tsx +11 -4
  42. package/src/core/components/tree/types.ts +4 -0
  43. package/src/core/primitives/avatar/avatar.tsx +19 -11
  44. package/src/core/primitives/badge/badge.tsx +20 -12
  45. package/src/core/primitives/box/__workshop__/asComponent.tsx +31 -0
  46. package/src/core/primitives/box/__workshop__/index.ts +5 -0
  47. package/src/core/primitives/box/box.test.tsx +142 -0
  48. package/src/core/primitives/box/box.tsx +41 -18
  49. package/src/core/primitives/button/__workshop__/disabled.tsx +2 -2
  50. package/src/core/primitives/button/__workshop__/sanityUploadButton.tsx +1 -1
  51. package/src/core/primitives/button/__workshop__/uploadButton.tsx +1 -1
  52. package/src/core/primitives/button/button.test.tsx +41 -3
  53. package/src/core/primitives/button/button.tsx +36 -12
  54. package/src/core/primitives/card/__workshop__/asComponent.tsx +1 -1
  55. package/src/core/primitives/card/card.tsx +24 -13
  56. package/src/core/primitives/code/code.tsx +16 -7
  57. package/src/core/primitives/container/container.tsx +23 -10
  58. package/src/core/primitives/flex/flex.tsx +21 -11
  59. package/src/core/primitives/grid/grid.test.tsx +80 -0
  60. package/src/core/primitives/grid/grid.tsx +39 -13
  61. package/src/core/primitives/heading/heading.tsx +19 -11
  62. package/src/core/primitives/inline/inline.test.tsx +51 -0
  63. package/src/core/primitives/inline/inline.tsx +28 -12
  64. package/src/core/primitives/kbd/kbd.tsx +19 -11
  65. package/src/core/primitives/label/label.tsx +19 -11
  66. package/src/core/primitives/popover/__workshop__/SidePanelStory.tsx +1 -1
  67. package/src/core/primitives/select/select.test.tsx +52 -0
  68. package/src/core/primitives/select/select.tsx +8 -2
  69. package/src/core/primitives/stack/stack.test.tsx +57 -0
  70. package/src/core/primitives/stack/stack.tsx +27 -13
  71. package/src/core/primitives/text/text.tsx +19 -11
  72. package/src/core/primitives/textInput/textInput.test.tsx +48 -0
  73. package/src/core/primitives/textInput/textInput.tsx +9 -4
  74. package/src/core/primitives/types.ts +33 -0
  75. package/src/core/types/component.ts +32 -0
  76. package/src/core/types/index.ts +1 -0
  77. package/src/core/utils/virtualList/virtualList.tsx +2 -2
  78. package/src/theme/system/css.ts +10 -4
@@ -1,7 +1,7 @@
1
1
  import {forwardRef, useEffect, useState} from 'react'
2
2
  import {styled} from 'styled-components'
3
3
 
4
- import {Box, BoxProps, ResponsiveRadiusProps} from '../../primitives'
4
+ import {Box, BoxOwnProps, ResponsiveRadiusProps} from '../../primitives'
5
5
  import {_getArrayProp} from '../../styles'
6
6
  import {responsiveRadiusStyle, ResponsiveRadiusStyleProps} from '../../styles/internal'
7
7
  import {skeletonStyle} from './styles'
@@ -14,7 +14,7 @@ const StyledSkeleton = styled(Box)<
14
14
  * This API might change. DO NOT USE IN PRODUCTION.
15
15
  * @beta
16
16
  */
17
- export interface SkeletonProps extends ResponsiveRadiusProps, Omit<BoxProps, 'children'> {
17
+ export interface SkeletonProps extends ResponsiveRadiusProps, Omit<BoxOwnProps, 'children'> {
18
18
  animated?: boolean
19
19
  delay?: number
20
20
  }
@@ -0,0 +1,48 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {inlineSpaceStyle} from '../../primitives/inline/styles'
5
+ import {Tab} from './tab'
6
+ import {TabList} from './tabList'
7
+
8
+ jest.mock('../../primitives/inline/styles', () => {
9
+ const actual = jest.requireActual('../../primitives/inline/styles')
10
+
11
+ return {
12
+ ...actual,
13
+ inlineSpaceStyle: jest.fn(actual.inlineSpaceStyle),
14
+ }
15
+ })
16
+
17
+ describe('components/tabList', () => {
18
+ const mockedInlineSpaceStyle = jest.mocked(inlineSpaceStyle)
19
+
20
+ beforeEach(() => {
21
+ mockedInlineSpaceStyle.mockClear()
22
+ })
23
+
24
+ function renderTabList(
25
+ props: Partial<React.ComponentProps<typeof TabList>> = {},
26
+ ): ReturnType<typeof render> {
27
+ return render(
28
+ <TabList {...props}>
29
+ <Tab aria-controls="tab-panel-a" id="tab-a" label="Tab A" />
30
+ <Tab aria-controls="tab-panel-b" id="tab-b" label="Tab B" />
31
+ </TabList>,
32
+ )
33
+ }
34
+
35
+ it('should support `space` and `gap` with the same behavior', () => {
36
+ renderTabList({space: 2})
37
+ expect(mockedInlineSpaceStyle).toHaveBeenCalledWith(expect.objectContaining({$space: [2]}))
38
+
39
+ mockedInlineSpaceStyle.mockClear()
40
+ renderTabList({gap: 2})
41
+ expect(mockedInlineSpaceStyle).toHaveBeenCalledWith(expect.objectContaining({$space: [2]}))
42
+ })
43
+
44
+ it('should prefer `gap` over `space` when both are provided', () => {
45
+ renderTabList({gap: 3, space: 1})
46
+ expect(mockedInlineSpaceStyle).toHaveBeenCalledWith(expect.objectContaining({$space: [3]}))
47
+ })
48
+ })
@@ -1,12 +1,12 @@
1
1
  import {Children, cloneElement, forwardRef, isValidElement, useCallback, useState} from 'react'
2
2
  import {styled} from 'styled-components'
3
3
 
4
- import {Inline, InlineProps} from '../../primitives'
4
+ import {Inline, InlineOwnProps} from '../../primitives'
5
5
 
6
6
  /**
7
7
  * @public
8
8
  */
9
- export interface TabListProps extends Omit<InlineProps, 'as' | 'height'> {
9
+ export interface TabListProps extends Omit<InlineOwnProps, 'height'> {
10
10
  children: Array<React.JSX.Element | null | undefined | false>
11
11
  }
12
12
 
@@ -1,11 +1,11 @@
1
1
  import {forwardRef} from 'react'
2
2
 
3
- import {Box, BoxProps} from '../../primitives'
3
+ import {Box, BoxOwnProps} from '../../primitives'
4
4
 
5
5
  /**
6
6
  * @public
7
7
  */
8
- export interface TabPanelProps extends BoxProps {
8
+ export interface TabPanelProps extends BoxOwnProps {
9
9
  /**
10
10
  * The `id` of the correlating `Tab` component.
11
11
  */
@@ -1,7 +1,7 @@
1
1
  import {getTheme_v2, ThemeColorStateToneKey} from '@sanity/ui/theme'
2
2
  import {styled} from 'styled-components'
3
3
 
4
- import {Card, Flex} from '../../primitives'
4
+ import {Card, type CardProps, Flex} from '../../primitives'
5
5
  import type {ButtonTone} from '../../types'
6
6
 
7
7
  const LOADING_BAR_HEIGHT = 2
@@ -61,7 +61,7 @@ export const LoadingBarMask = styled(Card)`
61
61
  z-index: 1;
62
62
  `
63
63
 
64
- type LoadingBarProgressProps = Omit<React.ComponentProps<typeof Card>, 'tone'> & {
64
+ type LoadingBarProgressProps = Omit<CardProps, 'tone'> & {
65
65
  tone: ThemeColorStateToneKey
66
66
  }
67
67
  export const LoadingBarProgress = styled<React.ComponentType<LoadingBarProgressProps>>(Card)`
@@ -9,7 +9,7 @@ function test<ElementType = unknown>(
9
9
  }
10
10
 
11
11
  export const perfTests = [
12
- test('Toggle tree groups', async ({target}: {target: HTMLDivElement}) => {
12
+ test('Toggle tree groups', async ({target}: {target: HTMLUListElement}) => {
13
13
  const apples = await findByTestId(target, 'apples')
14
14
  const oranges = await findByTestId(target, 'oranges')
15
15
 
@@ -19,7 +19,7 @@ export default function BasicStory() {
19
19
  if (testid) setId(testid)
20
20
  }, [])
21
21
 
22
- const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
22
+ const handleFocus = useCallback((event: React.FocusEvent<HTMLElement>) => {
23
23
  const elementFocus = event.target.getAttribute('data-testid')
24
24
  if (elementFocus) setFocus(elementFocus)
25
25
  }, [])
@@ -0,0 +1,55 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Stack} from '../../primitives'
5
+ import {Tree} from './tree'
6
+ import {TreeItem} from './treeItem'
7
+
8
+ jest.mock('../../primitives', () => {
9
+ const actual = jest.requireActual('../../primitives')
10
+
11
+ return {
12
+ ...actual,
13
+ Stack: jest.fn((props: Record<string, unknown>) => (actual.Stack as any).render(props, null)),
14
+ }
15
+ })
16
+
17
+ describe('components/tree spacing', () => {
18
+ const mockedStack = jest.mocked(Stack)
19
+
20
+ beforeEach(() => {
21
+ mockedStack.mockClear()
22
+ })
23
+
24
+ it('should support `space` and `gap` with the same behavior', () => {
25
+ render(
26
+ <Tree space={2}>
27
+ <TreeItem text="Item 1" />
28
+ </Tree>,
29
+ )
30
+ expect(mockedStack.mock.calls.map(([props]) => props)).toContainEqual(
31
+ expect.objectContaining({gap: 2}),
32
+ )
33
+
34
+ mockedStack.mockClear()
35
+ render(
36
+ <Tree gap={2}>
37
+ <TreeItem text="Item 1" />
38
+ </Tree>,
39
+ )
40
+ expect(mockedStack.mock.calls.map(([props]) => props)).toContainEqual(
41
+ expect.objectContaining({gap: 2}),
42
+ )
43
+ })
44
+
45
+ it('should prefer `gap` over `space` when both are provided', () => {
46
+ render(
47
+ <Tree gap={3} space={1}>
48
+ <TreeItem text="Item 1" />
49
+ </Tree>,
50
+ )
51
+ const propsList = mockedStack.mock.calls.map(([props]) => props)
52
+ expect(propsList).toContainEqual(expect.objectContaining({gap: 3}))
53
+ expect(propsList).not.toContainEqual(expect.objectContaining({gap: 1}))
54
+ })
55
+ })
@@ -18,6 +18,10 @@ import {TreeContextValue, TreeState} from './types'
18
18
  * @beta
19
19
  */
20
20
  export interface TreeProps {
21
+ gap?: number | number[]
22
+ /**
23
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
24
+ */
21
25
  space?: number | number[]
22
26
  }
23
27
 
@@ -27,11 +31,12 @@ export interface TreeProps {
27
31
  */
28
32
  export const Tree = forwardRef(function Tree(
29
33
  props: TreeProps &
30
- Omit<React.HTMLProps<HTMLDivElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
31
- forwardedRef: React.ForwardedRef<HTMLDivElement>,
34
+ Omit<React.HTMLProps<HTMLUListElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
35
+ forwardedRef: React.ForwardedRef<HTMLUListElement>,
32
36
  ): React.JSX.Element {
33
- const {children, space = 1, onFocus, ...restProps} = props
34
- const ref = useRef<HTMLDivElement | null>(null)
37
+ const {children, gap, space: deprecated_space = 1, onFocus, ...restProps} = props
38
+ const spacing = gap === undefined ? deprecated_space : gap
39
+ const ref = useRef<HTMLUListElement | null>(null)
35
40
  const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(null)
36
41
  const focusedElementRef = useRef(focusedElement)
37
42
  const path: string[] = useMemo(() => [], [])
@@ -39,7 +44,10 @@ export const Tree = forwardRef(function Tree(
39
44
  const [state, setState] = useState<TreeState>({})
40
45
  const stateRef = useRef(state)
41
46
 
42
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
47
+ useImperativeHandle<HTMLUListElement | null, HTMLUListElement | null>(
48
+ forwardedRef,
49
+ () => ref.current,
50
+ )
43
51
 
44
52
  useEffect(() => {
45
53
  focusedElementRef.current = focusedElement
@@ -89,14 +97,15 @@ export const Tree = forwardRef(function Tree(
89
97
  registerItem,
90
98
  setExpanded,
91
99
  setFocusedElement,
92
- space,
100
+ gap: spacing,
101
+ space: spacing,
93
102
  state,
94
103
  }),
95
- [focusedElement, itemElements, path, registerItem, setExpanded, space, state],
104
+ [focusedElement, itemElements, path, registerItem, setExpanded, spacing, state],
96
105
  )
97
106
 
98
107
  const handleKeyDown = useCallback(
99
- (event: React.KeyboardEvent<HTMLDivElement>) => {
108
+ (event: React.KeyboardEvent<HTMLUListElement>) => {
100
109
  if (!focusedElementRef.current) return
101
110
 
102
111
  if (event.key === 'ArrowDown') {
@@ -193,7 +202,7 @@ export const Tree = forwardRef(function Tree(
193
202
  )
194
203
 
195
204
  const handleFocus = useCallback(
196
- (event: React.FocusEvent<HTMLDivElement>) => {
205
+ (event: React.FocusEvent<HTMLUListElement>) => {
197
206
  setFocusedElement(event.target)
198
207
 
199
208
  // Call the element's `focus` handler
@@ -221,7 +230,7 @@ export const Tree = forwardRef(function Tree(
221
230
  onKeyDown={handleKeyDown}
222
231
  ref={ref}
223
232
  role="tree"
224
- space={space}
233
+ gap={spacing}
225
234
  >
226
235
  {children}
227
236
  </Stack>
@@ -7,7 +7,7 @@ export interface TreeGroupProps {
7
7
 
8
8
  export function TreeGroup(
9
9
  props: TreeGroupProps &
10
- Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
10
+ Omit<React.HTMLProps<HTMLUListElement>, 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
11
11
  ): React.JSX.Element {
12
12
  const {children, expanded = false, ...restProps} = props
13
13
  const tree = useTree()
@@ -18,9 +18,9 @@ export function TreeGroup(
18
18
  data-ui="TreeGroup"
19
19
  {...restProps}
20
20
  hidden={!expanded}
21
- marginTop={tree.space}
21
+ marginTop={tree.gap}
22
22
  role="group"
23
- space={tree.space}
23
+ gap={tree.gap}
24
24
  >
25
25
  {children}
26
26
  </Stack>
@@ -0,0 +1,65 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Box} from '../../primitives'
5
+ import {TreeContext} from './treeContext'
6
+ import {TreeItem} from './treeItem'
7
+ import {TreeContextValue, TreeState} from './types'
8
+
9
+ jest.mock('../../primitives', () => {
10
+ const actual = jest.requireActual('../../primitives')
11
+
12
+ return {
13
+ ...actual,
14
+ Box: jest.fn((props: Record<string, unknown>) => (actual.Box as any).render(props, null)),
15
+ }
16
+ })
17
+
18
+ const treeContextValue: TreeContextValue = {
19
+ version: 0.0,
20
+ focusedElement: null,
21
+ gap: 1,
22
+ level: 0,
23
+ path: [],
24
+ registerItem: () => () => undefined,
25
+ setExpanded: () => undefined,
26
+ setFocusedElement: () => undefined,
27
+ space: 1,
28
+ state: {} as TreeState,
29
+ }
30
+
31
+ function renderTreeItem(props: Partial<React.ComponentProps<typeof TreeItem>> = {}) {
32
+ return render(
33
+ <TreeContext.Provider value={treeContextValue}>
34
+ <TreeItem text="Item" {...props} />
35
+ </TreeContext.Provider>,
36
+ )
37
+ }
38
+
39
+ describe('components/treeItem spacing', () => {
40
+ const mockedBox = jest.mocked(Box)
41
+
42
+ beforeEach(() => {
43
+ mockedBox.mockClear()
44
+ })
45
+
46
+ it('should support `space` and `gap` with the same behavior', () => {
47
+ renderTreeItem({space: 2})
48
+ expect(mockedBox.mock.calls.map(([props]) => props)).toContainEqual(
49
+ expect.objectContaining({marginRight: 2}),
50
+ )
51
+
52
+ mockedBox.mockClear()
53
+ renderTreeItem({gap: 2})
54
+ expect(mockedBox.mock.calls.map(([props]) => props)).toContainEqual(
55
+ expect.objectContaining({marginRight: 2}),
56
+ )
57
+ })
58
+
59
+ it('should prefer `gap` over `space` when both are provided', () => {
60
+ renderTreeItem({gap: 3, space: 1})
61
+ const propsList = mockedBox.mock.calls.map(([props]) => props)
62
+ expect(propsList).toContainEqual(expect.objectContaining({marginRight: 3}))
63
+ expect(propsList).not.toContainEqual(expect.objectContaining({marginRight: 1}))
64
+ })
65
+ })
@@ -3,7 +3,8 @@ import {ThemeFontWeightKey} from '@sanity/ui/theme'
3
3
  import {startTransition, useCallback, useEffect, useId, useMemo, useRef, useState} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
 
6
- import {Box, BoxProps, Flex, Text} from '../../primitives'
6
+ import {Box, Flex, Text} from '../../primitives'
7
+ import {ElementType} from '../../types'
7
8
  import {
8
9
  treeItemBoxStyle,
9
10
  TreeItemBoxStyleProps,
@@ -24,8 +25,12 @@ export interface TreeItemProps {
24
25
  /**
25
26
  * Allows passing a custom element type to the link component
26
27
  */
27
- linkAs?: BoxProps['as']
28
+ linkAs?: ElementType
28
29
  padding?: number | number[]
30
+ gap?: number | number[]
31
+ /**
32
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
33
+ */
29
34
  space?: number | number[]
30
35
  text?: React.ReactNode
31
36
  weight?: ThemeFontWeightKey
@@ -60,11 +65,13 @@ export function TreeItem(
60
65
  onClick,
61
66
  padding = 2,
62
67
  selected = false,
63
- space = 2,
68
+ gap,
69
+ space: deprecated_space = 2,
64
70
  text,
65
71
  weight,
66
72
  ...restProps
67
73
  } = props
74
+ const spacing = gap === undefined ? deprecated_space : gap
68
75
  const [rootElement, _setRootElement] = useState<HTMLLIElement | null>(null)
69
76
  /**
70
77
  * The startTransition wrapper here is to avoid an issue when on React 18 where this error can happen:
@@ -133,7 +140,7 @@ export function TreeItem(
133
140
  const content = (
134
141
  <Flex padding={padding}>
135
142
  <Box
136
- marginRight={space}
143
+ marginRight={spacing}
137
144
  style={{
138
145
  visibility: IconComponent || children ? 'visible' : 'hidden',
139
146
  pointerEvents: 'none',
@@ -16,6 +16,10 @@ export interface TreeContextValue {
16
16
  registerItem: (element: HTMLElement, path: string, expanded: boolean, selected: boolean) => void
17
17
  setExpanded: (path: string, expanded: boolean) => void
18
18
  setFocusedElement: (focusedElement: HTMLElement | null) => void
19
+ gap: number | number[]
20
+ /**
21
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
22
+ */
19
23
  space: number | number[]
20
24
  state: TreeState
21
25
  }
@@ -5,19 +5,18 @@ import {styled} from 'styled-components'
5
5
 
6
6
  import {_getArrayProp} from '../../styles'
7
7
  import {useTheme_v2} from '../../theme'
8
- import {AvatarPosition, AvatarSize, AvatarStatus} from '../../types'
8
+ import {AvatarPosition, AvatarSize, AvatarStatus, ElementType, Props} from '../../types'
9
9
  import {Label} from '../label'
10
10
  import {avatarStyle, responsiveAvatarSizeStyle} from './styles'
11
11
 
12
12
  /**
13
13
  * @public
14
14
  */
15
- export interface AvatarProps {
15
+ export interface AvatarOwnProps {
16
16
  /** @beta */
17
17
  __unstable_hideInnerStroke?: boolean
18
18
  animateArrowFrom?: AvatarPosition
19
19
  arrowPosition?: AvatarPosition
20
- as?: React.ElementType | keyof React.JSX.IntrinsicElements
21
20
  color?: ThemeColorAvatarColorKey
22
21
  initials?: string
23
22
  onImageLoadError?: (event: Error) => void
@@ -31,6 +30,11 @@ export interface AvatarProps {
31
30
  title?: string
32
31
  }
33
32
 
33
+ /**
34
+ * @public
35
+ */
36
+ export type AvatarProps<E extends ElementType = 'div'> = Props<AvatarOwnProps, E>
37
+
34
38
  const StyledAvatar = styled.div<{$color: ThemeColorAvatarColorKey; $size: AvatarSize[]}>(
35
39
  responsiveAvatarSizeStyle,
36
40
  avatarStyle.root,
@@ -50,13 +54,8 @@ const InitialsLabel = styled(Label)({
50
54
 
51
55
  const AvatarImage = styled.svg(avatarStyle.image)
52
56
 
53
- /**
54
- * Avatars are used to represent people and other agents (e.g. bots).
55
- *
56
- * @public
57
- */
58
- export const Avatar = forwardRef(function Avatar(
59
- props: AvatarProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'ref'>,
57
+ const AvatarComponent = forwardRef(function Avatar(
58
+ props: AvatarOwnProps & {as?: ElementType} & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'ref'>,
60
59
  ref: React.ForwardedRef<HTMLDivElement>,
61
60
  ) {
62
61
  const {
@@ -189,4 +188,13 @@ export const Avatar = forwardRef(function Avatar(
189
188
  </StyledAvatar>
190
189
  )
191
190
  })
192
- Avatar.displayName = 'ForwardRef(Avatar)'
191
+ AvatarComponent.displayName = 'ForwardRef(Avatar)'
192
+
193
+ /**
194
+ * Avatars are used to represent people and other agents (e.g. bots).
195
+ *
196
+ * @public
197
+ */
198
+ export const Avatar = AvatarComponent as unknown as <E extends ElementType = 'div'>(
199
+ props: AvatarProps<E>,
200
+ ) => React.JSX.Element
@@ -3,8 +3,8 @@ import {styled} from 'styled-components'
3
3
 
4
4
  import {_getArrayProp} from '../../styles'
5
5
  import {responsiveRadiusStyle, ResponsiveRadiusStyleProps} from '../../styles/internal'
6
- import {BadgeMode, BadgeTone} from '../../types'
7
- import {Box, BoxProps} from '../box'
6
+ import {BadgeMode, BadgeTone, ElementType, Props} from '../../types'
7
+ import {Box, BoxOwnProps} from '../box'
8
8
  import {Text} from '../text'
9
9
  import {ResponsiveRadiusProps} from '../types'
10
10
  import {badgeStyle} from './styles'
@@ -13,26 +13,25 @@ import {BadgeStyleProps} from './types'
13
13
  /**
14
14
  * @public
15
15
  */
16
- export interface BadgeProps extends BoxProps, ResponsiveRadiusProps {
17
- as?: React.ElementType | keyof React.JSX.IntrinsicElements
16
+ export interface BadgeOwnProps extends BoxOwnProps, ResponsiveRadiusProps {
18
17
  fontSize?: number | number[]
19
18
  /** @deprecated No longer used. */
20
19
  mode?: BadgeMode
21
20
  tone?: BadgeTone
22
21
  }
23
22
 
23
+ /**
24
+ * @public
25
+ */
26
+ export type BadgeProps<E extends ElementType = 'div'> = Props<BadgeOwnProps, E>
27
+
24
28
  const StyledBadge = styled(Box)<BadgeStyleProps & ResponsiveRadiusStyleProps>(
25
29
  responsiveRadiusStyle,
26
30
  badgeStyle,
27
31
  )
28
32
 
29
- /**
30
- * Badges are used to tag resources.
31
- *
32
- * @public
33
- */
34
- export const Badge = forwardRef(function Badge(
35
- props: BadgeProps & React.HTMLProps<HTMLDivElement>,
33
+ const BadgeComponent = forwardRef(function Badge(
34
+ props: BadgeOwnProps & {as?: ElementType} & React.HTMLProps<HTMLDivElement>,
36
35
  ref,
37
36
  ) {
38
37
  const {
@@ -59,4 +58,13 @@ export const Badge = forwardRef(function Badge(
59
58
  </StyledBadge>
60
59
  )
61
60
  })
62
- Badge.displayName = 'ForwardRef(Badge)'
61
+ BadgeComponent.displayName = 'ForwardRef(Badge)'
62
+
63
+ /**
64
+ * Badges are used to tag resources.
65
+ *
66
+ * @public
67
+ */
68
+ export const Badge = BadgeComponent as unknown as <E extends ElementType = 'div'>(
69
+ props: BadgeProps<E>,
70
+ ) => React.JSX.Element
@@ -0,0 +1,31 @@
1
+ import {Box, Flex, Text} from '@sanity/ui'
2
+ import {forwardRef} from 'react'
3
+
4
+ // A custom component with its own props. When passed to `Box`'s `as` prop, these props are
5
+ // inferred on `Box` itself: `href` is required and `target` is optional, and unknown props
6
+ // are rejected by the type checker.
7
+ const Link = forwardRef(function Link(
8
+ props: {href: string; target?: string} & Omit<
9
+ React.HTMLProps<HTMLAnchorElement>,
10
+ 'as' | 'href' | 'target'
11
+ >,
12
+ ref: React.ForwardedRef<HTMLAnchorElement>,
13
+ ): React.JSX.Element {
14
+ const {children, ...restProps} = props
15
+
16
+ return (
17
+ <a {...restProps} ref={ref}>
18
+ {children}
19
+ </a>
20
+ )
21
+ })
22
+
23
+ export default function AsComponentStory() {
24
+ return (
25
+ <Flex align="center" height="fill" justify="center">
26
+ <Box as={Link} href="/" padding={4} target="_new">
27
+ <Text size={1}>As component</Text>
28
+ </Box>
29
+ </Flex>
30
+ )
31
+ }
@@ -15,5 +15,10 @@ export default defineScope({
15
15
  title: 'Responsive',
16
16
  component: lazy(() => import('./responsive')),
17
17
  },
18
+ {
19
+ name: 'as-component',
20
+ title: 'As component',
21
+ component: lazy(() => import('./asComponent')),
22
+ },
18
23
  ],
19
24
  })