@sanity/ui 2.0.0-alpha.16 → 2.0.0-alpha.18

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 (46) hide show
  1. package/dist/index.d.ts +83 -1
  2. package/dist/index.esm.js +35 -22
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.js +35 -22
  5. package/dist/index.js.map +1 -1
  6. package/package.json +5 -7
  7. package/src/components/autocomplete/autocomplete.tsx +6 -0
  8. package/src/components/dialog/dialog.tsx +2 -0
  9. package/src/components/hotkeys/hotkeys.tsx +2 -0
  10. package/src/components/menu/menu.tsx +2 -0
  11. package/src/components/menu/menuButton.tsx +2 -0
  12. package/src/components/toast/toast.tsx +4 -0
  13. package/src/primitives/_selectable/style.ts +2 -0
  14. package/src/primitives/avatar/avatar.tsx +16 -9
  15. package/src/primitives/avatar/avatarCounter.tsx +3 -3
  16. package/src/primitives/avatar/styles.ts +1 -1
  17. package/src/primitives/badge/badge.tsx +2 -0
  18. package/src/primitives/box/box.tsx +3 -0
  19. package/src/primitives/button/__workshop__/sanityUploadButton.tsx +2 -1
  20. package/src/primitives/button/styles.ts +1 -0
  21. package/src/primitives/card/card.tsx +3 -0
  22. package/src/primitives/card/styles.ts +2 -0
  23. package/src/primitives/checkbox/checkbox.tsx +2 -0
  24. package/src/primitives/code/code.tsx +1 -0
  25. package/src/primitives/container/container.tsx +2 -0
  26. package/src/primitives/flex/flex.tsx +2 -0
  27. package/src/primitives/grid/grid.tsx +2 -0
  28. package/src/primitives/heading/heading.tsx +2 -0
  29. package/src/primitives/inline/inline.tsx +3 -0
  30. package/src/primitives/kbd/kbd.tsx +2 -0
  31. package/src/primitives/label/label.tsx +2 -0
  32. package/src/primitives/popover/popover.tsx +6 -1
  33. package/src/primitives/radio/radio.tsx +2 -0
  34. package/src/primitives/select/select.tsx +2 -0
  35. package/src/primitives/select/styles.ts +2 -0
  36. package/src/primitives/spinner/spinner.tsx +2 -0
  37. package/src/primitives/stack/stack.tsx +2 -0
  38. package/src/primitives/switch/switch.tsx +4 -0
  39. package/src/primitives/text/text.tsx +3 -0
  40. package/src/primitives/textArea/textArea.tsx +3 -0
  41. package/src/primitives/textInput/textInput.tsx +2 -0
  42. package/src/primitives/tooltip/tooltip.test.tsx +67 -0
  43. package/src/primitives/tooltip/tooltip.tsx +28 -5
  44. package/src/primitives/types.ts +4 -0
  45. package/src/theme/lib/theme/avatar.ts +2 -0
  46. package/src/theme/studioTheme/theme.ts +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.0.0-alpha.16",
3
+ "version": "2.0.0-alpha.18",
4
4
  "sideEffects": false,
5
5
  "types": "./dist/index.d.ts",
6
6
  "source": "./src/index.ts",
@@ -44,8 +44,8 @@
44
44
  "test": "jest",
45
45
  "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
46
46
  "watch": "pkg watch --strict",
47
- "workshop:build": "node -r esbuild-register -r tsconfig-paths/register scripts/build",
48
- "workshop:dev": "node -r esbuild-register -r tsconfig-paths/register scripts/dev",
47
+ "workshop:build": "workshop build",
48
+ "workshop:dev": "workshop dev",
49
49
  "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist",
50
50
  "storybook": "storybook dev -p 6006",
51
51
  "build-storybook": "storybook build"
@@ -68,7 +68,7 @@
68
68
  "@juggle/resize-observer": "^3.4.0",
69
69
  "@sanity/pkg-utils": "^2.4.8",
70
70
  "@sanity/semantic-release-preset": "^4.1.4",
71
- "@sanity/ui-workshop": "^1.2.11",
71
+ "@sanity/ui-workshop": "^2.0.0",
72
72
  "@storybook/addon-a11y": "^7.4.5",
