@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
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.1.14",
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
  /**
@@ -0,0 +1,51 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Box} from '../../primitives'
5
+ import {Breadcrumbs} from './breadcrumbs'
6
+
7
+ jest.mock('../../primitives', () => {
8
+ const actual = jest.requireActual('../../primitives')
9
+
10
+ return {
11
+ ...actual,
12
+ Box: jest.fn((props: Record<string, unknown>) => (actual.Box as any).render(props, null)),
13
+ }
14
+ })
15
+
16
+ function renderBreadcrumbs(props: Partial<React.ComponentProps<typeof Breadcrumbs>> = {}) {
17
+ return render(
18
+ <Breadcrumbs {...props}>
19
+ <span>Root</span>
20
+ <span>Section</span>
21
+ </Breadcrumbs>,
22
+ )
23
+ }
24
+
25
+ describe('components/breadcrumbs spacing', () => {
26
+ const mockedBox = jest.mocked(Box)
27
+
28
+ beforeEach(() => {
29
+ mockedBox.mockClear()
30
+ })
31
+
32
+ it('should support `space` and `gap` with the same behavior', () => {
33
+ renderBreadcrumbs({space: 2})
34
+ expect(mockedBox.mock.calls.map(([props]) => props)).toContainEqual(
35
+ expect.objectContaining({paddingX: [2]}),
36
+ )
37
+
38
+ mockedBox.mockClear()
39
+ renderBreadcrumbs({gap: 2})
40
+ expect(mockedBox.mock.calls.map(([props]) => props)).toContainEqual(
41
+ expect.objectContaining({paddingX: [2]}),
42
+ )
43
+ })
44
+
45
+ it('should prefer `gap` over `space` when both are provided', () => {
46
+ renderBreadcrumbs({gap: 3, space: 1})
47
+ const propsList = mockedBox.mock.calls.map(([props]) => props)
48
+ expect(propsList).toContainEqual(expect.objectContaining({paddingX: [3]}))
49
+ expect(propsList).not.toContainEqual(expect.objectContaining({paddingX: [1]}))
50
+ })
51
+ })
@@ -20,6 +20,10 @@ import {ExpandButton, StyledBreadcrumbs} from './breadcrumbs.styles'
20
20
  export interface BreadcrumbsProps {
21
21
  maxLength?: number
22
22
  separator?: React.ReactNode
23
+ gap?: number | number[]
24
+ /**
25
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
26
+ */
23
27
  space?: number | number[]
24
28
  }
25
29
 
@@ -30,8 +34,8 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
30
34
  props: BreadcrumbsProps & Omit<React.HTMLProps<HTMLOListElement>, 'as' | 'ref' | 'type'>,
31
35
  ref: React.ForwardedRef<HTMLOListElement>,
