@sanity/ui 3.2.0 → 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 (59) hide show
  1. package/dist/_chunks-cjs/_visual-editing.js +54 -43
  2. package/dist/_chunks-cjs/_visual-editing.js.map +1 -1
  3. package/dist/_chunks-es/_visual-editing.mjs +54 -43
  4. package/dist/_chunks-es/_visual-editing.mjs.map +1 -1
  5. package/dist/_visual-editing.d.mts +129 -75
  6. package/dist/_visual-editing.d.ts +129 -75
  7. package/dist/index.d.mts +222 -139
  8. package/dist/index.d.ts +222 -139
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/theme.d.mts +10 -4
  12. package/dist/theme.d.ts +10 -4
  13. package/package.json +1 -1
  14. package/src/core/components/autocomplete/autocomplete.tsx +1 -1
  15. package/src/core/components/autocomplete/types.ts +2 -2
  16. package/src/core/components/menu/__workshop__/asComponent.tsx +1 -1
  17. package/src/core/components/menu/menuDivider.ts +16 -5
  18. package/src/core/components/menu/menuGroup.spacing.test.tsx +1 -7
  19. package/src/core/components/menu/menuGroup.tsx +14 -6
  20. package/src/core/components/menu/menuItem.tsx +17 -6
  21. package/src/core/components/skeleton/skeleton.tsx +2 -2
  22. package/src/core/components/tab/tabList.tsx +2 -2
  23. package/src/core/components/tab/tabPanel.tsx +2 -2
  24. package/src/core/components/toast/styles.ts +2 -2
  25. package/src/core/components/tree/__workshop__/basic.perf.ts +1 -1
  26. package/src/core/components/tree/__workshop__/tabFromElement.tsx +1 -1
  27. package/src/core/components/tree/tree.tsx +9 -6
  28. package/src/core/components/tree/treeGroup.tsx +1 -1
  29. package/src/core/components/tree/treeItem.tsx +3 -2
  30. package/src/core/primitives/avatar/avatar.tsx +19 -11
  31. package/src/core/primitives/badge/badge.tsx +20 -12
  32. package/src/core/primitives/box/__workshop__/asComponent.tsx +31 -0
  33. package/src/core/primitives/box/__workshop__/index.ts +5 -0
  34. package/src/core/primitives/box/box.test.tsx +12 -4
  35. package/src/core/primitives/box/box.tsx +23 -12
  36. package/src/core/primitives/button/__workshop__/disabled.tsx +2 -2
  37. package/src/core/primitives/button/__workshop__/sanityUploadButton.tsx +1 -1
  38. package/src/core/primitives/button/__workshop__/uploadButton.tsx +1 -1
  39. package/src/core/primitives/button/button.test.tsx +3 -3
  40. package/src/core/primitives/button/button.tsx +28 -9
  41. package/src/core/primitives/card/__workshop__/asComponent.tsx +1 -1
  42. package/src/core/primitives/card/card.tsx +24 -13
  43. package/src/core/primitives/code/code.tsx +16 -7
  44. package/src/core/primitives/container/container.tsx +23 -10
  45. package/src/core/primitives/flex/flex.tsx +21 -11
  46. package/src/core/primitives/grid/grid.test.tsx +3 -1
  47. package/src/core/primitives/grid/grid.tsx +22 -9
  48. package/src/core/primitives/heading/heading.tsx +19 -11
  49. package/src/core/primitives/inline/inline.tsx +19 -9
  50. package/src/core/primitives/kbd/kbd.tsx +19 -11
  51. package/src/core/primitives/label/label.tsx +19 -11
  52. package/src/core/primitives/popover/__workshop__/SidePanelStory.tsx +1 -1
  53. package/src/core/primitives/stack/stack.tsx +20 -11
  54. package/src/core/primitives/text/text.tsx +19 -11
  55. package/src/core/primitives/textInput/textInput.tsx +2 -2
  56. package/src/core/types/component.ts +32 -0
  57. package/src/core/types/index.ts +1 -0
  58. package/src/core/utils/virtualList/virtualList.tsx +2 -2
  59. package/src/theme/system/css.ts +10 -4
package/dist/theme.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {ColorHueKey} from '@sanity/color'
2
2
  import {ColorTint} from '@sanity/color'
3
3
  import {ColorTintKey} from '@sanity/color'
4
- import type {StyledObject} from 'styled-components'
4
+ import type * as CSS_2 from 'csstype'
5
5
 
