@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.30

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 +40 -0
  2. package/README.md +256 -26
  3. package/lang/ui.ca.json +43 -3
  4. package/lang/ui.cy.json +62 -0
  5. package/lang/ui.de.json +62 -0
  6. package/lang/ui.en-us.json +40 -0
  7. package/lang/ui.en.json +40 -0
  8. package/lang/ui.es-es.json +43 -3
  9. package/lang/ui.fr.json +44 -4
  10. package/lang/ui.ga-ie.json +62 -0
  11. package/lang/ui.it.json +62 -0
  12. package/lang/ui.ja.json +44 -4
  13. package/lang/ui.ko.json +44 -4
  14. package/lang/ui.lo.json +62 -0
  15. package/lang/ui.lol.json +40 -0
  16. package/lang/ui.nl.json +44 -4
  17. package/lang/ui.pl.json +44 -4
  18. package/lang/ui.pt-br.json +44 -4
  19. package/lang/ui.vi.json +62 -0
  20. package/lang/ui.zh-cn.json +62 -0
  21. package/lang/ui.zh-tw.json +44 -4
  22. package/package.json +19 -10
  23. package/postcss-legacy-safari.cjs +96 -0
  24. package/reset.css +35 -0
  25. package/src/Avatar.recipe.ts +166 -0
  26. package/src/Avatar.tsx +278 -0
  27. package/src/Breadcrumb.recipe.ts +39 -0
  28. package/src/Breadcrumb.tsx +114 -0
  29. package/src/Button.recipe.ts +88 -50
  30. package/src/Button.tsx +64 -26
  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 +20 -0
  40. package/src/Collapse.tsx +179 -0
  41. package/src/ComboBox.tsx +246 -0
  42. package/src/Divider.tsx +72 -8
  43. package/src/Drawer.recipe.ts +4 -5
  44. package/src/Drawer.tsx +3 -4
  45. package/src/ExternalLink.tsx +43 -0
  46. package/src/Fade.tsx +62 -0
  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 +7 -12
  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 +21 -8
  59. package/src/Kbd.tsx +26 -0
  60. package/src/Link.tsx +3 -3
  61. package/src/LinkBox.tsx +2 -3
  62. package/src/LinkButton.tsx +81 -0
  63. package/src/List.tsx +8 -6
  64. package/src/ListBox.recipe.ts +43 -0
  65. package/src/ListBox.tsx +88 -0
  66. package/src/Menu.recipe.ts +51 -17
  67. package/src/Menu.tsx +117 -2
  68. package/src/Modal.recipe.ts +14 -9
  69. package/src/Modal.tsx +137 -24
  70. package/src/MoreMenuButton.tsx +63 -0
  71. package/src/NativeSelect.tsx +39 -12
  72. package/src/NativeSelectField.tsx +84 -0
  73. package/src/NumberField.recipe.ts +108 -0
  74. package/src/NumberField.tsx +138 -0
  75. package/src/PopoverArrow.tsx +19 -5
  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 +51 -7
  83. package/src/Skeleton.tsx +146 -0
  84. package/src/Slide.tsx +2 -2
  85. package/src/Slider.recipe.ts +24 -15
  86. package/src/Slider.tsx +65 -5
  87. package/src/Spinner.tsx +11 -9
  88. package/src/Svg.tsx +2 -3
  89. package/src/Switch.recipe.ts +58 -13
  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 +54 -40
  94. package/src/Toast.recipe.ts +49 -12
  95. package/src/Toast.tsx +151 -46
  96. package/src/Tooltip.recipe.ts +32 -0
  97. package/src/Tooltip.tsx +51 -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 +336 -84
  102. package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
  103. package/src/button-icon.ts +22 -0
  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 +63 -0
  108. package/src/hooks/useDisclosure.ts +31 -0
  109. package/src/hooks/useMediaQuery.ts +27 -0
  110. package/src/hooks/usePrevious.ts +15 -0
  111. package/src/index.ts +29 -3
  112. package/src/rac-locale.ts +33 -0
  113. package/src/system.ts +22 -1
  114. package/src/TextField.recipe.ts +0 -54
