@microbit/ui 0.1.0-alpha.8 → 0.1.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 (114) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +171 -20
  3. package/lang/ui.ar.json +62 -0
  4. package/lang/ui.ca.json +43 -3
  5. package/lang/ui.cy.json +62 -0
  6. package/lang/ui.de.json +44 -4
  7. package/lang/ui.en-us.json +40 -0
  8. package/lang/ui.en.json +40 -0
  9. package/lang/ui.es-es.json +43 -3
  10. package/lang/ui.fr.json +44 -4
  11. package/lang/ui.ga-ie.json +43 -3
  12. package/lang/ui.it.json +62 -0
  13. package/lang/ui.ja.json +44 -4
  14. package/lang/ui.ko.json +44 -4
  15. package/lang/ui.lo.json +62 -0
  16. package/lang/ui.lol.json +40 -0
  17. package/lang/ui.nl.json +44 -4
  18. package/lang/ui.pl.json +44 -4
  19. package/lang/ui.pt-br.json +44 -4
  20. package/lang/ui.vi.json +62 -0
  21. package/lang/ui.zh-cn.json +44 -4
  22. package/lang/ui.zh-tw.json +44 -4
  23. package/package.json +12 -10
  24. package/reset.css +12 -0
  25. package/src/Avatar.recipe.ts +191 -0
  26. package/src/Avatar.tsx +278 -0
  27. package/src/Breadcrumb.recipe.ts +45 -0
  28. package/src/Breadcrumb.tsx +114 -0
  29. package/src/Button.recipe.ts +88 -50
  30. package/src/Button.tsx +67 -14
  31. package/src/ButtonGroup.tsx +37 -15
  32. package/src/Card.recipe.ts +1 -2
  33. package/src/Card.tsx +2 -1
  34. package/src/Checkbox.recipe.ts +49 -14
  35. package/src/Checkbox.tsx +111 -32
  36. package/src/CheckboxGroup.tsx +70 -0
  37. package/src/CloseButton.tsx +3 -3
  38. package/src/CloseIcon.tsx +6 -4
  39. package/src/Code.tsx +1 -1
  40. package/src/Collapse.tsx +13 -14
  41. package/src/ComboBox.tsx +246 -0
  42. package/src/Divider.tsx +40 -7
  43. package/src/Drawer.recipe.ts +21 -10
  44. package/src/Drawer.tsx +3 -4
  45. package/src/ExternalLink.tsx +43 -0
  46. package/src/Fade.tsx +18 -4
  47. package/src/Field.recipe.ts +114 -0
  48. package/src/Field.tsx +187 -0
  49. package/src/GridList.recipe.ts +57 -0
  50. package/src/GridList.tsx +81 -0
  51. package/src/Heading.recipe.ts +20 -1
  52. package/src/Heading.tsx +3 -3
  53. package/src/Icon.tsx +23 -7
  54. package/src/IconButton.tsx +8 -18
  55. package/src/Image.tsx +1 -1
  56. package/src/Input.recipe.ts +41 -28
  57. package/src/Input.tsx +23 -7
  58. package/src/InputGroup.tsx +26 -12
  59. package/src/Kbd.tsx +1 -1
  60. package/src/Link.tsx +3 -3
  61. package/src/LinkBox.tsx +2 -3
  62. package/src/LinkButton.tsx +14 -13
  63. package/src/List.tsx +5 -7
  64. package/src/ListBox.recipe.ts +43 -0
  65. package/src/ListBox.tsx +88 -0
  66. package/src/Menu.recipe.ts +22 -19
  67. package/src/Menu.tsx +54 -28
  68. package/src/Modal.recipe.ts +17 -9
  69. package/src/Modal.tsx +123 -24
  70. package/src/MoreMenuButton.tsx +63 -0
  71. package/src/NativeSelect.tsx +41 -14
  72. package/src/NativeSelectField.tsx +84 -0
  73. package/src/NumberField.recipe.ts +56 -15
  74. package/src/NumberField.tsx +74 -22
  75. package/src/PopoverArrow.tsx +1 -2
  76. package/src/ProgressBar.tsx +3 -5
  77. package/src/Radio.recipe.ts +108 -0
  78. package/src/Radio.tsx +62 -0
  79. package/src/RadioGroup.tsx +68 -0
  80. package/src/Select.recipe.ts +211 -0
  81. package/src/Select.tsx +194 -0
  82. package/src/SharedUIProvider.tsx +63 -7
  83. package/src/Skeleton.tsx +146 -0
  84. package/src/Slide.tsx +2 -2
  85. package/src/Slider.recipe.ts +27 -16
  86. package/src/Slider.tsx +20 -12
  87. package/src/Spinner.tsx +11 -9
  88. package/src/Svg.tsx +2 -3
  89. package/src/Switch.recipe.ts +57 -11
  90. package/src/Switch.tsx +39 -7
  91. package/src/Text.recipe.ts +26 -0
  92. package/src/Text.tsx +6 -2
  93. package/src/TextField.tsx +39 -40
  94. package/src/Toast.recipe.ts +52 -15
  95. package/src/Toast.tsx +147 -48
  96. package/src/Tooltip.recipe.ts +32 -0
  97. package/src/Tooltip.tsx +68 -28
  98. package/src/TooltipButton.tsx +229 -0
  99. package/src/UnmountCallback.tsx +2 -2
  100. package/src/VisuallyHidden.tsx +1 -1
  101. package/src/base-preset.ts +329 -84
  102. package/src/base-tokens.ts +0 -3
  103. package/src/button-icon.ts +4 -5
  104. package/src/data-attrs.ts +16 -0
  105. package/src/dense-preset.ts +107 -0
  106. package/src/hooks/useBreakpointValue.ts +4 -4
  107. package/src/hooks/useClipboard.ts +43 -7
  108. package/src/hooks/useDisclosure.ts +31 -0
  109. package/src/hooks/useMediaQuery.ts +3 -4
  110. package/src/hooks/usePrevious.ts +1 -4
  111. package/src/index.ts +20 -3
  112. package/src/rac-locale.ts +33 -0
  113. package/src/system.ts +22 -1
  114. package/src/TextField.recipe.ts +0 -54