6
6
  /**
7
7
  * @public
@@ -229,11 +229,17 @@ export declare function createColorTheme(
229
229
  ): ThemeColorSchemes
230
230
 
231
231
  /**
232
- * Work around types that are missing in `styled-components@6`
233
- * https://github.com/styled-components/styled-components/issues/4062
232
+ * A CSS-in-JS style object: standard CSS properties plus an index signature that allows nested
233
+ * selectors, at-rules and custom properties.
234
+ *
235
+ * This is a self-owned definition (based on `csstype`) so the published `.d.ts` types stay fully
236
+ * controlled by us and do not reference any external CSS-in-JS library types.
237
+ *
234
238
  * @internal
235
239
  */
236
- export declare type CSSObject = StyledObject<any>
240
+ export declare interface CSSObject extends CSS_2.Properties<number | (string & {})> {
241
+ [key: string]: CSSObject | string | number | undefined
242
+ }
237
243
 
238
244
  /** @internal */
239
245
  export declare function getContrastRatio(bg: string, fg: string): number
package/dist/theme.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {ColorHueKey} from '@sanity/color'
2
2
  import {ColorTint} from '@sanity/color'
3
3
  import {ColorTintKey} from '@sanity/color'
4
- import type {StyledObject} from 'styled-components'
4
+ import type * as CSS_2 from 'csstype'
5
5
 
6
6
  /**
7
7
  * @public
@@ -229,11 +229,17 @@ export declare function createColorTheme(
229
229
  ): ThemeColorSchemes
230
230
 
231
231
  /**
232
- * Work around types that are missing in `styled-components@6`
233
- * https://github.com/styled-components/styled-components/issues/4062
232
+ * A CSS-in-JS style object: standard CSS properties plus an index signature that allows nested
233
+ * selectors, at-rules and custom properties.
234
+ *
235
+ * This is a self-owned definition (based on `csstype`) so the published `.d.ts` types stay fully
236
+ * controlled by us and do not reference any external CSS-in-JS library types.
237
+ *
234
238
  * @internal
235
239
  */
236
- export declare type CSSObject = StyledObject<any>
240
+ export declare interface CSSObject extends CSS_2.Properties<number | (string & {})> {
241
+ [key: string]: CSSObject | string | number | undefined
242
+ }
237
243
 
238
244
  /** @internal */
239
245
  export declare function getContrastRatio(bg: string, fg: string): number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -190,7 +190,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
190
190
  const rootElementRef = useRef<HTMLDivElement | null>(null)
191
191
  const resultsPopoverElementRef = useRef<HTMLDivElement | null>(null)
192
192
  const inputElementRef = useRef<HTMLInputElement | null>(null)
193
- const listBoxElementRef = useRef<HTMLDivElement | null>(null)
193
+ const listBoxElementRef = useRef<HTMLUListElement | null>(null)
194
194
  // Element refs that need to be accessed during render
195
195
  const [inputElement, _setInputElement] = useState<HTMLInputElement | null>(null)
196
196
  /**
@@ -1,9 +1,9 @@
1
- import {ButtonProps} from '../../primitives'
1
+ import {ButtonOwnProps} from '../../primitives'
2
2
 
3
3
  /**
4
4
  * @public
5
5
  */
6
- export type AutocompleteOpenButtonProps = Omit<ButtonProps, 'as'> &
6
+ export type AutocompleteOpenButtonProps = ButtonOwnProps &
7
7
  Omit<React.HTMLProps<HTMLButtonElement>, 'as' | 'ref'>
8
8
 
9
9
  /**
@@ -16,7 +16,7 @@ const CustomLink = forwardRef(function CustomLink(
16
16
  })
17
17
 
18
18
  export default function AsComponentStory() {
19
- const props = {href: '#'}
19
+ const props = {href: '#', req: 'example'}
20
20
 
21
21
  return (
22
22
  <Flex align="center" height="fill" justify="center">
@@ -1,12 +1,23 @@
1
1
  import {styled} from 'styled-components'
2
2
 
3
- /**
4
- * @public
5
- */
6
- export const MenuDivider = styled.hr`
3
+ import {ElementType, EmptyProps, Props} from '../../types'
4
+
5
+ const StyledMenuDivider = styled.hr`
7
6
  height: 1px;
8
7
  border: 0;
9
8
  background: var(--card-hairline-soft-color);
10
9
  margin: 0;