73
73
  "@storybook/addon-docs": "^7.4.5",
74
74
  "@storybook/addon-essentials": "^7.4.5",
@@ -100,8 +100,6 @@
100
100
  "cypress": "^13.1.0",
101
101
  "cypress-real-events": "^1.10.1",
102
102
  "cz-conventional-changelog": "^3.3.0",
103
- "esbuild": "^0.19.2",
104
- "esbuild-register": "^3.4.2",
105
103
  "eslint": "^8.48.0",
106
104
  "eslint-config-prettier": "^9.0.0",
107
105
  "eslint-plugin-import": "^2.28.1",
@@ -126,7 +124,7 @@
126
124
  "rimraf": "^5.0.1",
127
125
  "semantic-release": "^21.1.1",
128
126
  "start-server-and-test": "^2.0.0",
129
- "storybook": "^7.4.5",
127
+ "storybook": "^7.5.2",
130
128
  "styled-components": "^6.0.7",
131
129
  "tsconfig-paths": "^4.2.0",
132
130
  "typescript": "^5.2.2"
@@ -60,6 +60,7 @@ export interface AutocompleteProps<Option extends BaseAutocompleteOption = BaseA
60
60
  onSelect?: (value: string) => void
61
61
  /** @beta */
62
62
  openButton?: boolean | AutocompleteOpenButtonProps
63
+ /** The options to render. */
63
64
  options?: Option[]
64
65
  padding?: number | number[]
65
66
  popover?: Omit<PopoverProps, 'content' | 'onMouseEnter' | 'onMouseLeave' | 'open'> &
@@ -68,6 +69,7 @@ export interface AutocompleteProps<Option extends BaseAutocompleteOption = BaseA
68
69
  radius?: Radius | Radius[]
69
70
  /** @beta */
70
71
  relatedElements?: HTMLElement[]
72
+ /** The callback function for rendering each option. */
71
73
  renderOption?: (option: Option) => ReactElement
72
74
  /** @beta */
73
75
  renderPopover?: (
@@ -82,6 +84,7 @@ export interface AutocompleteProps<Option extends BaseAutocompleteOption = BaseA
82
84
  ) => ReactNode
83
85
  renderValue?: (value: string, option?: Option) => string
84
86
  suffix?: ReactNode
87
+ /** The current value. */
85
88
  value?: string
86
89
  }
87
90
 
@@ -684,6 +687,9 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
684
687
  })
685
688
 
686
689
  /**
690
+ * The Autocomplete component is typically used for search components.
691
+ * It consists of a text input for writing a query, and properties for rendering suggestions.
692
+ *
687
693
  * @public
688
694
  */
