@microbit/ui 0.1.0-alpha.9 → 0.1.1
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.
- package/LICENSE.md +8 -0
- package/README.md +171 -20
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +44 -4
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +43 -3
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +44 -4
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +13 -11
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +67 -14
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +13 -12
- package/src/Checkbox.tsx +103 -34
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +1 -1
- package/src/Collapse.tsx +13 -14
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +40 -7
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +18 -4
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +8 -18
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +30 -26
- package/src/Input.tsx +16 -7
- package/src/InputGroup.tsx +9 -9
- package/src/Kbd.tsx +1 -1
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +14 -13
- package/src/List.tsx +5 -7
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +22 -19
- package/src/Menu.tsx +54 -28
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +123 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +37 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +56 -15
- package/src/NumberField.tsx +74 -22
- package/src/PopoverArrow.tsx +1 -2
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +13 -12
- package/src/Radio.tsx +2 -29
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +20 -12
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +40 -21
- package/src/Switch.tsx +35 -5
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +35 -38
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +147 -48
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +323 -84
- package/src/base-tokens.ts +0 -3
- package/src/button-icon.ts +4 -5
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +5 -6
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +3 -4
- package/src/hooks/usePrevious.ts +1 -4
- package/src/index.ts +19 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- 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
|
+
};
|
package/src/UnmountCallback.tsx
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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);
|
package/src/VisuallyHidden.tsx
CHANGED
|
@@ -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
|
|
10
|
+
* Renders a span; use `as="div"` for block children.
|
|
11
11
|
*/
|
|
12
12
|
export const VisuallyHidden = styled("span", {
|
|
13
13
|
base: { srOnly: true },
|