@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +40 -0
- package/README.md +279 -26
- 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 +62 -0
- 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 +62 -0
- 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 +62 -0
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +19 -10
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- 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 +68 -30
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +49 -14
- package/src/Checkbox.tsx +111 -32
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +179 -0
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +72 -8
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +62 -0
- 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 -13
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +26 -12
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +81 -0
- package/src/List.tsx +8 -6
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +51 -17
- package/src/Menu.tsx +117 -2
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +137 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +41 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +108 -0
- package/src/NumberField.tsx +138 -0
- package/src/PopoverArrow.tsx +19 -5
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +108 -0
- package/src/Radio.tsx +62 -0
- 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 +78 -6
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +57 -11
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +54 -40
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +151 -46
- 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 +336 -84
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
- package/src/button-icon.ts +22 -0
- 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 +63 -0
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/hooks/usePrevious.ts +15 -0
- package/src/index.ts +29 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/Modal.recipe.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Dialog slot recipe
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Dialog slot recipe. The overlay spans the full viewport (the iOS WKWebView
|
|
10
|
+
* 100% fix) and the `full` size adds safe-area insets and the brand
|
|
11
|
+
* status-bar gradient.
|
|
12
12
|
*
|
|
13
13
|
* A config recipe (rather than an atomic `sva`) so the `size` variant accepts
|
|
14
14
|
* responsive values, e.g. `{ base: "full", md: "4xl" }`. Consumed by the
|
|
@@ -33,6 +33,9 @@ const dialogBox = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// The `full` variant also styles these slots; same symmetry requirement.
|
|
36
|
+
// The header's padding stays physical to pair with that variant's
|
|
37
|
+
// window-controls inset, which is a screen position rather than a
|
|
38
|
+
// reading-order one; the base `px` covers the start side either way.
|
|
36
39
|
const dialogSlots = {
|
|
37
40
|
header: { pl: "6" },
|
|
38
41
|
body: { overflowY: "visible" },
|
|
@@ -54,7 +57,13 @@ export const dialog = defineSlotRecipe({
|
|
|
54
57
|
overlay: {
|
|
55
58
|
position: "fixed",
|
|
56
59
|
inset: 0,
|
|
57
|
-
|
|
60
|
+
// 100vw, not 100%: react-aria's scroll lock reserves the root
|
|
61
|
+
// scrollbar gutter (scrollbar-gutter: stable), which narrows the
|
|
62
|
+
// containing block for fixed elements — 100% leaves an uncovered
|
|
63
|
+
// strip where the page scrollbar was. Viewport units span the
|
|
64
|
+
// reserved gutter, so the backdrop (and a full-size dialog) reach
|
|
65
|
+
// the real viewport edge.
|
|
66
|
+
w: "100vw",
|
|
58
67
|
h: "100%",
|
|
59
68
|
bg: "blackAlpha.600",
|
|
60
69
|
zIndex: "modal",
|
|
@@ -79,7 +88,7 @@ export const dialog = defineSlotRecipe({
|
|
|
79
88
|
display: "flex",
|
|
80
89
|
flexDirection: "column",
|
|
81
90
|
outline: "none",
|
|
82
|
-
//
|
|
91
|
+
// Fade + scale enter/exit.
|
|
83
92
|
opacity: 1,
|
|
84
93
|
transform: "scale(1)",
|
|
85
94
|
transition: "opacity 0.2s ease-out, transform 0.2s ease-out",
|
|
@@ -108,16 +117,15 @@ export const dialog = defineSlotRecipe({
|
|
|
108
117
|
display: "flex",
|
|
109
118
|
alignItems: "center",
|
|
110
119
|
justifyContent: "flex-end",
|
|
111
|
-
//
|
|
112
|
-
// baked in as the house style. Override via css for tighter layouts.
|
|
120
|
+
// The gap is house style; override via css for tighter layouts.
|
|
113
121
|
gap: "5",
|
|
114
122
|
flexShrink: 0,
|
|
115
123
|
},
|
|
116
124
|
closeTrigger: { position: "absolute", top: "2", insetEnd: "3" },
|
|
117
125
|
},
|
|
118
126
|
variants: {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
127
|
+
// Vertically centre the dialog in the viewport rather than the default
|
|
128
|
+
// top alignment.
|
|
121
129
|
centered: {
|
|
122
130
|
true: {
|
|
123
131
|
overlay: { alignItems: "center" },
|
package/src/Modal.tsx
CHANGED
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
CSSProperties,
|
|
9
|
+
ReactNode,
|
|
10
|
+
RefObject,
|
|
11
|
+
useContext,
|
|
12
|
+
} from "react";
|
|
7
13
|
import {
|
|
8
14
|
Button as RACButton,
|
|
9
15
|
Dialog,
|
|
16
|
+
DialogTrigger as RACDialogTrigger,
|
|
10
17
|
Heading as RACHeading,
|
|
11
18
|
Modal as RACModal,
|
|
12
19
|
ModalOverlay,
|
|
20
|
+
OverlayTriggerStateContext,
|
|
13
21
|
} from "react-aria-components";
|
|
14
22
|
import { css, cx } from "styled-system/css";
|
|
15
23
|
import { dialog } from "styled-system/recipes";
|
|
16
24
|
import { ConditionalValue, SystemStyleObject } from "styled-system/types";
|
|
17
25
|
import { useIntl } from "react-intl";
|
|
18
26
|
import { CloseIcon } from "./CloseIcon";
|
|
27
|
+
import { dataAttrs } from "./data-attrs";
|
|
19
28
|
import { uiMessage } from "./messages";
|
|
20
29
|
import { UnmountCallback } from "./UnmountCallback";
|
|
21
30
|
|
|
@@ -42,38 +51,39 @@ export type ModalSize = ConditionalValue<
|
|
|
42
51
|
| "full"
|
|
43
52
|
>;
|
|
44
53
|
|
|
45
|
-
export interface
|
|
46
|
-
isOpen: boolean;
|
|
47
|
-
onClose: () => void;
|
|
54
|
+
export interface ModalOwnProps {
|
|
48
55
|
size?: ModalSize;
|
|
49
56
|
/** Allow closing by clicking the backdrop (default true; Escape always closes). */
|
|
50
57
|
isDismissable?: boolean;
|
|
51
|
-
/** Disable the enter/exit transitions
|
|
58
|
+
/** Disable the enter/exit transitions. */
|
|
52
59
|
motionless?: boolean;
|
|
53
|
-
/** Prevent Escape closing the dialog
|
|
60
|
+
/** Prevent Escape closing the dialog. */
|
|
54
61
|
isKeyboardDismissDisabled?: boolean;
|
|
55
|
-
/** Style overrides for the dialog box
|
|
62
|
+
/** Style overrides for the dialog box. */
|
|
56
63
|
contentCss?: SystemStyleObject;
|
|
57
64
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
65
|
+
* Inline styles for the dialog box, for runtime-computed positioning that
|
|
66
|
+
* Panda can't statically extract (e.g. a dialog aligned to a measured
|
|
67
|
+
* element). Prefer `contentCss` for static styles.
|
|
60
68
|
*/
|
|
61
|
-
|
|
69
|
+
contentStyle?: CSSProperties;
|
|
62
70
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
71
|
+
* Style overrides for the backdrop, e.g. a transparent backdrop when
|
|
72
|
+
* something else provides the dimming.
|
|
65
73
|
*/
|
|
74
|
+
overlayCss?: SystemStyleObject;
|
|
75
|
+
/** Use "alertdialog" for confirmations that interrupt the user. */
|
|
66
76
|
role?: "dialog" | "alertdialog";
|
|
67
|
-
/** Vertically centre the dialog
|
|
77
|
+
/** Vertically centre the dialog. */
|
|
68
78
|
isCentered?: boolean;
|
|
69
79
|
/**
|
|
70
80
|
* Called after the dialog has fully closed (exit transition done and the
|
|
71
|
-
* dialog removed).
|
|
81
|
+
* dialog removed).
|
|
72
82
|
*/
|
|
73
83
|
onCloseComplete?: () => void;
|
|
74
84
|
/**
|
|
75
85
|
* Element to focus when the dialog closes, instead of the element that was
|
|
76
|
-
* focused when it opened.
|
|
86
|
+
* focused when it opened.
|
|
77
87
|
*/
|
|
78
88
|
finalFocusRef?: RefObject<HTMLElement>;
|
|
79
89
|
/**
|
|
@@ -84,12 +94,51 @@ export interface ModalProps {
|
|
|
84
94
|
*/
|
|
85
95
|
"aria-label"?: string;
|
|
86
96
|
children: ReactNode;
|
|
97
|
+
/**
|
|
98
|
+
* `data-*` attributes land on the dialog box, so end-to-end tests can
|
|
99
|
+
* address a dialog. Shells that forward their caller's data attributes can
|
|
100
|
+
* spread them straight in.
|
|
101
|
+
*/
|
|
102
|
+
[key: `data-${string}`]: unknown;
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
/**
|
|
90
|
-
* Modal
|
|
91
|
-
*
|
|
106
|
+
* A Modal you drive yourself. Also the type for a component that *forwards*
|
|
107
|
+
* modal props — `Omit<ControlledModalProps, "children">` — because a spread
|
|
108
|
+
* cannot be matched against the union `ModalProps` is: TypeScript has no way
|
|
109
|
+
* to know which half of it an object with `isOpen?: boolean` satisfies.
|
|
110
|
+
*/
|
|
111
|
+
export type ControlledModalProps = ModalOwnProps & {
|
|
112
|
+
/** Whether the dialog is showing. */
|
|
113
|
+
isOpen: boolean;
|
|
114
|
+
/** Called when the dialog asks to close. */
|
|
115
|
+
onClose: () => void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The props of a `Modal`: its own, plus an open state that is either entirely
|
|
120
|
+
* yours or entirely a `DialogTrigger`'s. Never half of each — `isOpen`
|
|
121
|
+
* without `onClose` leaves the close button and Escape with nothing to call,
|
|
122
|
+
* so the pair is enforced rather than merely documented.
|
|
123
|
+
*/
|
|
124
|
+
export type ModalProps =
|
|
125
|
+
| ControlledModalProps
|
|
126
|
+
| (ModalOwnProps & { isOpen?: never; onClose?: never });
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Modal — a focus-trapping dialog in a single shell; place ModalHeader,
|
|
92
130
|
* ModalBody and ModalFooter inside.
|
|
131
|
+
*
|
|
132
|
+
* Two ways to drive it:
|
|
133
|
+
*
|
|
134
|
+
* - **Controlled** (`isOpen` + `onClose`), which is what any dialog with more
|
|
135
|
+
* than one opener needs — a menu item and a toolbar button opening the same
|
|
136
|
+
* dialog, or one opened from a handler after an async result.
|
|
137
|
+
* - **Inside a `DialogTrigger`**, with neither prop: react-aria holds the
|
|
138
|
+
* open state, the trigger opens it, and `ModalCloseButton` and the footer's
|
|
139
|
+
* `useDialogClose()` still close it. Prefer this where a dialog has exactly
|
|
140
|
+
* one trigger sitting next to it — there is no state to hold, and none to
|
|
141
|
+
* get out of step.
|
|
93
142
|
*/
|
|
94
143
|
export const Modal = ({
|
|
95
144
|
isOpen,
|
|
@@ -99,6 +148,7 @@ export const Modal = ({
|
|
|
99
148
|
motionless,
|
|
100
149
|
isKeyboardDismissDisabled,
|
|
101
150
|
contentCss,
|
|
151
|
+
contentStyle,
|
|
102
152
|
overlayCss,
|
|
103
153
|
role,
|
|
104
154
|
isCentered,
|
|
@@ -106,7 +156,14 @@ export const Modal = ({
|
|
|
106
156
|
finalFocusRef,
|
|
107
157
|
"aria-label": ariaLabel,
|
|
108
158
|
children,
|
|
159
|
+
...rest
|
|
109
160
|
}: ModalProps) => {
|
|
161
|
+
// Set by a DialogTrigger (or any react-aria overlay trigger) above us. When
|
|
162
|
+
// `isOpen` is given it is ignored: RAC's ModalOverlay prefers an explicit
|
|
163
|
+
// prop over the context, and so do we for the close function.
|
|
164
|
+
const triggerState = useContext(OverlayTriggerStateContext);
|
|
165
|
+
const close = onClose ?? (() => triggerState?.close());
|
|
166
|
+
const dataProps = dataAttrs(rest);
|
|
110
167
|
const slots = dialog({ size, centered: isCentered });
|
|
111
168
|
const motionlessClass = motionless
|
|
112
169
|
? css({
|
|
@@ -128,11 +185,17 @@ export const Modal = ({
|
|
|
128
185
|
isOpen={isOpen}
|
|
129
186
|
onOpenChange={(open) => {
|
|
130
187
|
if (!open) {
|
|
131
|
-
|
|
188
|
+
close();
|
|
132
189
|
}
|
|
133
190
|
}}
|
|
134
191
|
isDismissable={isDismissable}
|
|
135
192
|
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
|
193
|
+
// Marker for the html:has() rule in base-preset.ts that releases the
|
|
194
|
+
// scroll lock's reserved scrollbar gutter while a full-size dialog is
|
|
195
|
+
// open: the reserved strip is a hit-testing dead zone (clicks fall
|
|
196
|
+
// through to the root and read as outside-dismissal), and the page
|
|
197
|
+
// reflowing behind an opaque full-screen dialog is invisible.
|
|
198
|
+
data-fullsize={size === "full" || undefined}
|
|
136
199
|
className={cx(
|
|
137
200
|
slots.overlay,
|
|
138
201
|
motionlessClass,
|
|
@@ -141,6 +204,8 @@ export const Modal = ({
|
|
|
141
204
|
>
|
|
142
205
|
<UnmountCallback callback={handleUnmount} />
|
|
143
206
|
<RACModal
|
|
207
|
+
{...dataProps}
|
|
208
|
+
style={contentStyle}
|
|
144
209
|
className={cx(
|
|
145
210
|
slots.content,
|
|
146
211
|
motionlessClass,
|
|
@@ -148,7 +213,7 @@ export const Modal = ({
|
|
|
148
213
|
)}
|
|
149
214
|
>
|
|
150
215
|
<Dialog role={role} aria-label={ariaLabel} className={slots.inner}>
|
|
151
|
-
<SlotContext.Provider value={{ slots, onClose }}>
|
|
216
|
+
<SlotContext.Provider value={{ slots, onClose: close }}>
|
|
152
217
|
{children}
|
|
153
218
|
</SlotContext.Provider>
|
|
154
219
|
</Dialog>
|
|
@@ -157,10 +222,40 @@ export const Modal = ({
|
|
|
157
222
|
);
|
|
158
223
|
};
|
|
159
224
|
|
|
225
|
+
/**
|
|
226
|
+
* DialogTrigger — react-aria-components' <DialogTrigger>: wrap a trigger
|
|
227
|
+
* element and a `Modal`, and the open state is theirs rather than yours.
|
|
228
|
+
*
|
|
229
|
+
* ```tsx
|
|
230
|
+
* <DialogTrigger>
|
|
231
|
+
* <Button>Settings</Button>
|
|
232
|
+
* <Modal size="lg">
|
|
233
|
+
* <ModalHeader>Settings</ModalHeader>
|
|
234
|
+
* …
|
|
235
|
+
* </Modal>
|
|
236
|
+
* </DialogTrigger>
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* Only for a dialog with a single trigger beside it. A dialog opened from
|
|
240
|
+
* more than one place, from a menu item (which cannot hold a dialog — a
|
|
241
|
+
* non-collection child truncates the menu), or from a handler, wants the
|
|
242
|
+
* controlled `Modal` instead.
|
|
243
|
+
*/
|
|
244
|
+
export const DialogTrigger = RACDialogTrigger;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The current dialog's close function — the same one `ModalCloseButton` uses,
|
|
248
|
+
* for a footer's own Cancel/Done buttons. Works in both modes, so a dialog's
|
|
249
|
+
* content need not know which is driving it.
|
|
250
|
+
*/
|
|
251
|
+
export const useDialogClose = () => useDialog().onClose;
|
|
252
|
+
|
|
160
253
|
interface SlotProps {
|
|
161
254
|
children?: ReactNode;
|
|
162
255
|
css?: SystemStyleObject;
|
|
163
256
|
className?: string;
|
|
257
|
+
/** `data-*` attributes land on the slot element. */
|
|
258
|
+
[key: `data-${string}`]: unknown;
|
|
164
259
|
}
|
|
165
260
|
|
|
166
261
|
/** Modal title. Rendered as RAC's labelling heading for the dialog. */
|
|
@@ -169,12 +264,18 @@ export const ModalHeader = ({
|
|
|
169
264
|
css: cssProp,
|
|
170
265
|
className,
|
|
171
266
|
level,
|
|
267
|
+
...rest
|
|
172
268
|
}: SlotProps & {
|
|
173
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Heading element level. Defaults to 2: RAC's Dialog supplies that through
|
|
271
|
+
* HeadingContext for the `title` slot, not the bare Heading default of 3.
|
|
272
|
+
*/
|
|
273
|
+
level?: number;
|
|
174
274
|
}) => {
|
|
175
275
|
const { slots } = useDialog();
|
|
176
276
|
return (
|
|
177
277
|
<RACHeading
|
|
278
|
+
{...dataAttrs(rest)}
|
|
178
279
|
slot="title"
|
|
179
280
|
level={level}
|
|
180
281
|
className={cx(
|
|
@@ -188,10 +289,16 @@ export const ModalHeader = ({
|
|
|
188
289
|
);
|
|
189
290
|
};
|
|
190
291
|
|
|
191
|
-
export const ModalBody = ({
|
|
292
|
+
export const ModalBody = ({
|
|
293
|
+
children,
|
|
294
|
+
css: cssProp,
|
|
295
|
+
className,
|
|
296
|
+
...rest
|
|
297
|
+
}: SlotProps) => {
|
|
192
298
|
const { slots } = useDialog();
|
|
193
299
|
return (
|
|
194
300
|
<div
|
|
301
|
+
{...dataAttrs(rest)}
|
|
195
302
|
className={cx(slots.body, cssProp ? css(cssProp) : undefined, className)}
|
|
196
303
|
>
|
|
197
304
|
{children}
|
|
@@ -203,10 +310,12 @@ export const ModalFooter = ({
|
|
|
203
310
|
children,
|
|
204
311
|
css: cssProp,
|
|
205
312
|
className,
|
|
313
|
+
...rest
|
|
206
314
|
}: SlotProps) => {
|
|
207
315
|
const { slots } = useDialog();
|
|
208
316
|
return (
|
|
209
317
|
<div
|
|
318
|
+
{...dataAttrs(rest)}
|
|
210
319
|
className={cx(
|
|
211
320
|
slots.footer,
|
|
212
321
|
cssProp ? css(cssProp) : undefined,
|
|
@@ -221,19 +330,23 @@ export const ModalFooter = ({
|
|
|
221
330
|
export interface ModalCloseButtonProps {
|
|
222
331
|
/** Accessible name; defaults to the localized close label. */
|
|
223
332
|
"aria-label"?: string;
|
|
333
|
+
/** `data-*` attributes land on the button. */
|
|
334
|
+
[key: `data-${string}`]: unknown;
|
|
224
335
|
}
|
|
225
336
|
|
|
226
337
|
/**
|
|
227
|
-
* ModalCloseButton — the X in the dialog's top corner
|
|
228
|
-
*
|
|
338
|
+
* ModalCloseButton — the X in the dialog's top corner. Closes via the Modal's
|
|
339
|
+
* onClose.
|
|
229
340
|
*/
|
|
230
341
|
export const ModalCloseButton = ({
|
|
231
342
|
"aria-label": ariaLabel,
|
|
343
|
+
...rest
|
|
232
344
|
}: ModalCloseButtonProps) => {
|
|
233
345
|
const intl = useIntl();
|
|
234
346
|
const { slots, onClose } = useDialog();
|
|
235
347
|
return (
|
|
236
348
|
<RACButton
|
|
349
|
+
{...dataAttrs(rest)}
|
|
237
350
|
aria-label={ariaLabel ?? intl.formatMessage(uiMessage("ui.close-action"))}
|
|
238
351
|
onPress={onClose}
|
|
239
352
|
className={cx(
|
|
@@ -255,7 +368,7 @@ export const ModalCloseButton = ({
|
|
|
255
368
|
transitionDuration: "normal",
|
|
256
369
|
_hover: { bg: "blackAlpha.100" },
|
|
257
370
|
_active: { bg: "blackAlpha.200" },
|
|
258
|
-
_focusVisible: {
|
|
371
|
+
_focusVisible: { focusRing: "outline" },
|
|
259
372
|
}),
|
|
260
373
|
)}
|
|
261
374
|
>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
import { MdMoreVert } from "react-icons/md";
|
|
8
|
+
import { Icon } from "./Icon";
|
|
9
|
+
import { IconButton, IconButtonProps } from "./IconButton";
|
|
10
|
+
|
|
11
|
+
export interface MoreMenuButtonProps
|
|
12
|
+
extends Omit<IconButtonProps, "children"> {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MoreMenuButton — the "more options" half of a split button. Use as the
|
|
16
|
+
* trigger inside a MenuTrigger, alongside the main action, both in an
|
|
17
|
+
* attached ButtonGroup:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <ButtonGroup isAttached>
|
|
21
|
+
* <Button variant="primary" onPress={send}>Send</Button>
|
|
22
|
+
* <MenuTrigger>
|
|
23
|
+
* <MoreMenuButton variant="primary" aria-label="More send options" />
|
|
24
|
+
* <MenuList>…</MenuList>
|
|
25
|
+
* </MenuTrigger>
|
|
26
|
+
* </ButtonGroup>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Give it the main action's `variant`, `tone` and `size`: the two halves are
|
|
30
|
+
* one control. The seam between them is ButtonGroup's job.
|
|
31
|
+
*/
|
|
32
|
+
export const MoreMenuButton = forwardRef<
|
|
33
|
+
HTMLButtonElement,
|
|
34
|
+
MoreMenuButtonProps
|
|
35
|
+
>(function MoreMenuButton({ css: cssProp, ...props }, ref) {
|
|
36
|
+
return (
|
|
37
|
+
<IconButton ref={ref} css={cssProp} {...props}>
|
|
38
|
+
<Icon
|
|
39
|
+
as={MdMoreVert}
|
|
40
|
+
css={{
|
|
41
|
+
// Optical centring: an attached end button is a rectangle plus a
|
|
42
|
+
// semicircle, whose area sits 4.7% of the width towards the flat
|
|
43
|
+
// edge. A share of the width rather than a length because that is a
|
|
44
|
+
// property of the shape, not the size — the button stays square and
|
|
45
|
+
// the radius always clamps to half its height (see IconButton), so
|
|
46
|
+
// one percentage holds everywhere. Leading is the mirror image; a
|
|
47
|
+
// middle or lone button is symmetric and needs nothing.
|
|
48
|
+
//
|
|
49
|
+
// Position rather than margin so the nudge can't feed back into the
|
|
50
|
+
// width the percentage resolves against.
|
|
51
|
+
"[data-attached] > *:not(:first-child):last-child &": {
|
|
52
|
+
position: "relative",
|
|
53
|
+
insetInlineStart: "-4.7%",
|
|
54
|
+
},
|
|
55
|
+
"[data-attached] > *:first-child:not(:last-child) &": {
|
|
56
|
+
position: "relative",
|
|
57
|
+
insetInlineStart: "4.7%",
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</IconButton>
|
|
62
|
+
);
|
|
63
|
+
});
|
package/src/NativeSelect.tsx
CHANGED
|
@@ -5,41 +5,56 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { forwardRef, SelectHTMLAttributes } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
|
-
import { input } from "styled-system/recipes";
|
|
8
|
+
import { input, InputVariantProps } from "styled-system/recipes";
|
|
9
9
|
import { SystemStyleObject } from "styled-system/types";
|
|
10
10
|
|
|
11
11
|
export interface NativeSelectProps
|
|
12
|
-
|
|
12
|
+
// `size` is the recipe's size scale; the native visible-rows attribute it
|
|
13
|
+
// shadows was unused.
|
|
14
|
+
extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "className" | "size">,
|
|
15
|
+
InputVariantProps {
|
|
13
16
|
/**
|
|
14
17
|
* Suppress the dropdown chevron, e.g. when an adjacent control provides the
|
|
15
18
|
* affordance. Without the chevron a bare <select> element is rendered (no
|
|
16
19
|
* wrapper), so it can participate directly in e.g. an attached ButtonGroup.
|
|
17
20
|
*/
|
|
18
21
|
hideChevron?: boolean;
|
|
19
|
-
/** Per-instance style overrides, merged after the recipe. */
|
|
22
|
+
/** Per-instance style overrides for the select, merged after the recipe. */
|
|
20
23
|
css?: SystemStyleObject;
|
|
24
|
+
/**
|
|
25
|
+
* Style overrides for the chevron wrapper, which is where width constraints
|
|
26
|
+
* belong (the select fills it). No wrapper is rendered with `hideChevron`;
|
|
27
|
+
* constrain the select directly.
|
|
28
|
+
*/
|
|
29
|
+
wrapperCss?: SystemStyleObject;
|
|
21
30
|
className?: string;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
* NativeSelect — a native select styled like
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* NativeSelect — a native select styled like the outline Input. The recipe's
|
|
35
|
+
* `appearance: none` removes the platform chevron, so one is drawn back in by
|
|
36
|
+
* default (`currentColor`).
|
|
28
37
|
*/
|
|
29
38
|
export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
30
39
|
function NativeSelect(
|
|
31
|
-
{ hideChevron = false, css: cssProp, className, ...rest },
|
|
40
|
+
{ hideChevron = false, size, css: cssProp, wrapperCss, className, ...rest },
|
|
32
41
|
ref,
|
|
33
42
|
) {
|
|
34
43
|
const select = (
|
|
35
44
|
<select
|
|
36
45
|
ref={ref}
|
|
37
46
|
className={cx(
|
|
38
|
-
input(),
|
|
47
|
+
input({ size }),
|
|
39
48
|
css(
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
49
|
+
// Option text sits a hair higher than input text; the 1px bottom
|
|
50
|
+
// padding compensates.
|
|
51
|
+
{
|
|
52
|
+
cursor: "pointer",
|
|
53
|
+
paddingBottom: "1px",
|
|
54
|
+
_disabled: { cursor: "not-allowed" },
|
|
55
|
+
},
|
|
56
|
+
// Room for the chevron overlay (constant across sizes).
|
|
57
|
+
hideChevron ? undefined : { paddingEnd: "8" },
|
|
43
58
|
cssProp,
|
|
44
59
|
),
|
|
45
60
|
className,
|
|
@@ -51,21 +66,33 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
51
66
|
return select;
|
|
52
67
|
}
|
|
53
68
|
return (
|
|
54
|
-
<span
|
|
69
|
+
<span
|
|
70
|
+
className={css(
|
|
71
|
+
// Full-width like every other field; the select's own recipe width
|
|
72
|
+
// fills it. Constrain via wrapperCss.
|
|
73
|
+
{ position: "relative", display: "inline-flex", width: "100%" },
|
|
74
|
+
wrapperCss,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
55
77
|
{select}
|
|
56
|
-
{/*
|
|
78
|
+
{/* The chevron, fixed-size across field sizes. The path is Chakra
|
|
79
|
+
UI's Select chevron, inlined for visual parity with the apps'
|
|
80
|
+
original look (see the notice in LICENSE.md). */}
|
|
57
81
|
<svg
|
|
58
82
|
viewBox="0 0 24 24"
|
|
59
83
|
aria-hidden
|
|
60
84
|
className={css({
|
|
61
85
|
position: "absolute",
|
|
62
|
-
|
|
86
|
+
insetEnd: "2",
|
|
63
87
|
top: "50%",
|
|
64
88
|
transform: "translateY(-50%)",
|
|
65
89
|
width: "5",
|
|
66
90
|
height: "5",
|
|
67
91
|
pointerEvents: "none",
|
|
68
92
|
fill: "currentColor",
|
|
93
|
+
// The chevron sits outside the select so it doesn't inherit its
|
|
94
|
+
// disabled dimming; dim it explicitly to match.
|
|
95
|
+
"select:disabled + &": { opacity: 0.5 },
|
|
69
96
|
})}
|
|
70
97
|
>
|
|
71
98
|
<path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" />
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, ReactNode, useId } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { field } from "styled-system/recipes";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
import { FieldHelperText, FieldLabel, FieldLayoutProps } from "./Field";
|
|
11
|
+
import { NativeSelect, NativeSelectProps } from "./NativeSelect";
|
|
12
|
+
|
|
13
|
+
export interface NativeSelectFieldProps
|
|
14
|
+
extends NativeSelectProps,
|
|
15
|
+
FieldLayoutProps {
|
|
16
|
+
/** Visible label, associated with the select via `htmlFor`. */
|
|
17
|
+
label: ReactNode;
|
|
18
|
+
/** Label style overrides. */
|
|
19
|
+
labelCss?: SystemStyleObject;
|
|
20
|
+
/** Help text below the field, wired to the select's `aria-describedby`. */
|
|
21
|
+
helperText?: ReactNode;
|
|
22
|
+
/** Per-instance style overrides for the field root. */
|
|
23
|
+
rootCss?: SystemStyleObject;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* NativeSelectField — a labelled `NativeSelect`, pairing the bare control with
|
|
28
|
+
* the field chrome the RAC fields get from react-aria, as `TextField` pairs
|
|
29
|
+
* with `Input`. The label association and `aria-describedby` are wired here,
|
|
30
|
+
* since there is no RAC context to do it; the label dims with the control
|
|
31
|
+
* exactly as the RAC fields' do. No `errorMessage` yet — no consumer needs
|
|
32
|
+
* one; render `FieldErrorMessage` beside it if yours does.
|
|
33
|
+
*
|
|
34
|
+
* With `labelPosition="side"` this is the settings row both
|
|
35
|
+
* `SelectFormControl`s hand-rolled: label beside a fixed-width select
|
|
36
|
+
* (`wrapperCss={{ width: "28ch" }}`), label absorbing the free space.
|
|
37
|
+
*/
|
|
38
|
+
export const NativeSelectField = forwardRef<
|
|
39
|
+
HTMLSelectElement,
|
|
40
|
+
NativeSelectFieldProps
|
|
41
|
+
>(function NativeSelectField(
|
|
42
|
+
{ label, labelCss, helperText, labelPosition, rootCss, id, ...rest },
|
|
43
|
+
ref,
|
|
44
|
+
) {
|
|
45
|
+
const generatedId = useId();
|
|
46
|
+
const selectId = id ?? generatedId;
|
|
47
|
+
const helperId = useId();
|
|
48
|
+
const describedBy =
|
|
49
|
+
[rest["aria-describedby"], helperText != null ? helperId : undefined]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(" ") || undefined;
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={cx(
|
|
55
|
+
field({ size: rest.size, labelPosition }).root,
|
|
56
|
+
rootCss ? css(rootCss) : undefined,
|
|
57
|
+
)}
|
|
58
|
+
// The field recipe's label dims off the root's data-disabled, which RAC
|
|
59
|
+
// stamps for the other fields; here it is restated from the attribute.
|
|
60
|
+
data-disabled={rest.disabled || undefined}
|
|
61
|
+
>
|
|
62
|
+
<FieldLabel
|
|
63
|
+
htmlFor={selectId}
|
|
64
|
+
size={rest.size}
|
|
65
|
+
labelPosition={labelPosition}
|
|
66
|
+
isRequired={rest.required}
|
|
67
|
+
css={labelCss}
|
|
68
|
+
>
|
|
69
|
+
{label}
|
|
70
|
+
</FieldLabel>
|
|
71
|
+
<NativeSelect
|
|
72
|
+
ref={ref}
|
|
73
|
+
id={selectId}
|
|
74
|
+
{...rest}
|
|
75
|
+
aria-describedby={describedBy}
|
|
76
|
+
/>
|
|
77
|
+
{helperText != null && (
|
|
78
|
+
<FieldHelperText id={helperId} labelPosition={labelPosition}>
|
|
79
|
+
{helperText}
|
|
80
|
+
</FieldHelperText>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
});
|