@@ -0,0 +1,229 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ ReactNode,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useId,
12
+ useRef,
13
+ } from "react";
14
+ import { TooltipTriggerStateContext } from "react-aria-components";
15
+ import { css } from "styled-system/css";
16
+ import { SystemStyleObject } from "styled-system/types";
17
+ import { Button } from "./Button";
18
+ import { Tooltip, TooltipProps } from "./Tooltip";
19
+ import { VisuallyHidden } from "./VisuallyHidden";
20
+
21
+ // A tooltip whose text *is* the point of the control — an information affordance
22
+ // beside a heading, say — rather than a hint about what a button does. That
23
+ // difference drives everything here, because react-aria's tooltips are built for
24
+ // the second case:
25
+ //
26
+ // - They never open on press, since a tooltip isn't a touch pattern. Sighted
27
+ // touch users would have no way in, so this toggles on press.
28
+ // - Their text is associated with the trigger only while open, so touch screen
29
+ // readers (iPadOS VoiceOver, TalkBack) never reach it. The same text is
30
+ // therefore always present on a visually hidden node, named or described from
31
+ // the button. The visible tooltip is aria-hidden to avoid double announcement.
32
+ // - Any key press dismisses them (see Tooltip's shouldCloseOnPress), which for
33
+ // this pattern means a keyboard user can dismiss but never re-open.
34
+ //
35
+ // Everything else is left to react-aria: it opens on hover and on keyboard
36
+ // focus, closes on Escape without disturbing a surrounding dialog, and keeps
37
+ // only one tooltip open at a time across the whole document. Hovering the
38
+ // tooltip to keep it open relies on Tooltip's non-zero close delay, so don't
39
+ // pass `closeDelay={0}` through to it.
40
+
41
+ // How far outside the tooltip the pointer still counts as on it, covering the
42
+ // trigger/tooltip gap and the arrow.
43
+ const pointerMarginPx = 12;
44
+
45
+ const triggerStyle: SystemStyleObject = {
46
+ // The button recipe's size variants set a height and horizontal padding for
47
+ // text buttons; shrink to the glyph so the focus ring is an even circle
48
+ // around it and the control doesn't stretch its row.
49
+ display: "inline-flex",
50
+ alignItems: "center",
51
+ justifyContent: "center",
52
+ height: "auto",
53
+ minHeight: "0",
54
+ minWidth: "0",
55
+ padding: "0",
56
+ lineHeight: "1",
57
+ cursor: "pointer",
58
+ borderRadius: "50%",
59
+ _focusVisible: { focusRing: "outline" },
60
+ };
61
+
62
+ export interface TooltipButtonProps {
63
+ /**
64
+ * Tooltip body. Also the button's accessible name, or its description when
65
+ * `aria-label` is given.
66
+ */
67
+ label: ReactNode;
68
+ /** Button content, typically an `Icon`. */
69
+ children: ReactNode;
70
+ /**
71
+ * Short accessible name for the button, e.g. "Live graph". Recommended when
72
+ * `label` runs to more than a few words: without it the whole body becomes
73
+ * the button's name, which a screen reader reads out in full.
74
+ */
75
+ "aria-label"?: string;
76
+ placement?: TooltipProps["placement"];
77
+ hasArrow?: boolean;
78
+ /** Style overrides for the tooltip, e.g. padding for a multi-line body. */
79
+ css?: SystemStyleObject;
80
+ /** Style overrides for the button. */
81
+ triggerCss?: SystemStyleObject;
82
+ }
83
+
84
+ /**
85
+ * TooltipButton — a small button, usually an icon, whose tooltip carries
86
+ * information the user needs rather than a hint about an action.
87
+ *
88
+ * Unlike a bare `Tooltip` it works by pointer, keyboard and touch, and its text
89
+ * reaches screen readers on every platform. Use it for an information icon
90
+ * beside a heading or a "partially supported" marker; use `Tooltip` for a hint
91
+ * on a button that does something else.
92
+ *
93
+ * Open question: react-spectrum makes this pattern a popover
94
+ * (`ContextualHelp`), not a tooltip, which would remove the hidden copy of the
95
+ * body and the pointer-geometry keep-alive below rather than work around them.
96
+ * Tracked as microbit-foundation/ui#63, which would deprecate this component;
97
+ * prefer that direction over extending it.
98
+ */
99
+ export const TooltipButton = ({
100
+ label,
101
+ children,
102
+ "aria-label": ariaLabel,
103
+ placement,
104
+ hasArrow,
105
+ css: cssProp,
106
+ triggerCss,
107
+ }: TooltipButtonProps) => {
108
+ const textId = useId();
109
+ const tooltipBodyId = useId();
110
+ return (
111
+ <Tooltip
112
+ label={
113
+ <div id={tooltipBodyId} aria-hidden={true}>
114
+ {label}
115
+ </div>
116
+ }
117
+ placement={placement}
118
+ hasArrow={hasArrow}
119
+ css={cssProp}
120
+ // The tooltip is this button's whole explanation — an icon with a 1.5s
121
+ // wait before anything appears reads as broken — so opt out of the warmup
122
+ // the labelled controls want.
123
+ delay={0}
124
+ shouldCloseOnPress={false}
125
+ >
126
+ <span className={css({ display: "flex" })}>
127
+ <TooltipButtonTrigger
128
+ aria-label={ariaLabel}
129
+ textId={textId}
130
+ tooltipBodyId={tooltipBodyId}
131
+ css={triggerCss}
132
+ >
133
+ {children}
134
+ </TooltipButtonTrigger>
135
+ <VisuallyHidden as="div" id={textId} aria-hidden={true}>
136
+ {label}
137
+ </VisuallyHidden>
138
+ </span>
139
+ </Tooltip>
140
+ );
141
+ };
142
+
143
+ interface TooltipButtonTriggerProps {
144
+ children: ReactNode;
145
+ "aria-label"?: string;
146
+ /** Visually hidden copy of the body, naming or describing the button. */
147
+ textId: string;
148
+ /** The body inside the visible tooltip, used to find it in the document. */
149
+ tooltipBodyId: string;
150
+ css?: SystemStyleObject;
151
+ }
152
+
153
+ /**
154
+ * The button itself, split out so it can read the tooltip's state from context.
155
+ * Being a RAC component it registers itself as the tooltip's trigger — hover,
156
+ * focus and positioning all follow from that, even nested inside the span.
157
+ */
158
+ const TooltipButtonTrigger = ({
159
+ children,
160
+ "aria-label": ariaLabel,
161
+ textId,
162
+ tooltipBodyId,
163
+ css: cssProp,
164
+ }: TooltipButtonTriggerProps) => {
165
+ const state = useContext(TooltipTriggerStateContext);
166
+ const ref = useRef<HTMLButtonElement>(null);
167
+ const handlePress = useCallback(() => {
168
+ if (state?.isOpen) {
169
+ state.close(true);
170
+ } else {
171
+ state?.open(true);
172
+ }
173
+ }, [state]);
174
+ // Hovering the tooltip keeps it open, so it can be read at magnification
175
+ // (WCAG 1.4.13). react-aria does that by re-opening on hover, which fails
176
+ // when the tooltip is portalled into a container a modal has marked inert:
177
+ // it is painted but can never be the target of a mouse event. Pointer
178
+ // geometry works either way — open() clears the pending close.
179
+ //
180
+ // Leaving the tooltip has to close it here too. The trigger's own hover-end
181
+ // fired long ago, when the pointer set off across the gap, so nothing else
182
+ // will. Not while the trigger is hovered or focused, though: those are
183
+ // react-aria's own reasons to be open, and it will close on its own terms.
184
+ const isOpen = state?.isOpen;
185
+ useEffect(() => {
186
+ if (!isOpen) {
187
+ return;
188
+ }
189
+ const listener = (e: MouseEvent) => {
190
+ const rect = document
191
+ .getElementById(tooltipBodyId)
192
+ ?.closest('[role="tooltip"]')
193
+ ?.getBoundingClientRect();
194
+ const onTooltip =
195
+ !!rect &&
196
+ e.clientX >= rect.left - pointerMarginPx &&
197
+ e.clientX <= rect.right + pointerMarginPx &&
198
+ e.clientY >= rect.top - pointerMarginPx &&
199
+ e.clientY <= rect.bottom + pointerMarginPx;
200
+ if (onTooltip) {
201
+ state?.open(true);
202
+ } else if (
203
+ ref.current !== document.activeElement &&
204
+ !ref.current?.matches(":hover")
205
+ ) {
206
+ state?.close();
207
+ }
208
+ };
209
+ document.addEventListener("mousemove", listener);
210
+ return () => document.removeEventListener("mousemove", listener);
211
+ }, [isOpen, state, tooltipBodyId]);
212
+ return (
213
+ <Button
214
+ ref={ref}
215
+ variant="unstyled"
216
+ aria-label={ariaLabel}
217
+ // Without a short name the body is the name; with one it is the
218
+ // description. react-aria overwrites aria-describedby with the visible
219
+ // tooltip's id while open, and that copy is aria-hidden so announces
220
+ // nothing; closed — the state a touch screen reader is in — this applies.
221
+ aria-labelledby={ariaLabel ? undefined : textId}
222
+ aria-describedby={ariaLabel ? textId : undefined}
223
+ onPress={handlePress}
224
+ css={{ ...triggerStyle, ...cssProp }}
225
+ >
226
+ {children}
227
+ </Button>
228
+ );
229
+ };
@@ -7,8 +7,8 @@ import { useEffect, useRef } from "react";
7
7
 
8
8
  /**
9
9
  * Runs its callback when unmounted. RAC keeps an overlay tree mounted until
10
- * the exit transition finishes, so mounting this inside a Modal/Drawer gives
11
- * the equivalent of Chakra's `onCloseComplete`. Internal to shared-ui.
10
+ * the exit transition finishes, so mounting this inside the overlay
11
+ * implements Modal/Drawer's `onCloseComplete`. Internal to shared-ui.
12
12
  */
13
13
  export const UnmountCallback = ({ callback }: { callback?: () => void }) => {
14
14
  const ref = useRef(callback);
@@ -7,7 +7,7 @@ import { styled } from "styled-system/jsx";
7
7
 
8
8
  /**
9
9
  * VisuallyHidden — screen-reader-only content (Panda's `srOnly` utility).
10
- * Renders a span like Chakra's; use `as="div"` for block children.
10
+ * Renders a span; use `as="div"` for block children.
11
11
  */
12
12
  export const VisuallyHidden = styled("span", {
13
13
  base: { srOnly: true },