689
695
  export const Autocomplete = InnerAutocomplete as <Option extends BaseAutocompleteOption>(
@@ -352,6 +352,8 @@ const DialogCard = forwardRef(function DialogCard(
352
352
  })
353
353
 
354
354
  /**
355
+ * The Dialog component.
356
+ *
355
357
  * @public
356
358
  */
357
359
  export const Dialog = forwardRef(function Dialog(
@@ -29,6 +29,8 @@ const Key = styled(KBD)`
29
29
  `
30
30
 
31
31
  /**
32
+ * Represent hotkeys (a keyboard combination) with semantic `<kbd>` elements.
33
+ *
32
34
  * @public
33
35
  */
34
36
  export const Hotkeys = forwardRef(function Hotkeys(
@@ -35,6 +35,8 @@ const Root = styled(Box)`
35
35
  `
36
36
 
37
37
  /**
38
+ * The `Menu` component is a building block for application menus.
39
+ *
38
40
  * @public
39
41
  */
40
42
  export const Menu = forwardRef(function Menu(
@@ -47,6 +47,8 @@ export interface MenuButtonProps {
47
47
  }
48
48
 
49
49
  /**
50
+ * The `MenuButton` component follows the WAI-ARIA specification for menu buttons.
51
+ *
50
52
  * @public
51
53
  */
52
54
  export const MenuButton = forwardRef(function MenuButton(
@@ -37,6 +37,10 @@ const TextBox = styled(Flex)`
37
37
  `
38
38
 
39
39
  /**
40
+ * The `Toast` component gives feedback to users when an action has taken place.
41
+ *
42
+ * Toasts can be closed with a close button, or auto-dismiss after a certain timeout.
43
+ *
40
44
  * @public
41
45
  */
42
46
  export function Toast(
@@ -22,6 +22,8 @@ export function selectableBaseStyle(): ReturnType<typeof css> {
22
22
  font: inherit;
23
23
  text-align: inherit;
24
24
  border: 0;
25
+ width: -moz-available;
26
+ width: -webkit-fill-available;
25
27
  width: stretch;
26
28
  }
27
29
 
@@ -15,6 +15,8 @@ export interface AvatarProps {
15
15
  arrowPosition?: AvatarPosition
16
16
  as?: React.ElementType | keyof JSX.IntrinsicElements
17
17
  color?: ThemeColorSpotKey
18
+ /** @beta */
19
+ hideInnerStroke?: boolean
18
20
  initials?: string
19
21
  onImageLoadError?: (event: Error) => void
20
22
  size?: AvatarSize | AvatarSize[]
@@ -41,6 +43,8 @@ const Stroke = styled.ellipse(avatarStyle.stroke)
41
43
  const Initials = styled.div(avatarStyle.initials)
42
44
 
43
45
  /**
46
+ * Avatars are used to represent people and other agents (e.g. bots).
47
+ *
44
48
  * @public
45
49
  */
46
50
  export const Avatar = forwardRef(function Avatar(
@@ -56,6 +60,7 @@ export const Avatar = forwardRef(function Avatar(
56
60
  onImageLoadError,
57
61
  arrowPosition: arrowPositionProp,
58
62
  animateArrowFrom,
63
+ hideInnerStroke,
59
64
  status = 'online',
60
65
  size: sizeProp = 0,
61
66
  ...restProps
@@ -140,13 +145,15 @@ export const Avatar = forwardRef(function Avatar(
140
145
  </defs>
141
146
 
142
147
  <circle cx={_radius} cy={_radius} r={_radius} fill={`url(#${imageId})`} />
143
- <BgStroke
144
- cx={_radius}
145
- cy={_radius}
146
- rx={_radius}
147
- ry={_radius}
148
- vectorEffect="non-scaling-stroke"
149
- />
148
+ {!hideInnerStroke && (
149
+ <BgStroke
150
+ cx={_radius}
151
+ cy={_radius}
152
+ rx={_radius}
153
+ ry={_radius}
154
+ vectorEffect="non-scaling-stroke"
155
+ />
156
+ )}
150
157
  <Stroke
151
158
  cx={_radius}
152
159
  cy={_radius}
@@ -161,8 +168,8 @@ export const Avatar = forwardRef(function Avatar(
161
168
  {(imageFailed || !src) && initials && (
162
169
  <>
163
170
  <Initials>
164
- <Text as="span" size={initialsSize}>
165
- <strong>{initials}</strong>
171
+ <Text as="span" size={initialsSize} weight="medium">
172
+ {initials}
166
173
  </Text>
167
174
  </Initials>
168
175
  </>
@@ -36,7 +36,7 @@ function _avatarCounterBaseStyle(props: ThemeProps) {
36
36
  background: var(--card-bg-color);
37
37
  box-shadow:
38
38
  0 0 0 1px var(--card-bg-color),
39
- inset 0 0 0 1.5px var(--card-hairline-hard-color);
39
+ inset 0 0 0 1px var(--card-hairline-hard-color);
40
40
  padding: 0 ${rem(theme.sanity.space[2])};
41
41
 
42
42
  &:not([hidden]) {
@@ -73,8 +73,8 @@ export const AvatarCounter = forwardRef(function AvatarCounter(
73
73
 
74
74
  return (
75
75
  <Root $size={size} data-ui="AvatarCounter" ref={ref}>
76
- <Text as="span" size={counterSize}>
77
- <strong>{count}</strong>
76
+ <Text as="span" size={counterSize} weight="medium">
77
+ {count}
78
78
  </Text>
79
79
  </Root>
80
80
  )
@@ -152,7 +152,7 @@ function avatarBgStrokeStyle(): CSSObject {
152
152
 
153
153
  function avatarStrokeStyle(): CSSObject {
154
154
  return {
155
- strokeWidth: '3px',
155
+ strokeWidth: '2px',
156
156
 
157
157
  '[data-status="editing"] &': {
158
158
  strokeDasharray: '2 4',
@@ -25,6 +25,8 @@ const Root = styled(Box)<BadgeStyleProps & ResponsiveRadiusStyleProps>(
25
25
  )
26
26
 
27
27
  /**
28
+ * Badges are used to tag resources.
29
+ *
28
30
  * @public
29
31
  */
30
32
  export const Badge = forwardRef(function Badge(
@@ -51,6 +51,9 @@ const Root = styled.div<
51
51
  )
52
52
 
53
53
  /**
54
+ * The `Box` component is a basic layout wrapper component which provides utility properties
55
+ * for flex, margins and padding.
56
+ *
54
57
  * @public
55
58
  */
56
59
  export const Box = forwardRef(function Box(
@@ -4,7 +4,7 @@ import styled from 'styled-components'
4
4
 
5
5
  const SanityUploadButton = styled(Button).attrs({forwardedAs: 'label'})`
6
6
  & input {
7
- -webkit-appearance: none;
7
+ appearance: none;
8
8
  overflow: hidden;
9
9
  overflow: clip;
10
10
  top: 0;
@@ -14,6 +14,7 @@ const SanityUploadButton = styled(Button).attrs({forwardedAs: 'label'})`
14
14
  opacity: 0;
15
15
  position: absolute;
16
16
  max-width: 0;
17
+ width: -webkit-fill-available;
17
18
  width: stretch;
18
19
  }
19
20
 
@@ -29,6 +29,7 @@ export function buttonBaseStyles({$width}: {$width?: ButtonWidth}): ReturnType<t
29
29
 
30
30
  ${$width === 'fill' &&
31
31
  css`
32
+ width: -moz-available;
32
33
  width: -webkit-fill-available;
33
34
  width: stretch;
34
35
  `}
@@ -48,6 +48,9 @@ const Root = styled(Box)<
48
48
  >(responsiveBorderStyle, responsiveRadiusStyle, responsiveShadowStyle, cardStyle)
49
49
 
50
50
  /**
51
+ * The `Card` component acts much like a `Box`, but with a background and foreground color.
52
+ * Components within a `Card` inherit its colors.
53
+ *
51
54
  * @public
52
55
  */
53
56
  export const Card = forwardRef(function Card(
@@ -29,6 +29,8 @@ export function cardBaseStyle(props: CardStyleProps & ThemeProps): ReturnType<ty
29
29
  font: inherit;
30
30
  text-align: inherit;
31
31
  border: 0;
32
+ width: -moz-available;
33
+ width: -webkit-fill-available;
32
34
  width: stretch;
33
35
  }
34
36
 
@@ -16,6 +16,8 @@ const Root = styled.div(checkboxBaseStyles)
16
16
  const Input = styled.input(inputElementStyles)
17
17
 
18
18
  /**
19
+ * Checkboxes allow the user to select one or more items from a set.
20
+ *
19
21
  * @public
20
22
  */
21
23
  export const Checkbox = forwardRef(function Checkbox(
@@ -11,6 +11,7 @@ import {codeBaseStyle} from './styles'
11
11
  */
12
12
  export interface CodeProps {
13
13
  as?: React.ElementType | keyof JSX.IntrinsicElements
14
+ /** Define the language to use for syntax highlighting. */
14
15
  language?: string
15
16
  size?: number | number[]
16
17
  weight?: string
@@ -17,6 +17,8 @@ const Root = styled(Box)<ResponsiveWidthStyleProps>(
17
17
  )
18
18
 
19
19
  /**
20
+ * The `Container` component wraps content layout in a defined set of widths.
21
+ *
20
22
  * @public
21
23
  */
22
24
  export const Container = forwardRef(function Container(
@@ -26,6 +26,8 @@ const Root = styled(Box)<FlexItemStyleProps & ResponsiveFlexStyleProps>(
26
26
  )
27
27
 
28
28
  /**
29
+ * The `Flex` component is a wrapper component for flexible elements (`Box`, `Card` and `Flex`).
30
+ *
29
31
  * @public
30
32
  */
31
33
  export const Flex = forwardRef(function Flex(
@@ -13,6 +13,8 @@ export interface GridProps extends Omit<BoxProps, 'display'>, ResponsiveGridProp
13
13
  const Root = styled(Box)<ResponsiveGridStyleProps>(responsiveGridStyle)
14
14
 
15
15
  /**
16
+ * The `Grid` component is for building 2-dimensional layers (based on CSS grid).
17
+ *
16
18
  * @public
17
19
  */
18
20
  export const Grid = forwardRef(function Grid(
@@ -43,6 +43,8 @@ const SpanWithTextOverflow = styled.span`
43
43
  `
44
44
 
45
45
  /**
46
+ * Typographic headings.
47
+ *
46
48
  * @public
47
49
  */
48
50
  export const Heading = forwardRef(function Heading(
@@ -10,12 +10,15 @@ import {ResponsiveInlineSpaceStyleProps} from './types'
10
10
  * @public
11
11
  */
12
12
  export interface InlineProps extends Omit<BoxProps, 'display'> {
13
+ /** The spacing between children. */
13
14
  space?: number | number[]
14
15
  }
15
16
 
16
17
  const Root = styled(Box)<ResponsiveInlineSpaceStyleProps>(inlineBaseStyle, inlineSpaceStyle)
17
18
 
18
19
  /**
20
+ * The `Inline` component is a layout utility for aligning and spacing items horizontally.
21
+ *
19
22
  * @public
20
23
  */
21
24
  export const Inline = forwardRef(function Inline(
@@ -30,6 +30,8 @@ function kbdStyle() {
30
30
  const Root = styled.kbd<ResponsiveRadiusStyleProps>(responsiveRadiusStyle, kbdStyle)
31
31
 
32
32
  /**
33
+ * Used to define some text as keyboard input.
34
+ *
33
35
  * @public
34
36
  */
35
37
  export const KBD = forwardRef(function KBD(
@@ -40,6 +40,8 @@ const SpanWithTextOverflow = styled.span`
40
40
  `
41
41
 
42
42
  /**
43
+ * Typographic labels.
44
+ *
43
45
  * @public
44
46
  */
45
47
  export const Label = forwardRef(function Label(
@@ -59,6 +59,7 @@ export interface PopoverProps
59
59
  overflow?: BoxOverflow
60
60
  padding?: number | number[]
61
61
  placement?: Placement
62
+ /** Whether or not to render the popover in a portal element. */
62
63
  portal?: boolean | string
63
64
  preventOverflow?: boolean
64
65
  referenceBoundary?: HTMLElement | null
@@ -72,7 +73,11 @@ export interface PopoverProps
72
73
  width?: PopoverWidth | PopoverWidth[]
73
74
  }
74
75
 
75
- /** @public */
76
+ /**
77
+ * The `Popover` component is used to display some content on top of another.
78
+ *
79
+ * @public
80
+ */
76
81
  export const Popover = memo(
77
82
  forwardRef(function Popover(
78
83
  props: PopoverProps &
@@ -14,6 +14,8 @@ const Root = styled.div(radioBaseStyle)
14
14
  const Input = styled.input(inputElementStyle)
15
15
 
16
16
  /**
17
+ * The `Radio` component allows the user to select one option from a set.
18
+ *
17
19
  * @public
18
20
  */
19
21
  export const Radio = forwardRef(function Radio(
@@ -30,6 +30,8 @@ const Input = styled.select<{
30
30
  const IconBox = styled(Box)(selectStyle.iconBox)
31
31
 
32
32
  /**
33
+ * The `Select` component provides control of options.
34
+ *
33
35
  * @public
34
36
  */
35
37
  export const Select = forwardRef(function Select(
@@ -13,6 +13,8 @@ import {CSSObject} from '../../types/styled'
13
13
  function rootStyle(): ReturnType<typeof css> {
14
14
  return css`
15
15
  position: relative;
16
+ width: -moz-available;
17
+ width: -webkit-fill-available;
16
18
  width: stretch;
17
19
 
18
20
  &:not([hidden]) {
@@ -28,6 +28,8 @@ const Root = styled(Text)`
28
28
  `
29
29
 
30
30
  /**
31
+ * Indicate that something is loading for an indeterminate amount of time.
32
+ *
31
33
  * @public
32
34
  */
33
35
  export const Spinner = forwardRef(function Spinner(
@@ -15,6 +15,8 @@ export interface StackProps extends BoxProps {
15
15
  const Root = styled(Box)<ResponsiveStackSpaceStyleProps>(stackBaseStyle, responsiveStackSpaceStyle)
16
16
 
17
17
  /**
18
+ * The `Stack` component is used to place elements on top of each other.
19
+ *
18
20
  * @public
19
21
  */
20
22
  export const Stack = forwardRef(function Stack(
@@ -23,6 +23,10 @@ const Track = styled.span(switchTrackStyles)
23
23
  const Thumb = styled.span<{$checked?: boolean; $indeterminate?: boolean}>(switchThumbStyles)
24
24
 
25
25
  /**
26
+ * The `Switch` component allows the user to toggle a setting on and off.
27
+ *
28
+ * Extends all properties of an `<input type="checkbox" />` element, except type.
29
+ *
26
30
  * @public
27
31
  */
28
32
  export const Switch = forwardRef(function Switch(
@@ -17,6 +17,7 @@ export interface TextProps {
17
17
  accent?: boolean
18
18
  align?: TextAlign | TextAlign[]
19
19
  as?: React.ElementType | keyof JSX.IntrinsicElements
20
+ /** When `true` the text color will be muted. */
20
21
  muted?: boolean
21
22
  size?: number | number[]
22
23
  /**
@@ -43,6 +44,8 @@ const SpanWithTextOverflow = styled.span`
43
44
  `
44
45
 
45
46
  /**
47
+ * The `Text` component is an agile, themed typographic element.
48
+ *
46
49
  * @public
47
50
  */
48
51
  export const Text = forwardRef(function Text(
@@ -48,6 +48,9 @@ const Presentation = styled.div<ResponsiveRadiusStyleProps & TextInputRepresenta
48
48
  )
49
49
 
50
50
  /**
51
+ * A multiline text input.
52
+ *
53
+
51
54
  * @public
52
55
  */
53
56
  export const TextArea = forwardRef(function TextArea(
@@ -140,6 +140,8 @@ const TextInputClearButton = styled(Button)({
140
140
  })
141
141
 
142
142
  /**
143
+ * Single line text input.
144
+ *
143
145
  * @public
144
146
  */
145
147
  export const TextInput = forwardRef(function TextInput(
@@ -275,4 +275,71 @@ describe('Tooltip', () => {
275
275
  jest.clearAllMocks()
276
276
  })
277
277
  })
278
+ describe('Closing the <Tooltip /> with the Escape key', () => {
279
+ it('Standalone tooltip closes immediately with Escape key', () => {
280
+ const delay = 150
281
+
282
+ jest.useFakeTimers()
283
+
284
+ render(
285
+ <Tooltip
286
+ content={<Text size={1}>{'Tooltip content'}</Text>}
287
+ placement={'top'}
288
+ delay={delay}
289
+ >
290
+ <Button mode="bleed" text="Hover me" />
291
+ </Tooltip>,
292
+ )
293
+
294
+ const button = screen.getByText('Hover me')
295
+
296
+ // Validate tooltip content is not rendered
297
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
298
+ fireEvent.focus(button)
299
+ act(() => jest.advanceTimersByTime(delay))
300
+
301
+ // Validate tooltip content is rendered
302
+ screen.getByText('Tooltip content')
303
+
304
+ act(() => {
305
+ fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
306
+ })
307
+ // Validate tooltip content is not rendered anymore
308
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
309
+ })
310
+ it('With <TooltipDelayGroupProvider /> closes immediately with Escape key', () => {
311
+ const delay = 150
312
+
313
+ jest.useFakeTimers()
314
+
315
+ render(
316
+ <TooltipDelayGroupProvider delay={{close: delay}}>
317
+ <Tooltip
318
+ content={<Text size={1}>{'Tooltip content'}</Text>}
319
+ placement={'top'}
320
+ delay={{close: delay}}
321
+ >
322
+ <Button mode="bleed" text="Hover me" />
323
+ </Tooltip>
324
+ </TooltipDelayGroupProvider>,
325
+ )
326
+
327
+ const button = screen.getByText('Hover me')
328
+
329
+ // Validate tooltip content is not rendered
330
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
331
+ fireEvent.focus(button)
332
+
333
+ act(() => jest.advanceTimersByTime(delay))
334
+
335
+ // Validate tooltip content is rendered
336
+ screen.getByText('Tooltip content')
337
+
338
+ act(() => {
339
+ fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
340
+ })
341
+ // Validate tooltip content is not rendered anymore
342
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
343
+ })
344
+ })
278
345
  })
@@ -48,6 +48,7 @@ export interface TooltipProps extends Omit<LayerProps, 'as'> {
48
48
  fallbackPlacements?: Placement[]
49
49
  padding?: number | number[]
50
50
  placement?: Placement
51
+ /** Whether or not to render the tooltip in a portal element. */
51
52
  portal?: boolean | string
52
53
  scheme?: ThemeColorSchemeKey
53
54
  shadow?: number | number[]
@@ -66,6 +67,8 @@ const Root = styled(Layer)`
66
67
  `
67
68
 
68
69
  /**
70
+ * Tooltips display information when hovering, focusing or tapping.
71
+ *
69
72
  * @public
70
73
  */
71
74
  export const Tooltip = forwardRef(function Tooltip(
@@ -177,23 +180,27 @@ export const Tooltip = forwardRef(function Tooltip(
177
180
  const closeDelay = isInsideGroup ? delayGroupContext.closeDelay : closeDelayProp
178
181
 
179
182
  const handleIsOpenChange = useCallback(
180
- (open: boolean) => {
183
+ (open: boolean, immediate?: boolean) => {
181
184
  if (isInsideGroup) {
182
185
  // When it's inside a group, the open or close status will be handled by the group.
183
186
  if (open) {
184
- delayGroupContext.setIsGroupActive(open, openDelay)
185
- delayGroupContext.setOpenTooltipId(tooltipId, openDelay)
187
+ const groupedOpenDelay = immediate ? 0 : openDelay
188
+
189
+ delayGroupContext.setIsGroupActive(open, groupedOpenDelay)
190
+ delayGroupContext.setOpenTooltipId(tooltipId, groupedOpenDelay)
186
191
  } else {
187
192
  const minimumGroupDeactivateDelay = 200 // We should provide some delay to allow the user to reach the next tooltip.
188
193
  const groupDeactivateDelay =
189
194
  closeDelay > minimumGroupDeactivateDelay ? closeDelay : minimumGroupDeactivateDelay
190
195
 
191
196
  delayGroupContext.setIsGroupActive(open, groupDeactivateDelay)
192
- delayGroupContext.setOpenTooltipId(null, closeDelay)
197
+ delayGroupContext.setOpenTooltipId(null, immediate ? 0 : closeDelay)
193
198
  }
194
199
  } else {
200
+ const standaloneDelay = immediate ? 0 : open ? openDelay : closeDelay
201
+
195
202
  // When it's not inside a group, the open or close status will be handled by the tooltip itself.
196
- setIsOpen(open, open ? openDelay : closeDelay)
203
+ setIsOpen(open, standaloneDelay)
197
204
  }
198
205
  },
199
206
  [isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
@@ -243,6 +250,22 @@ export const Tooltip = forwardRef(function Tooltip(
243
250
  // Update reference
244
251
  useEffect(() => refs.setReference(referenceElement), [referenceElement, refs])
245
252
 
253
+ useEffect(() => {
254
+ // If the user clicks on escape key, close the tooltip.
255
+ if (!showTooltip) return
256
+
257
+ function handleWindowKeyDown(event: KeyboardEvent) {
258
+ if (event.key === 'Escape') {
259
+ handleIsOpenChange(false, true)
260
+ }
261
+ }
262
+
263
+ window.addEventListener('keydown', handleWindowKeyDown)
264
+
265
+ return () => {
266
+ window.removeEventListener('keydown', handleWindowKeyDown)
267
+ }
268
+ }, [handleIsOpenChange, showTooltip])
246
269
  const setArrow = useCallback(
247
270
  (arrowEl: HTMLDivElement | null) => {
248
271
  arrowRef.current = arrowEl