package/src/Toast.tsx CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { ReactNode, useMemo } from "react";
6
+ import { ReactNode, useMemo, useSyncExternalStore } from "react";
7
7
  import {
8
8
  Button as RACButton,
9
9
  UNSTABLE_Toast as RACToast,
@@ -23,12 +23,12 @@ import { toast as toastRecipe } from "styled-system/recipes";
23
23
  import { CloseIcon } from "./CloseIcon";
24
24
  import { Icon } from "./Icon";
25
25
  import { uiMessage } from "./messages";
26
- import { VisuallyHidden } from "./VisuallyHidden";
26
+ import { darkSurface } from "./system";
27
27
 
28
28
  export type ToastStatus = "info" | "success" | "warning" | "error";
29
29
 
30
30
  export interface ToastContent {
31
- /** Dedup key: adding a toast whose id is already visible is a no-op. */
31
+ /** Dedup key: adding a toast whose id is already queued is a no-op. */
32
32
  id?: string;
33
33
  title?: ReactNode;
34
34
  description?: ReactNode;
@@ -36,18 +36,68 @@ export interface ToastContent {
36
36
  isClosable?: boolean;
37
37
  }
38
38
 
39
+ // Enter/exit/reflow animation: queue updates run inside a view transition
40
+ // (react-aria's supported mechanism — toasts unmount synchronously, so CSS
41
+ // transitions on the element can't animate the exit). The keyframes and the
42
+ // ::view-transition rules live in base-preset.ts, scoped by this class,
43
+ // which marks the transition as toast-initiated while it runs: the rules
44
+ // select entering/exiting groups with `(*)`, avoiding view-transition-class
45
+ // (needs Safari 18.2/Chrome 125 vs 18.0/111 for the API itself), and the
46
+ // scoping keeps them — and the pointer-events override — away from any view
47
+ // transitions the app runs. Browsers without the API and reduced-motion
48
+ // users get the bare update.
49
+ const TRANSITION_CLASS = "microbit-ui-toast-transition";
50
+ let activeTransitions = 0;
51
+ const wrapUpdate = (fn: () => void) => {
52
+ if (
53
+ typeof document !== "undefined" &&
54
+ document.startViewTransition &&
55
+ !window.matchMedia("(prefers-reduced-motion: reduce)").matches
56
+ ) {
57
+ activeTransitions++;
58
+ document.documentElement.classList.add(TRANSITION_CLASS);
59
+ const transition = document.startViewTransition(fn);
60
+ // A transition is routinely skipped — another update starts one while
61
+ // this is still animating, or the tab is hidden — and the browser then
62
+ // rejects its promises with "AbortError: Transition was skipped". The
63
+ // DOM update has still run; only the animation is dropped. Settle on
64
+ // rejection too (`.finally()` would re-throw, surfacing routine skips
65
+ // as unhandled rejections in error reporting) and swallow `ready`,
66
+ // which some engines reject without marking handled.
67
+ transition.ready.catch(() => {});
68
+ const settle = () => {
69
+ if (--activeTransitions === 0) {
70
+ document.documentElement.classList.remove(TRANSITION_CLASS);
71
+ }
72
+ };
73
+ transition.finished.then(settle, settle);
74
+ } else {
75
+ fn();
76
+ }
77
+ };
78
+
39
79
  // Module-level queue shared by useToast() and the <ToastProvider/> region.
40
80
  // (RAC's Toast API is still flagged UNSTABLE_*; the surface is small and behind
41
81
  // this module, so a swap to a custom queue later is contained.)
42
82
  export const toastQueue = new RACToastQueue<ToastContent>({
43
83
  maxVisibleToasts: 5,
84
+ wrapUpdate,
44
85
  });
45
86
 
46
- // Status icon matching Chakra's AlertIcon (filled glyphs, coloured by the
47
- // toast foreground = white here). Warning is a triangle, error a circle, as
48
- // in Chakra the glyph must distinguish them because the colours alone
49
- // don't reliably. The icon is decorative; the status is announced via the
50
- // visually hidden status text.
87
+ // Index of our ids to the queue's own keys. The queue only exposes its
88
+ // visible slice the newest `maxVisibleToasts` so ids can't be resolved by
89
+ // scanning it: once newer toasts arrive an older one is still queued but out
90
+ // of sight, and dedup would let a second copy through while update() added
91
+ // rather than replaced. react-aria's per-toast `onClose` keeps this honest
92
+ // however a toast goes (timeout, close button, or update). clear() doesn't
93
+ // call onClose, so closeAll empties both.
94
+ const keysById = new Map<string, string>();
95
+
96
+ // Status icon (filled glyphs, coloured by the toast foreground = white
97
+ // here). Warning is a triangle, error a circle — the glyph must distinguish
98
+ // them because the colours alone don't reliably. The glyph is also what
99
+ // carries the status for assistive tech, via its accessible name (see
100
+ // ToastProvider).
51
101
  const statusIcon: Record<ToastStatus, IconType> = {
52
102
  info: RiInformationFill,
53
103
  success: RiCheckboxCircleFill,
@@ -55,6 +105,10 @@ const statusIcon: Record<ToastStatus, IconType> = {
55
105
  error: RiErrorWarningFill,
56
106
  };
57
107
 
108
+ // Module scope so useSyncExternalStore doesn't resubscribe every render.
109
+ const subscribeToQueue = (fn: () => void) => toastQueue.subscribe(fn);
110
+ const getVisibleCount = () => toastQueue.visibleToasts.length;
111
+
58
112
  /**
59
113
  * Mount once near the app root, inside the IntlProvider (the close button's
60
114
  * label and the status announcements are react-intl messages).
@@ -63,32 +117,57 @@ const statusIcon: Record<ToastStatus, IconType> = {
63
117
  export const ToastProvider = () => {
64
118
  const intl = useIntl();
65
119
  const slots = toastRecipe();
120
+ // The region's landmark label counts the visible toasts, so it has to
121
+ // track the queue.
122
+ const count = useSyncExternalStore(
123
+ subscribeToQueue,
124
+ getVisibleCount,
125
+ getVisibleCount,
126
+ );
66
127
  return (
67
- <RACToastRegion queue={toastQueue} className={slots.region}>
128
+ <RACToastRegion
129
+ queue={toastQueue}
130
+ aria-label={intl.formatMessage(uiMessage("ui.toast-region"), { count })}
131
+ className={slots.region}
132
+ >
68
133
  {({ toast }) => {
69
134
  const status = toast.content.status ?? "info";
70
135
  return (
71
- <RACToast toast={toast} className={toastRecipe({ status }).root}>
72
- <Icon as={statusIcon[status]} className={slots.icon} aria-hidden />
73
- <RACToastContent>
74
- {/* Colour and icon are the only visible status signals; say it
75
- for assistive tech too. */}
76
- <VisuallyHidden>
77
- {intl.formatMessage(uiMessage(`ui.toast-status-${status}`))}{" "}
78
- </VisuallyHidden>
79
- {toast.content.title && (
80
- <p className={slots.title}>{toast.content.title}</p>
81
- )}
82
- {toast.content.description && (
83
- <div className={slots.description}>
84
- {toast.content.description}
85
- </div>
86
- )}
136
+ <RACToast
137
+ toast={toast}
138
+ className={toastRecipe({ status }).root}
139
+ // A unique view-transition-name per toast creates its snapshot
140
+ // group and lets old/new pair up across the transition.
141
+ style={{ viewTransitionName: toast.key }}
142
+ >
143
+ <RACToastContent className={slots.content}>
144
+ <Icon
145
+ as={statusIcon[status]}
146
+ className={slots.icon}
147
+ aria-label={intl.formatMessage(
148
+ uiMessage(`ui.toast-status-${status}`),
149
+ )}
150
+ />
151
+ <div className={slots.body}>
152
+ {toast.content.title && (
153
+ <p className={slots.title}>{toast.content.title}</p>
154
+ )}
155
+ {toast.content.description && (
156
+ <div className={slots.description}>
157
+ {toast.content.description}
158
+ </div>
159
+ )}
160
+ </div>
87
161
  </RACToastContent>
88
162
  {toast.content.isClosable && (
89
163
  <RACButton
90
164
  slot="close"
91
165
  aria-label={intl.formatMessage(uiMessage("ui.close-action"))}
166
+ // The card is dark at every status, and no app can tag a
167
+ // surface the package ships. Tagged here, not on the card:
168
+ // the tag covers the element it sits on, whose own ring is
169
+ // drawn outside it, on the page.
170
+ {...darkSurface}
92
171
  className={slots.closeButton}
93
172
  >
94
173
  <CloseIcon />
@@ -102,34 +181,39 @@ export const ToastProvider = () => {
102
181
  };
103
182
 
104
183
  export interface ToastOptions extends ToastContent {
184
+ /** Auto-dismiss after this many ms. Default 5000. Ignored when `persistent`. */
185
+ duration?: number;
105
186
  /**
106
- * Auto-dismiss after ms; null means no auto-dismiss (Chakra's semantics).
107
- * RAC enforces a 5000ms minimum for accessibility.
187
+ * Never auto-dismiss. The close button is forced on so the toast is not
188
+ * permanent and unremovable.
108
189
  */
109
- duration?: number | null;
190
+ persistent?: boolean;
110
191
  }
111
192
 
112
193
  export interface ToastFn {
113
194
  (options: ToastOptions): void;
114
- /** Whether a toast with this id is currently visible. */
195
+ /**
196
+ * Whether a toast with this id is still queued — displayed, or waiting
197
+ * behind newer toasts for its turn.
198
+ */
115
199
  isActive(id: string): boolean;
116
200
  /**
117
- * Replace a visible toast's content (Chakra's toast.update). The toast is
118
- * re-added, so unlike Chakra it re-animates and restarts any timeout.
201
+ * Replace a queued toast's content. The toast is re-added: it re-animates,
202
+ * restarts any timeout, and takes its place at the front of the queue.
119
203
  */
120
204
  update(id: string, options: ToastOptions): void;
121
- /** Dismiss all visible toasts (Chakra's toast.closeAll). */
205
+ /** Dismiss every toast, queued as well as displayed. */
122
206
  closeAll(): void;
123
207
  }
124
208
 
125
209
  /**
126
- * useToast — imperative toast trigger matching the shape of Chakra's
127
- * `useToast()` call sites: `toast({ title, description, status, duration })`.
210
+ * useToast — imperative toast trigger:
211
+ * `toast({ title, description, status, duration })`. There is no
212
+ * `duration: null`; use `persistent: true` instead.
128
213
  */
129
214
  export const useToast = (): ToastFn =>
130
215
  useMemo(() => {
131
- const isActive = (id: string) =>
132
- toastQueue.visibleToasts.some((t) => t.content.id === id);
216
+ const isActive = (id: string) => keysById.has(id);
133
217
  const add = ({
134
218
  id,
135
219
  title,
@@ -137,35 +221,50 @@ export const useToast = (): ToastFn =>
137
221
  status,
138
222
  isClosable,
139
223
  duration,
224
+ persistent,
140
225
  }: ToastOptions) => {
141
226
  if (id && isActive(id)) {
142
227
  return;
143
228
  }
144
- toastQueue.add(
229
+ const key = toastQueue.add(
145
230
  {
146
231
  id,
147
232
  title,
148
233
  description,
149
234
  status,
150
- // A toast that never times out must be dismissable or it is
151
- // permanent and unremovable; force the close button on.
152
- isClosable: isClosable || duration == null,
235
+ isClosable: isClosable || persistent,
236
+ },
237
+ {
238
+ timeout: persistent ? undefined : duration ?? 5000,
239
+ onClose: id
240
+ ? // Only our own entry is ours to drop: an id reused after this
241
+ // toast closed belongs to the later add.
242
+ () => {
243
+ if (keysById.get(id) === key) {
244
+ keysById.delete(id);
245
+ }
246
+ }
247
+ : undefined,
153
248
  },
154
- { timeout: duration ?? undefined },
155
249
  );
250
+ if (id) {
251
+ keysById.set(id, key);
252
+ }
156
253
  };
157
254
  const update = (id: string, options: ToastOptions) => {
158
- const existing = toastQueue.visibleToasts.find(
159
- (t) => t.content.id === id,
160
- );
161
- if (existing) {
162
- toastQueue.close(existing.key);
255
+ const key = keysById.get(id);
256
+ if (key !== undefined) {
257
+ toastQueue.close(key);
163
258
  }
164
259
  add({ ...options, id });
165
260
  };
261
+ // clear() empties the whole queue, including the toasts held back by
262
+ // maxVisibleToasts. Closing the visible ones one by one would only
263
+ // promote the queued ones into view. It's also a single update, so the
264
+ // whole set exits in one view transition.
166
265
  const closeAll = () => {
167
- // Copy first: closing mutates visibleToasts as we iterate.
168
- [...toastQueue.visibleToasts].forEach((t) => toastQueue.close(t.key));
266
+ keysById.clear();
267
+ toastQueue.clear();
169
268
  };
170
269
  return Object.assign(add, { isActive, update, closeAll });
171
270
  }, []);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { defineRecipe } from "@pandacss/dev";
7
+
8
+ /**
9
+ * Tooltip recipe — the dark tooltip.
10
+ *
11
+ * A recipe rather than styles inside the component because tooltip typography
12
+ * is the kind of thing an app sets once for all of them (classroom does,
13
+ * `fontSize: md`), and a `css` override at today's call sites would quietly
14
+ * not apply to tomorrow's.
15
+ *
16
+ * Registered in the base preset (base-preset.ts).
17
+ */
18
+ export const tooltip = defineRecipe({
19
+ className: "tooltip",
20
+ base: {
21
+ bg: "gray.700",
22
+ color: "whiteAlpha.900",
23
+ px: "2",
24
+ py: "0.5",
25
+ borderRadius: "sm",
26
+ fontSize: "sm",
27
+ fontWeight: "medium",
28
+ boxShadow: "md",
29
+ maxW: "xs",
30
+ zIndex: "tooltip",
31
+ },
32
+ });
package/src/Tooltip.tsx CHANGED
@@ -5,44 +5,46 @@
5
5
  */
6
6
  import { ReactElement, ReactNode, RefObject } from "react";
7
7
  import { Tooltip as RACTooltip, TooltipTrigger } from "react-aria-components";
8
- import { css } from "styled-system/css";
8
+ import { css, cx } from "styled-system/css";
9
+ import { tooltip } from "styled-system/recipes";
9
10
  import { SystemStyleObject } from "styled-system/types";
10
11
  import { PopoverArrow } from "./PopoverArrow";
11
12
 
12
- // Base as an object (not a precomputed class) so a caller's `css` override is
13
- // merged into a single css() call — Panda then dedupes conflicting utilities
14
- // (e.g. px/py) so overrides actually win.
15
- const tooltipBase: SystemStyleObject = {
16
- bg: "gray.700",
17
- color: "white",
18
- px: "2",
19
- py: "1",
20
- borderRadius: "md",
21
- fontSize: "sm",
22
- fontWeight: "medium",
23
- boxShadow: "md",
24
- maxW: "xs",
25
- zIndex: "tooltip",
26
- };
27
-
28
13
  export interface TooltipProps {
29
14
  /**
30
- * Tooltip body (Chakra's `label`). Not named `content`: Panda extracts
31
- * utility-named props with literal values from any JSX component, so a
32
- * `content` prop taking a string would emit a broken CSS `content` rule.
15
+ * Tooltip body. Not named `content`: Panda extracts utility-named props
16
+ * with literal values from any JSX component, so a `content` prop taking a
17
+ * string would emit a broken CSS `content` rule.
33
18
  */
34
19
  label: ReactNode;
35
20
  /** A single focusable trigger element (e.g. a Button). */
36
21
  children: ReactElement;
22
+ /**
23
+ * Which side of the trigger the tooltip prefers. A preference, not a
24
+ * promise: react-aria flips it at runtime when the tooltip would leave the
25
+ * viewport, so pick the side that reads right rather than one that dodges
26
+ * an edge.
27
+ *
28
+ * `top`/`bottom` are direction-neutral and suit most tooltips.
29
+ * `start`/`end` follow reading order and mirror in RTL. `left`/`right` are
30
+ * screen sides and never mirror — reach for them only when the trigger is
31
+ * pinned to a physical edge.
32
+ */
37
33
  placement?:
38
34
  | "top"
39
35
  | "bottom"
36
+ | "start"
37
+ | "end"
40
38
  | "left"
41
39
  | "right"
42
40
  | "top start"
43
41
  | "top end"
44
42
  | "bottom start"
45
43
  | "bottom end"
44
+ | "start top"
45
+ | "start bottom"
46
+ | "end top"
47
+ | "end bottom"
46
48
  | "left top"
47
49
  | "left bottom"
48
50
  | "right top"
@@ -55,16 +57,47 @@ export interface TooltipProps {
55
57
  * the trigger (i.e. it is not a RAC component or `Focusable`).
56
58
  */
57
59
  triggerRef?: RefObject<HTMLElement | null>;
58
- /** Hover open delay in ms (RAC default ~1500; pass 0 for instant). */
60
+ /**
61
+ * Hover open delay in ms, defaulting to react-aria's 1500.
62
+ *
63
+ * The delay is per bout of interest, not per control: react-aria keeps a
64
+ * global "warm" flag, so the first tooltip waits and every one after it opens
65
+ * instantly until half a second or so after the last one closes. That is what
66
+ * keeps a row of buttons from firing tooltips at a pointer merely crossing
67
+ * them.
68
+ *
69
+ * **Pass 0 where the tooltip is the label** — an icon-only button, where the
70
+ * text is the only explanation of the glyph and waiting for it reads as
71
+ * broken. Leave it alone where the control already says what it is and the
72
+ * tooltip adds detail.
73
+ */
59
74
  delay?: number;
75
+ /**
76
+ * Close delay in ms, defaulting to react-aria's 500.
77
+ *
78
+ * This is what makes a tooltip hoverable, as WCAG 1.4.13 asks: react-aria
79
+ * puts hover handlers on the tooltip that re-open it, but with an immediate
80
+ * close it has unmounted before the pointer can cross the gap. Pass 0 where
81
+ * the delay is wrong for a particular control.
82
+ */
83
+ closeDelay?: number;
84
+ /**
85
+ * Whether pressing the trigger closes the tooltip (RAC default true).
86
+ *
87
+ * react-aria binds this to keydown as well as pointerdown, so with the
88
+ * default *any* key press dismisses the tooltip and only hover or focus
89
+ * brings it back. Pass false where the tooltip's text is the point of the
90
+ * control rather than a hint about an action.
91
+ */
92
+ shouldCloseOnPress?: boolean;
60
93
  css?: SystemStyleObject;
61
94
  }
62
95
 
63
96
  /**
64
- * Tooltip — react-aria-components TooltipTrigger + Tooltip, styled to match
65
- * Chakra's dark tooltip. The child must be a focusable element so the tooltip
66
- * is reachable by keyboard (RAC requirement), unless `triggerRef` provides
67
- * the anchor and the caller manages open state and keyboard access itself.
97
+ * Tooltip — react-aria-components TooltipTrigger + Tooltip in the dark
98
+ * tooltip style. The child must be a focusable element so the tooltip is
99
+ * reachable by keyboard (RAC requirement), unless `triggerRef` provides the
100
+ * anchor and the caller manages open state and keyboard access itself.
68
101
  */
69
102
  export const Tooltip = ({
70
103
  label,
@@ -73,16 +106,23 @@ export const Tooltip = ({
73
106
  hasArrow,
74
107
  isOpen,
75
108
  triggerRef,
76
- delay = 0,
109
+ delay,
110
+ closeDelay,
111
+ shouldCloseOnPress,
77
112
  css: cssProp,
78
113
  }: TooltipProps) => (
79
- <TooltipTrigger isOpen={isOpen} delay={delay} closeDelay={0}>
114
+ <TooltipTrigger
115
+ isOpen={isOpen}
116
+ delay={delay}
117
+ closeDelay={closeDelay}
118
+ shouldCloseOnPress={shouldCloseOnPress}
119
+ >
80
120
  {children}
81
121
  <RACTooltip
82
122
  triggerRef={triggerRef}
83
123
  placement={placement}
84
124
  offset={hasArrow ? 8 : 4}
85
- className={css({ ...tooltipBase, ...cssProp })}
125
+ className={cx(tooltip(), cssProp ? css(cssProp) : undefined)}
86
126
  >
87
127
  {hasArrow && <PopoverArrow css={{ "& svg": { fill: "gray.700" } }} />}
88
128
  {label}