32
36
  ) {
33
- const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
34
- const space = _getArrayProp(spaceRaw)
37
+ const {children, gap, maxLength, separator, space: deprecated_space = 2, ...restProps} = props
38
+ const space = _getArrayProp(gap === undefined ? deprecated_space : gap)
35
39
  const [open, setOpen] = useState(false)
36
40
  const expandElementRef = useRef<HTMLButtonElement | null>(null)
37
41
  const popoverElementRef = useRef<HTMLDivElement | null>(null)
@@ -101,7 +105,7 @@ function useItems({
101
105
  <Popover
102
106
  constrainSize
103
107
  content={
104
- <Stack as="ol" overflow="auto" padding={space} space={space}>
108
+ <Stack as="ol" overflow="auto" padding={space} gap={space}>
105
109
  {rawItems.slice(beforeLength - 1, len - afterLength)}
106
110
  </Stack>
107
111
  }
@@ -0,0 +1,42 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Inline} from '../../primitives'
5
+ import {Hotkeys} from './hotkeys'
6
+
7
+ jest.mock('../../primitives', () => {
8
+ const actual = jest.requireActual('../../primitives')
9
+
10
+ return {
11
+ ...actual,
12
+ Inline: jest.fn((props: Record<string, unknown>) => (actual.Inline as any).render(props, null)),
13
+ }
14
+ })
15
+
16
+ describe('components/hotkeys spacing', () => {
17
+ const mockedInline = jest.mocked(Inline)
18
+
19
+ beforeEach(() => {
20
+ mockedInline.mockClear()
21
+ })
22
+
23
+ it('should support `space` and `gap` with the same behavior', () => {
24
+ render(<Hotkeys keys={['Ctrl', 'S']} space={2} />)
25
+ expect(mockedInline.mock.calls.map(([props]) => props)).toContainEqual(
26
+ expect.objectContaining({gap: [2]}),
27
+ )
28
+
29
+ mockedInline.mockClear()
30
+ render(<Hotkeys gap={2} keys={['Ctrl', 'S']} />)
31
+ expect(mockedInline.mock.calls.map(([props]) => props)).toContainEqual(
32
+ expect.objectContaining({gap: [2]}),
33
+ )
34
+ })
35
+
36
+ it('should prefer `gap` over `space` when both are provided', () => {
37
+ render(<Hotkeys gap={3} keys={['Ctrl', 'S']} space={1} />)
38
+ const propsList = mockedInline.mock.calls.map(([props]) => props)
39
+ expect(propsList).toContainEqual(expect.objectContaining({gap: [3]}))
40
+ expect(propsList).not.toContainEqual(expect.objectContaining({gap: [1]}))
41
+ })
42
+ })
@@ -10,8 +10,12 @@ import {Radius} from '../../types'
10
10
  */
11
11
  export interface HotkeysProps {
12
12
  fontSize?: number | number[]
13
+ gap?: number | number[]
13
14
  padding?: number | number[]
14
15
  radius?: Radius | Radius[]
16
+ /**
17
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
18
+ */
15
19
  space?: number | number[]
16
20
  keys?: string[]
17
21
  }
@@ -40,8 +44,8 @@ export const Hotkeys = forwardRef(function Hotkeys(
40
44
  props: HotkeysProps & Omit<React.HTMLProps<HTMLElement>, 'as' | 'ref' | 'size'>,
41
45
  ref: React.Ref<HTMLElement>,
42
46
  ) {
43
- const {fontSize, keys, padding, radius, space: spaceProp = 0.5, ...restProps} = props
44
- const space = _getArrayProp(spaceProp)
47
+ const {fontSize, gap, keys, padding, radius, space: deprecated_space = 0.5, ...restProps} = props
48
+ const spacing = _getArrayProp(gap === undefined ? deprecated_space : gap)
45
49
 
46
50
  if (!keys || keys.length === 0) {
47
51
  return <></>
@@ -49,7 +53,7 @@ export const Hotkeys = forwardRef(function Hotkeys(
49
53
 
50
54
  return (
51
55
  <StyledHotkeys data-ui="Hotkeys" {...restProps} ref={ref}>
52
- <Inline as="span" space={space}>
56
+ <Inline as="span" gap={spacing}>
53
57
  {keys.map((key, i) => (
54
58
  <Key fontSize={fontSize} key={i} padding={padding} radius={radius}>
55
59
  {key}
@@ -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">
@@ -0,0 +1,63 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Stack} from '../../primitives'
5
+ import {Menu} from './menu'
6
+
7
+ jest.mock('../../primitives', () => {
8
+ const actual = jest.requireActual('../../primitives')
9
+
10
+ return {
11
+ ...actual,
12
+ Stack: jest.fn((props: Record<string, unknown>) => (actual.Stack as any).render(props, null)),
13
+ }
14
+ })
15
+
16
+ jest.mock('../../utils', () => {
17
+ const actual = jest.requireActual('../../utils')
18
+
19
+ return {
20
+ ...actual,
21
+ useLayer: () => ({isTopLayer: true}),
22
+ }
23
+ })
24
+
25
+ describe('components/menu spacing', () => {
26
+ const mockedStack = jest.mocked(Stack)
27
+
28
+ beforeEach(() => {
29
+ mockedStack.mockClear()
30
+ })
31
+
32
+ it('should support `space` and `gap` with the same behavior', () => {
33
+ render(
34
+ <Menu space={2}>
35
+ <div>Item</div>
36
+ </Menu>,
37
+ )
38
+ expect(mockedStack.mock.calls.map(([props]) => props)).toContainEqual(
39
+ expect.objectContaining({gap: 2}),
40
+ )
41
+
42
+ mockedStack.mockClear()
43
+ render(
44
+ <Menu gap={2}>
45
+ <div>Item</div>
46
+ </Menu>,
47
+ )
48
+ expect(mockedStack.mock.calls.map(([props]) => props)).toContainEqual(
49
+ expect.objectContaining({gap: 2}),
50
+ )
51
+ })
52
+
53
+ it('should prefer `gap` over `space` when both are provided', () => {
54
+ render(
55
+ <Menu gap={3} space={1}>
56
+ <div>Item</div>
57
+ </Menu>,
58
+ )
59
+ const propsList = mockedStack.mock.calls.map(([props]) => props)
60
+ expect(propsList).toContainEqual(expect.objectContaining({gap: 3}))
61
+ expect(propsList).not.toContainEqual(expect.objectContaining({gap: 1}))
62
+ })
63
+ })
@@ -27,6 +27,10 @@ export interface MenuProps extends ResponsivePaddingProps {
27
27
  'originElement'?: HTMLElement | null
28
28
  'registerElement'?: (el: HTMLElement) => () => void
29
29
  'shouldFocus'?: 'first' | 'last' | null
30
+ 'gap'?: number | number[]
31
+ /**
32
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
33
+ */
30
34
  'space'?: number | number[]
31
35
  'aria-labelledby'?: string
32
36
  'onBlurCapture'?: (event: FocusEvent) => void
@@ -61,9 +65,11 @@ export const Menu = forwardRef(function Menu(
61
65
  padding = 1,
62
66
  registerElement,
63
67
  shouldFocus: _shouldFocus,
64
- space = 1,
68
+ gap,
69
+ space: deprecated_space = 1,
65
70
  ...restProps
66
71
  } = props
72
+ const spacing = gap === undefined ? deprecated_space : gap
67
73
  const shouldFocus =
68
74
  _shouldFocus ?? ((props.focusFirst && 'first') || (props.focusLast && 'last') || null)
69
75
 
@@ -164,7 +170,7 @@ export const Menu = forwardRef(function Menu(
164
170
  role="menu"
165
171
  tabIndex={-1}
166
172
  >
167
- <Stack space={space}>{children}</Stack>
173
+ <Stack gap={spacing}>{children}</Stack>
168
174
  </StyledMenu>
169
175
  </MenuContext.Provider>
170
176
  )
@@ -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
@@ -0,0 +1,63 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Flex} from '../../primitives'
5
+ import {MenuContext, MenuContextValue} from './menuContext'
6
+ import {MenuGroup} from './menuGroup'
7
+ import {MenuItem} from './menuItem'
8
+
9
+ jest.mock('../../primitives', () => {
10
+ const actual = jest.requireActual('../../primitives')
11
+
12
+ return {
13
+ ...actual,
14
+ Flex: jest.fn((props: Record<string, unknown>) => (actual.Flex as any).render(props, null)),
15
+ Popover: jest.fn(({children}: {children?: React.ReactNode}) => children),
16
+ }
17
+ })
18
+
19
+ const menuContextValue: MenuContextValue = {
20
+ version: 2,
21
+ activeElement: null,
22
+ mount: () => () => undefined,
23
+ onItemMouseEnter: () => undefined,
24
+ onItemMouseLeave: () => undefined,
25
+ }
26
+
27
+ function renderMenuGroup(props: Partial<React.ComponentProps<typeof MenuGroup>> = {}) {
28
+ return render(
29
+ <MenuContext.Provider value={menuContextValue}>
30
+ <MenuGroup id="menu-group" text="Menu group" {...props}>
31
+ <MenuItem id="submenu-item" text="Submenu item" />
32
+ </MenuGroup>
33
+ </MenuContext.Provider>,
34
+ )
35
+ }
36
+
37
+ describe('components/menuGroup spacing', () => {
38
+ const mockedFlex = jest.mocked(Flex)
39
+
40
+ beforeEach(() => {
41
+ mockedFlex.mockClear()
42
+ })
43
+
44
+ it('should support `space` and `gap` with the same behavior', () => {
45
+ renderMenuGroup({space: 2})
46
+ expect(mockedFlex.mock.calls.map(([props]) => props)).toContainEqual(
47
+ expect.objectContaining({gap: 2}),
48
+ )
49
+
50
+ mockedFlex.mockClear()
51
+ renderMenuGroup({gap: 2})
52
+ expect(mockedFlex.mock.calls.map(([props]) => props)).toContainEqual(
53
+ expect.objectContaining({gap: 2}),
54
+ )
55
+ })
56
+
57
+ it('should prefer `gap` over `space` when both are provided', () => {
58
+ renderMenuGroup({gap: 3, space: 1})
59
+ const propsList = mockedFlex.mock.calls.map(([props]) => props)
60
+ expect(propsList).toContainEqual(expect.objectContaining({gap: 3}))
61
+ expect(propsList).not.toContainEqual(expect.objectContaining({gap: 1}))
62
+ })
63
+ })
@@ -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<
@@ -31,6 +30,10 @@ export interface MenuGroupProps {
31
30
  padding?: number | number[]
32
31
  popover?: Omit<PopoverProps, 'content' | 'open'>
33
32
  radius?: Radius | Radius[]
33
+ gap?: number | number[]
34
+ /**
35
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
36
+ */
34
37
  space?: number | number[]
35
38
  text: React.ReactNode
36
39
  tone?: SelectableTone
@@ -39,9 +42,11 @@ export interface MenuGroupProps {
39
42
  /**
40
43
  * @public
41
44
  */
42
- export function MenuGroup(
45
+ export type MenuGroupProps<E extends ElementType = 'button'> = Props<MenuGroupOwnProps, E>
46
+
47
+ function MenuGroupComponent(
43
48
  props: Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'popover' | 'ref' | 'tabIndex'> &
44
- MenuGroupProps,
49
+ MenuGroupOwnProps & {as?: ElementType},
45
50
  ): React.JSX.Element {
46
51
  const {
47
52
  as = 'button',
@@ -53,11 +58,13 @@ export function MenuGroup(
53
58
  padding = 3,
54
59
  popover,
55
60
  radius = 2,
56
- space = 3,
61
+ gap,
62
+ space: deprecated_space = 3,
57
63
  text,
58
64
  tone = 'default',
59
65
  ...restProps
60
66
  } = props
67
+ const spacing = gap === undefined ? deprecated_space : gap
61
68
  const menu = useMenu()
62
69
  const {scheme} = useRootTheme()
63
70
  const {
@@ -193,7 +200,7 @@ export function MenuGroup(
193
200
  tabIndex={-1}
194
201
  type={as === 'button' ? 'button' : undefined}
195
202
  >
196
- <Flex gap={space} padding={padding}>
203
+ <Flex gap={spacing} padding={padding}>
197
204
  {IconComponent && (
198
205
  <Text size={fontSize}>
199
206
  {isValidElement(IconComponent) && IconComponent}
@@ -216,4 +223,11 @@ export function MenuGroup(
216
223
  )
217
224
  }
218
225
 
219
- 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
@@ -0,0 +1,59 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {render} from '../../../../test'
4
+ import {Flex} from '../../primitives'
5
+ import {MenuContext, MenuContextValue} from './menuContext'
6
+ import {MenuItem} from './menuItem'
7
+
8
+ jest.mock('../../primitives', () => {
9
+ const actual = jest.requireActual('../../primitives')
10
+
11
+ return {
12
+ ...actual,
13
+ Flex: jest.fn((props: Record<string, unknown>) => (actual.Flex as any).render(props, null)),
14
+ }
15
+ })
16
+
17
+ const menuContextValue: MenuContextValue = {
18
+ version: 2,
19
+ activeElement: null,
20
+ mount: () => () => undefined,
21
+ onItemMouseEnter: () => undefined,
22
+ onItemMouseLeave: () => undefined,
23
+ }
24
+
25
+ function renderMenuItem(props: Partial<React.ComponentProps<typeof MenuItem>> = {}) {
26
+ return render(
27
+ <MenuContext.Provider value={menuContextValue}>
28
+ <MenuItem id="menu-item" text="Menu item" {...props} />
29
+ </MenuContext.Provider>,
30
+ )
31
+ }
32
+
33
+ describe('components/menuItem spacing', () => {
34
+ const mockedFlex = jest.mocked(Flex)
35
+
36
+ beforeEach(() => {
37
+ mockedFlex.mockClear()
38
+ })
39
+
40
+ it('should support `space` and `gap` with the same behavior', () => {
41
+ renderMenuItem({space: 2})
42
+ expect(mockedFlex.mock.calls.map(([props]) => props)).toContainEqual(
43
+ expect.objectContaining({gap: 2}),
44
+ )
45
+
46
+ mockedFlex.mockClear()
47
+ renderMenuItem({gap: 2})
48
+ expect(mockedFlex.mock.calls.map(([props]) => props)).toContainEqual(
49
+ expect.objectContaining({gap: 2}),
50
+ )
51
+ })
52
+
53
+ it('should prefer `gap` over `space` when both are provided', () => {
54
+ renderMenuItem({gap: 3, space: 1})
55
+ const propsList = mockedFlex.mock.calls.map(([props]) => props)
56
+ expect(propsList).toContainEqual(expect.objectContaining({gap: 3}))
57
+ expect(propsList).not.toContainEqual(expect.objectContaining({gap: 1}))
58
+ })
59
+ })
@@ -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,14 +23,17 @@ 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
30
30
  iconRight?: React.ElementType | React.ReactNode
31
31
  pressed?: boolean
32
32
  selected?: boolean
33
+ gap?: number | number[]
34
+ /**
35
+ * @deprecated Use `gap` instead. `space` will be removed in v4.
36
+ */
33
37
  space?: number | number[]
34
38
  text?: React.ReactNode
35
39
  tone?: SelectableTone
@@ -38,9 +42,13 @@ export interface MenuItemProps extends ResponsivePaddingProps, ResponsiveRadiusP
38
42
  /**
39
43
  * @public
40
44
  */
41
- export const MenuItem = forwardRef(function MenuItem(
42
- props: MenuItemProps &
43
- 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
+ >,
44
52
  forwardedRef: React.ForwardedRef<HTMLDivElement>,
45
53
  ) {
46
54
  const {
@@ -62,11 +70,13 @@ export const MenuItem = forwardRef(function MenuItem(
62
70
  pressed,
63
71
  radius = 2,
64
72
  selected: selectedProp,
65
- space = 3,
73
+ gap,
74
+ space: deprecated_space = 3,
66
75
  text,
67
76
  tone = 'default',
68
77
  ...restProps
69
78
  } = props
79
+ const spacing = gap === undefined ? deprecated_space : gap
70
80
  const {scheme} = useRootTheme()
71
81
  const menu = useMenu()
72
82
  const {
@@ -137,7 +147,7 @@ export const MenuItem = forwardRef(function MenuItem(
137
147
  type={as === 'button' ? 'button' : undefined}
138
148
  >
139
149
  {(IconComponent || text || IconRightComponent) && (
140
- <Flex as="span" gap={space} align="center" {...paddingProps}>
150
+ <Flex as="span" gap={spacing} align="center" {...paddingProps}>
141
151
  {IconComponent && (
142
152
  <Text size={fontSize}>
143
153
  {isValidElement(IconComponent) && IconComponent}
@@ -177,4 +187,11 @@ export const MenuItem = forwardRef(function MenuItem(
177
187
  </Selectable>
178
188
  )
179
189
  })
180
- 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