11
10
  `
12
- MenuDivider.displayName = 'MenuDivider'
11
+ StyledMenuDivider.displayName = 'MenuDivider'
12
+
13
+ /**
14
+ * @public
15
+ */
16
+ export type MenuDividerProps<E extends ElementType = 'hr'> = Props<EmptyProps, E>
17
+
18
+ /**
19
+ * @public
20
+ */
21
+ export const MenuDivider = StyledMenuDivider as unknown as <E extends ElementType = 'hr'>(
22
+ props: MenuDividerProps<E>,
23
+ ) => React.JSX.Element
@@ -12,13 +12,7 @@ jest.mock('../../primitives', () => {
12
12
  return {
13
13
  ...actual,
14
14
  Flex: jest.fn((props: Record<string, unknown>) => (actual.Flex as any).render(props, null)),
15
- Popover: jest.fn(
16
- ({
17
- children,
18
- }: {
19
- children?: React.ReactNode
20
- }) => children,
21
- ),
15
+ Popover: jest.fn(({children}: {children?: React.ReactNode}) => children),
22
16
  }
23
17
  })
24
18
 
@@ -6,15 +6,14 @@ import {Box, Flex, Popover, PopoverProps, Text} from '../../primitives'
6
6
  import {Selectable} from '../../primitives/_selectable'
7
7
  import {_getArrayProp} from '../../styles'
8
8
  import {useRootTheme} from '../../theme'
9
- import {Radius, SelectableTone} from '../../types'
9
+ import {ElementType, Props, Radius, SelectableTone} from '../../types'
10
10
  import {Menu, MenuProps} from './menu'
11
11
  import {useMenu} from './useMenu'
12
12
 
13
13
  /**
14
14
  * @public
15
15
  */
16
- export interface MenuGroupProps {
17
- as?: React.ElementType | keyof React.JSX.IntrinsicElements
16
+ export interface MenuGroupOwnProps {
18
17
  fontSize?: number | number[]
19
18
  icon?: React.ElementType | React.ReactNode
20
19
  menu?: Omit<
@@ -43,9 +42,11 @@ export interface MenuGroupProps {
43
42
  /**
44
43
  * @public
45
44
  */
46
- export function MenuGroup(
45
+ export type MenuGroupProps<E extends ElementType = 'button'> = Props<MenuGroupOwnProps, E>
46
+
47
+ function MenuGroupComponent(
47
48
  props: Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'popover' | 'ref' | 'tabIndex'> &
48
- MenuGroupProps,
49
+ MenuGroupOwnProps & {as?: ElementType},
49
50
  ): React.JSX.Element {
50
51
  const {
51
52
  as = 'button',
@@ -222,4 +223,11 @@ export function MenuGroup(
222
223
  )
223
224
  }
224
225
 
225
- MenuGroup.displayName = 'MenuGroup'
226
+ MenuGroupComponent.displayName = 'MenuGroup'
227
+
228
+ /**
229
+ * @public
230
+ */
231
+ export const MenuGroup = MenuGroupComponent as unknown as <E extends ElementType = 'button'>(
232
+ props: MenuGroupProps<E>,
233
+ ) => React.JSX.Element
@@ -15,6 +15,7 @@ import {Selectable} from '../../primitives/_selectable'
15
15
  import {ResponsivePaddingProps, ResponsiveRadiusProps} from '../../primitives/types'
16
16
  import {_getArrayProp} from '../../styles'
17
17
  import {useRootTheme} from '../../theme'
18
+ import {ElementType, Props} from '../../types'
18
19
  import {SelectableTone} from '../../types/selectable'
19
20
  import {Hotkeys} from '../hotkeys'
20
21
  import {useMenu} from './useMenu'
@@ -22,8 +23,7 @@ import {useMenu} from './useMenu'
22
23
  /**
23
24
  * @public
24
25
  */
25
- export interface MenuItemProps extends ResponsivePaddingProps, ResponsiveRadiusProps {
26
- as?: React.ElementType | keyof React.JSX.IntrinsicElements
26
+ export interface MenuItemOwnProps extends ResponsivePaddingProps, ResponsiveRadiusProps {
27
27
  fontSize?: number | number[]
28
28
  hotkeys?: string[]
29
29
  icon?: React.ElementType | React.ReactNode
@@ -42,9 +42,13 @@ export interface MenuItemProps extends ResponsivePaddingProps, ResponsiveRadiusP
42
42
  /**
43
43
  * @public
44
44
  */
45
- export const MenuItem = forwardRef(function MenuItem(
46
- props: MenuItemProps &
47
- Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'selected' | 'tabIndex'>,
45
+ export type MenuItemProps<E extends ElementType = 'button'> = Props<MenuItemOwnProps, E>
46
+
47
+ const MenuItemComponent = forwardRef(function MenuItem(
48
+ props: MenuItemOwnProps & {as?: ElementType} & Omit<
49
+ React.HTMLProps<HTMLDivElement>,
50
+ 'as' | 'height' | 'ref' | 'selected' | 'tabIndex'
51
+ >,
48
52
  forwardedRef: React.ForwardedRef<HTMLDivElement>,
49
53
  ) {
50
54
  const {
@@ -183,4 +187,11 @@ export const MenuItem = forwardRef(function MenuItem(
183
187
  </Selectable>
184
188
  )
185
189
  })
186
- MenuItem.displayName = 'ForwardRef(MenuItem)'
190
+ MenuItemComponent.displayName = 'ForwardRef(MenuItem)'
191
+
192
+ /**
193
+ * @public
194
+ */
195
+ export const MenuItem = MenuItemComponent as unknown as <E extends ElementType = 'button'>(
196
+ props: MenuItemProps<E>,
197
+ ) => React.JSX.Element
@@ -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
  }
@@ -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
  }, [])
@@ -31,12 +31,12 @@ export interface TreeProps {
31
31
  */
32
32
  export const Tree = forwardRef(function Tree(
33
33
  props: TreeProps &
34
- Omit<React.HTMLProps<HTMLDivElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
35
- forwardedRef: React.ForwardedRef<HTMLDivElement>,
34
+ Omit<React.HTMLProps<HTMLUListElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
35
+ forwardedRef: React.ForwardedRef<HTMLUListElement>,
36
36
  ): React.JSX.Element {
37
37
  const {children, gap, space: deprecated_space = 1, onFocus, ...restProps} = props
38
38
  const spacing = gap === undefined ? deprecated_space : gap
39
- const ref = useRef<HTMLDivElement | null>(null)
39
+ const ref = useRef<HTMLUListElement | null>(null)
40
40
  const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(null)
41
41
  const focusedElementRef = useRef(focusedElement)
42
42
  const path: string[] = useMemo(() => [], [])
@@ -44,7 +44,10 @@ export const Tree = forwardRef(function Tree(
44
44
  const [state, setState] = useState<TreeState>({})
45
45
  const stateRef = useRef(state)
46
46
 
47
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
47
+ useImperativeHandle<HTMLUListElement | null, HTMLUListElement | null>(
48
+ forwardedRef,
49
+ () => ref.current,
50
+ )
48
51
 
49
52
  useEffect(() => {
50
53
  focusedElementRef.current = focusedElement
@@ -102,7 +105,7 @@ export const Tree = forwardRef(function Tree(
102
105
  )
103
106
 
104
107
  const handleKeyDown = useCallback(
105
- (event: React.KeyboardEvent<HTMLDivElement>) => {
108
+ (event: React.KeyboardEvent<HTMLUListElement>) => {
106
109
  if (!focusedElementRef.current) return
107
110
 
108
111
  if (event.key === 'ArrowDown') {
@@ -199,7 +202,7 @@ export const Tree = forwardRef(function Tree(
199
202
  )
200
203
 
201
204
  const handleFocus = useCallback(
202
- (event: React.FocusEvent<HTMLDivElement>) => {
205
+ (event: React.FocusEvent<HTMLUListElement>) => {
203
206
  setFocusedElement(event.target)
204
207
 
205
208
  // Call the element's `focus` handler
@@ -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()
@@ -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,7 +25,7 @@ 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[]
29
30
  gap?: number | number[]
30
31
  /**
@@ -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
  })
@@ -25,20 +25,28 @@ describe('<Box />', () => {
25
25
  it('uses column when gridColumn is not provided', () => {
26
26
  render(<Box column={3} />)
27
27
 
28
- expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(expect.objectContaining({$column: [3]}))
28
+ expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(
29
+ expect.objectContaining({$column: [3]}),
30
+ )
29
31
  })
30
32
 
31
33
  it('uses gridColumn when provided', () => {
32
34
  render(<Box gridColumn={4} />)
33
35
 
34
- expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(expect.objectContaining({$column: [4]}))
36
+ expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(
37
+ expect.objectContaining({$column: [4]}),
38
+ )
35
39
  })
36
40
 
37
41
  it('prefers gridColumn over column when both are provided', () => {
38
42
  render(<Box column={3} gridColumn={5} />)
39
43
 
40
- expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(expect.objectContaining({$column: [5]}))
41
- expect(mockedResponsiveGridItemStyle).not.toHaveBeenCalledWith(expect.objectContaining({$column: [3]}))
44
+ expect(mockedResponsiveGridItemStyle).toHaveBeenCalledWith(
45
+ expect.objectContaining({$column: [5]}),
46
+ )
47
+ expect(mockedResponsiveGridItemStyle).not.toHaveBeenCalledWith(
48
+ expect.objectContaining({$column: [3]}),
49
+ )
42
50
  })
43
51
 
44
52
  it('supports responsive arrays with gridColumn', () => {