@microbit/ui 0.1.0-alpha.17 → 0.1.0-alpha.19
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 +34 -11
- package/lang/ui.ca.json +36 -0
- package/lang/ui.cy.json +36 -0
- package/lang/ui.de.json +36 -0
- package/lang/ui.en-us.json +36 -0
- package/lang/ui.en.json +28 -0
- package/lang/ui.es-es.json +36 -0
- package/lang/ui.fr.json +36 -0
- package/lang/ui.ga-ie.json +36 -0
- package/lang/ui.it.json +36 -0
- package/lang/ui.ja.json +36 -0
- package/lang/ui.ko.json +36 -0
- package/lang/ui.lol.json +36 -0
- package/lang/ui.nl.json +36 -0
- package/lang/ui.pl.json +36 -0
- package/lang/ui.pt-br.json +36 -0
- package/lang/ui.zh-cn.json +36 -0
- package/lang/ui.zh-tw.json +36 -0
- package/package.json +1 -1
- package/src/Avatar.recipe.ts +3 -1
- package/src/Checkbox.recipe.ts +5 -0
- package/src/Checkbox.tsx +42 -2
- package/src/CheckboxGroup.tsx +6 -10
- package/src/ComboBox.tsx +38 -16
- package/src/Field.recipe.ts +113 -0
- package/src/Field.tsx +189 -0
- package/src/Input.recipe.ts +4 -2
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +25 -7
- package/src/NumberField.tsx +52 -22
- package/src/Radio.recipe.ts +4 -0
- package/src/Radio.tsx +1 -63
- package/src/RadioGroup.tsx +69 -0
- package/src/Select.recipe.ts +27 -17
- package/src/Select.tsx +38 -15
- package/src/Skeleton.tsx +5 -3
- package/src/Switch.recipe.ts +19 -0
- package/src/Switch.tsx +34 -3
- package/src/TextField.tsx +19 -8
- package/src/Toast.tsx +17 -2
- package/src/base-preset.ts +45 -10
- package/src/index.ts +3 -1
- package/src/FieldSupport.tsx +0 -68
- package/src/TextField.recipe.ts +0 -58
package/src/Field.tsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
FieldError,
|
|
9
|
+
Label as RACLabel,
|
|
10
|
+
LabelProps as RACLabelProps,
|
|
11
|
+
Text as RACText,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { field, FieldVariantProps } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The label/helper/error chrome every labelled form field shares — Chakra's
|
|
19
|
+
* FormControl parts, generalised out of TextField so Select, ComboBox,
|
|
20
|
+
* NumberField, RadioGroup and CheckboxGroup carry the same props
|
|
21
|
+
* (data-microbit-org's forms attach helper and error text to all of these).
|
|
22
|
+
*/
|
|
23
|
+
export interface FieldSupportProps {
|
|
24
|
+
/** Help text below the field (Chakra's FormHelperText). */
|
|
25
|
+
helperText?: ReactNode;
|
|
26
|
+
/** Shown below the field when invalid (Chakra's FormErrorMessage). */
|
|
27
|
+
errorMessage?: ReactNode;
|
|
28
|
+
/** Per-instance style overrides for the helper text. */
|
|
29
|
+
helperTextCss?: SystemStyleObject;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `labelPosition` for the four single-control fields (TextField, NumberField,
|
|
34
|
+
* Select, ComboBox). Deliberately not on RadioGroup/CheckboxGroup: their roots
|
|
35
|
+
* carry no layout, and RAC's own `orientation` prop is a different axis there
|
|
36
|
+
* (it lays out the radios, not the label).
|
|
37
|
+
*/
|
|
38
|
+
export interface FieldLayoutProps {
|
|
39
|
+
/**
|
|
40
|
+
* `side` puts the label beside the control — the settings-row pattern. The
|
|
41
|
+
* label absorbs the free space; the control keeps its own width, so give it
|
|
42
|
+
* one (`groupCss`, `wrapperCss` or `triggerCss` depending on the field).
|
|
43
|
+
* Helper and error text drop to a full-width line below the pair.
|
|
44
|
+
*/
|
|
45
|
+
labelPosition?: FieldVariantProps["labelPosition"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper text and error message for a react-aria field container. Render
|
|
50
|
+
* inside any RAC component with field validation context (TextField, Select,
|
|
51
|
+
* ComboBox, NumberField, RadioGroup, CheckboxGroup) — react-aria wires the
|
|
52
|
+
* description to the input's aria-describedby, and the error renders only
|
|
53
|
+
* while the field is invalid. Also exported for app-side composites built on
|
|
54
|
+
* RAC containers.
|
|
55
|
+
*/
|
|
56
|
+
export const FieldSupport = ({
|
|
57
|
+
helperText,
|
|
58
|
+
errorMessage,
|
|
59
|
+
helperTextCss,
|
|
60
|
+
labelPosition,
|
|
61
|
+
}: FieldSupportProps & FieldLayoutProps) => {
|
|
62
|
+
const slots = field({ labelPosition });
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
{helperText != null && (
|
|
66
|
+
<RACText
|
|
67
|
+
slot="description"
|
|
68
|
+
className={cx(
|
|
69
|
+
slots.helperText,
|
|
70
|
+
helperTextCss ? css(helperTextCss) : undefined,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{helperText}
|
|
74
|
+
</RACText>
|
|
75
|
+
)}
|
|
76
|
+
<FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
interface FieldTextProps extends HTMLAttributes<HTMLDivElement> {
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
/** The field's `labelPosition`, so the text lays out to match. */
|
|
84
|
+
labelPosition?: FieldVariantProps["labelPosition"];
|
|
85
|
+
/** Per-instance style overrides. */
|
|
86
|
+
css?: SystemStyleObject;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Context-free helper text (Chakra's FormHelperText) for a control react-aria
|
|
91
|
+
* isn't wiring — a native select or a masked input. Inside a RAC field
|
|
92
|
+
* container use `FieldSupport`, which wires `aria-describedby` and validation
|
|
93
|
+
* for free; here the caller owns that wiring: give this an `id` and reference
|
|
94
|
+
* it from the control's `aria-describedby` (as `NativeSelectField` does).
|
|
95
|
+
*/
|
|
96
|
+
export const FieldHelperText = ({
|
|
97
|
+
children,
|
|
98
|
+
labelPosition,
|
|
99
|
+
css: cssProp,
|
|
100
|
+
...rest
|
|
101
|
+
}: FieldTextProps) => (
|
|
102
|
+
<div
|
|
103
|
+
{...rest}
|
|
104
|
+
className={cx(
|
|
105
|
+
field({ labelPosition }).helperText,
|
|
106
|
+
cssProp ? css(cssProp) : undefined,
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Context-free error message (Chakra's FormErrorMessage), the counterpart to
|
|
115
|
+
* `FieldHelperText` — see its note on the wiring the caller owns. RAC's
|
|
116
|
+
* `FieldError` renders only while its field is invalid; here that decision is
|
|
117
|
+
* the caller's too: render it conditionally.
|
|
118
|
+
*/
|
|
119
|
+
export const FieldErrorMessage = ({
|
|
120
|
+
children,
|
|
121
|
+
labelPosition,
|
|
122
|
+
css: cssProp,
|
|
123
|
+
...rest
|
|
124
|
+
}: FieldTextProps) => (
|
|
125
|
+
<div
|
|
126
|
+
{...rest}
|
|
127
|
+
className={cx(
|
|
128
|
+
field({ labelPosition }).errorMessage,
|
|
129
|
+
cssProp ? css(cssProp) : undefined,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The required-field asterisk (Chakra's FormLabel indicator). Render inside
|
|
138
|
+
* the field's label when `isRequired`; aria-hidden because react-aria already
|
|
139
|
+
* announces requiredness from the input itself.
|
|
140
|
+
*/
|
|
141
|
+
export const FieldRequiredIndicator = () => (
|
|
142
|
+
<span aria-hidden className={field().requiredIndicator}>
|
|
143
|
+
*
|
|
144
|
+
</span>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export interface FieldLabelProps
|
|
148
|
+
extends Omit<RACLabelProps, "className" | "children" | "style"> {
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
/** Adds the required asterisk; pass the field's `isRequired`. */
|
|
151
|
+
isRequired?: boolean;
|
|
152
|
+
/** The field's `size`, so the label scales with its control. */
|
|
153
|
+
size?: FieldVariantProps["size"];
|
|
154
|
+
/** The field's `labelPosition`, so the label lays out to match. */
|
|
155
|
+
labelPosition?: FieldVariantProps["labelPosition"];
|
|
156
|
+
/** Per-instance style overrides. */
|
|
157
|
+
css?: SystemStyleObject;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A field's visible label (Chakra's FormLabel), asterisk included. Every
|
|
162
|
+
* labelled field in the library renders its label through this, so the `field`
|
|
163
|
+
* recipe is the single answer to what a label looks like — Select and ComboBox
|
|
164
|
+
* previously carried a near-identical `label` slot on the `select` recipe,
|
|
165
|
+
* which is how the two drifted.
|
|
166
|
+
*
|
|
167
|
+
* Also exported for app-side composites: inside a RAC field container the
|
|
168
|
+
* association is automatic, and outside one (a masked or native input) pass
|
|
169
|
+
* `id` and `htmlFor` yourself.
|
|
170
|
+
*/
|
|
171
|
+
export const FieldLabel = ({
|
|
172
|
+
children,
|
|
173
|
+
isRequired,
|
|
174
|
+
size,
|
|
175
|
+
labelPosition,
|
|
176
|
+
css: cssProp,
|
|
177
|
+
...rest
|
|
178
|
+
}: FieldLabelProps) => (
|
|
179
|
+
<RACLabel
|
|
180
|
+
{...rest}
|
|
181
|
+
className={cx(
|
|
182
|
+
field({ size, labelPosition }).label,
|
|
183
|
+
cssProp ? css(cssProp) : undefined,
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{children}
|
|
187
|
+
{isRequired ? <FieldRequiredIndicator /> : null}
|
|
188
|
+
</RACLabel>
|
|
189
|
+
);
|
package/src/Input.recipe.ts
CHANGED
|
@@ -44,10 +44,12 @@ export const input = defineRecipe({
|
|
|
44
44
|
transitionProperty: transitionCommon,
|
|
45
45
|
transitionDuration: "normal",
|
|
46
46
|
border: "1px solid",
|
|
47
|
-
|
|
47
|
+
// The accessible outline stops: gray.400 is the ramp's 3:1-on-white
|
|
48
|
+
// boundary grey (WCAG 1.4.11), and hover steps darker, never lighter.
|
|
49
|
+
borderColor: "gray.400",
|
|
48
50
|
bg: "inherit",
|
|
49
51
|
color: "inherit",
|
|
50
|
-
_hover: { borderColor: "gray.
|
|
52
|
+
_hover: { borderColor: "gray.500" },
|
|
51
53
|
"&&:is([data-invalid], :user-invalid)": {
|
|
52
54
|
borderColor: "danger.500",
|
|
53
55
|
boxShadow: "0 0 0 1px token(colors.danger.500)",
|
|
@@ -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
|
+
});
|
|
@@ -11,18 +11,18 @@ import { defineSlotRecipe } from "@pandacss/dev";
|
|
|
11
11
|
* two stacked buttons. Consumed by the shared-ui NumberField
|
|
12
12
|
* (react-aria-components NumberField).
|
|
13
13
|
*
|
|
14
|
-
* Registered in the base preset (base-preset.ts)
|
|
15
|
-
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
15
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
16
16
|
*/
|
|
17
17
|
export const numberField = defineSlotRecipe({
|
|
18
18
|
className: "numberField",
|
|
19
19
|
slots: ["root", "group", "stepper", "stepperButton"],
|
|
20
20
|
base: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
21
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
22
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
23
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
24
|
+
// slot stays for apps and variants to target.
|
|
25
|
+
root: {},
|
|
26
26
|
group: {
|
|
27
27
|
position: "relative",
|
|
28
28
|
zIndex: 0,
|
|
@@ -64,4 +64,22 @@ export const numberField = defineSlotRecipe({
|
|
|
64
64
|
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
+
variants: {
|
|
68
|
+
// Chakra's NumberInput: the stepper column stays 24px wide at every size
|
|
69
|
+
// (as does the input padding paired with it in NumberField.tsx); only the
|
|
70
|
+
// arrow glyphs scale, at 0.75 × the field's font size. The base's `xs` is
|
|
71
|
+
// exactly md × 0.75, so md adds nothing.
|
|
72
|
+
size: {
|
|
73
|
+
lg: {
|
|
74
|
+
stepperButton: { fontSize: "calc(token(fontSizes.lg) * 0.75)" },
|
|
75
|
+
},
|
|
76
|
+
md: {},
|
|
77
|
+
sm: {
|
|
78
|
+
stepperButton: { fontSize: "calc(token(fontSizes.sm) * 0.75)" },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultVariants: {
|
|
83
|
+
size: "md",
|
|
84
|
+
},
|
|
67
85
|
});
|
package/src/NumberField.tsx
CHANGED
|
@@ -8,33 +8,43 @@ import {
|
|
|
8
8
|
Button as RACButton,
|
|
9
9
|
Group as RACGroup,
|
|
10
10
|
Input as RACInput,
|
|
11
|
-
Label as RACLabel,
|
|
12
11
|
NumberField as RACNumberField,
|
|
13
12
|
NumberFieldProps as RACNumberFieldProps,
|
|
14
13
|
} from "react-aria-components";
|
|
14
|
+
import { useIntl } from "react-intl";
|
|
15
15
|
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
16
|
import { css, cx } from "styled-system/css";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
field,
|
|
19
|
+
input,
|
|
20
|
+
InputVariantProps,
|
|
21
|
+
numberField,
|
|
22
|
+
} from "styled-system/recipes";
|
|
18
23
|
import { SystemStyleObject } from "styled-system/types";
|
|
19
24
|
import {
|
|
20
|
-
|
|
25
|
+
FieldLabel,
|
|
26
|
+
FieldLayoutProps,
|
|
21
27
|
FieldSupport,
|
|
22
28
|
FieldSupportProps,
|
|
23
|
-
} from "./
|
|
29
|
+
} from "./Field";
|
|
24
30
|
import { Icon } from "./Icon";
|
|
31
|
+
import { uiMessage } from "./messages";
|
|
25
32
|
|
|
26
33
|
export interface NumberFieldProps
|
|
27
34
|
extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
|
|
28
|
-
|
|
35
|
+
InputVariantProps,
|
|
36
|
+
FieldSupportProps,
|
|
37
|
+
FieldLayoutProps {
|
|
29
38
|
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
30
39
|
label?: ReactNode;
|
|
31
|
-
/** Root style overrides (
|
|
32
|
-
|
|
40
|
+
/** Root style overrides (for a label-beside-field row, prefer
|
|
41
|
+
* `labelPosition="side"`). */
|
|
42
|
+
rootCss?: SystemStyleObject;
|
|
33
43
|
/** Label style overrides. */
|
|
34
44
|
labelCss?: SystemStyleObject;
|
|
35
45
|
/** Group (input + steppers) style overrides — set `width` here. */
|
|
36
46
|
groupCss?: SystemStyleObject;
|
|
37
|
-
/** Input style overrides (
|
|
47
|
+
/** Input style overrides (for sizing, prefer the `size` prop). */
|
|
38
48
|
inputCss?: SystemStyleObject;
|
|
39
49
|
}
|
|
40
50
|
|
|
@@ -51,31 +61,49 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
51
61
|
helperText,
|
|
52
62
|
errorMessage,
|
|
53
63
|
helperTextCss,
|
|
54
|
-
|
|
64
|
+
rootCss,
|
|
55
65
|
labelCss,
|
|
56
66
|
groupCss,
|
|
57
67
|
inputCss,
|
|
58
|
-
|
|
68
|
+
labelPosition,
|
|
69
|
+
...props
|
|
59
70
|
},
|
|
60
71
|
ref,
|
|
61
72
|
) {
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
// As Input: forward every recipe variant group, not just `size`, so a
|
|
74
|
+
// preset that adds one keeps working.
|
|
75
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
76
|
+
const slots = numberField({ size: variantProps.size });
|
|
77
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
78
|
+
const intl = useIntl();
|
|
79
|
+
// Fold the field's name into the stepper labels as react-aria does; the
|
|
80
|
+
// trim eats the leftover space when there is no usable name.
|
|
81
|
+
const fieldLabel =
|
|
82
|
+
rest["aria-label"] ?? (typeof label === "string" ? label : "");
|
|
64
83
|
return (
|
|
65
84
|
<RACNumberField
|
|
85
|
+
incrementAriaLabel={intl
|
|
86
|
+
.formatMessage(uiMessage("ui.numberfield-increase"), { fieldLabel })
|
|
87
|
+
.trim()}
|
|
88
|
+
decrementAriaLabel={intl
|
|
89
|
+
.formatMessage(uiMessage("ui.numberfield-decrease"), { fieldLabel })
|
|
90
|
+
.trim()}
|
|
66
91
|
{...rest}
|
|
67
|
-
className={cx(
|
|
92
|
+
className={cx(
|
|
93
|
+
fieldSlots.root,
|
|
94
|
+
slots.root,
|
|
95
|
+
rootCss ? css(rootCss) : undefined,
|
|
96
|
+
)}
|
|
68
97
|
>
|
|
69
98
|
{label != null && (
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
<FieldLabel
|
|
100
|
+
size={variantProps.size}
|
|
101
|
+
labelPosition={labelPosition}
|
|
102
|
+
isRequired={rest.isRequired}
|
|
103
|
+
css={labelCss}
|
|
75
104
|
>
|
|
76
105
|
{label}
|
|
77
|
-
|
|
78
|
-
</RACLabel>
|
|
106
|
+
</FieldLabel>
|
|
79
107
|
)}
|
|
80
108
|
<RACGroup
|
|
81
109
|
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
@@ -83,8 +111,9 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
83
111
|
<RACInput
|
|
84
112
|
ref={ref}
|
|
85
113
|
className={cx(
|
|
86
|
-
input(),
|
|
87
|
-
// Room for the stepper column
|
|
114
|
+
input(variantProps),
|
|
115
|
+
// Room for the stepper column (constant across sizes, as the
|
|
116
|
+
// stepper's width is).
|
|
88
117
|
css({ paddingEnd: "6" }, inputCss),
|
|
89
118
|
)}
|
|
90
119
|
/>
|
|
@@ -101,6 +130,7 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
101
130
|
helperText={helperText}
|
|
102
131
|
errorMessage={errorMessage}
|
|
103
132
|
helperTextCss={helperTextCss}
|
|
133
|
+
labelPosition={labelPosition}
|
|
104
134
|
/>
|
|
105
135
|
</RACNumberField>
|
|
106
136
|
);
|
package/src/Radio.recipe.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const radio = defineSlotRecipe({
|
|
|
28
28
|
verticalAlign: "top",
|
|
29
29
|
cursor: "pointer",
|
|
30
30
|
position: "relative",
|
|
31
|
+
// As the checkbox: the accessible outline stop (WCAG 1.4.11), on the
|
|
32
|
+
// root so the control's `borderColor: inherit` reads it and call
|
|
33
|
+
// sites can still tint at the root.
|
|
34
|
+
borderColor: "gray.400",
|
|
31
35
|
"&[data-disabled]": { cursor: "not-allowed" },
|
|
32
36
|
},
|
|
33
37
|
control: {
|
package/src/Radio.tsx
CHANGED
|
@@ -5,74 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ReactNode } from "react";
|
|
7
7
|
import {
|
|
8
|
-
Label as RACLabel,
|
|
9
8
|
Radio as RACRadio,
|
|
10
9
|
RadioProps as RACRadioProps,
|
|
11
|
-
RadioGroup as RACRadioGroup,
|
|
12
|
-
RadioGroupProps as RACRadioGroupProps,
|
|
13
10
|
} from "react-aria-components";
|
|
14
11
|
import { css, cx } from "styled-system/css";
|
|
15
|
-
import {
|
|
12
|
+
import { radio, RadioVariantProps } from "styled-system/recipes";
|
|
16
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
17
|
-
import {
|
|
18
|
-
FieldRequiredIndicator,
|
|
19
|
-
FieldSupport,
|
|
20
|
-
FieldSupportProps,
|
|
21
|
-
} from "./FieldSupport";
|
|
22
|
-
|
|
23
|
-
export interface RadioGroupProps
|
|
24
|
-
extends Omit<RACRadioGroupProps, "className" | "style">,
|
|
25
|
-
FieldSupportProps {
|
|
26
|
-
/**
|
|
27
|
-
* Visible label for the group (Chakra's FormLabel above it). Use
|
|
28
|
-
* `aria-label` instead where the design has none.
|
|
29
|
-
*/
|
|
30
|
-
label?: ReactNode;
|
|
31
|
-
/** Per-instance style overrides, merged after the recipe. */
|
|
32
|
-
css?: SystemStyleObject;
|
|
33
|
-
className?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Beyond
|
|
38
|
-
* the optional field chrome (label/helperText/errorMessage — Chakra's
|
|
39
|
-
* FormControl parts) it carries no styling of its own: compose with Stack for
|
|
40
|
-
* layout, as Chakra call sites did.
|
|
41
|
-
*/
|
|
42
|
-
export const RadioGroup = ({
|
|
43
|
-
label,
|
|
44
|
-
helperText,
|
|
45
|
-
errorMessage,
|
|
46
|
-
helperTextCss,
|
|
47
|
-
css: cssProp,
|
|
48
|
-
className,
|
|
49
|
-
children,
|
|
50
|
-
...rest
|
|
51
|
-
}: RadioGroupProps) => {
|
|
52
|
-
return (
|
|
53
|
-
<RACRadioGroup
|
|
54
|
-
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
55
|
-
{...rest}
|
|
56
|
-
>
|
|
57
|
-
{(renderProps) => (
|
|
58
|
-
<>
|
|
59
|
-
{label != null && (
|
|
60
|
-
<RACLabel className={field().label}>
|
|
61
|
-
{label}
|
|
62
|
-
{rest.isRequired ? <FieldRequiredIndicator /> : null}
|
|
63
|
-
</RACLabel>
|
|
64
|
-
)}
|
|
65
|
-
{typeof children === "function" ? children(renderProps) : children}
|
|
66
|
-
<FieldSupport
|
|
67
|
-
helperText={helperText}
|
|
68
|
-
errorMessage={errorMessage}
|
|
69
|
-
helperTextCss={helperTextCss}
|
|
70
|
-
/>
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
</RACRadioGroup>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
14
|
|
|
77
15
|
export interface RadioProps
|
|
78
16
|
extends Omit<RACRadioProps, "className" | "children" | "style">,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
RadioGroup as RACRadioGroup,
|
|
9
|
+
RadioGroupProps as RACRadioGroupProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
13
|
+
import { FieldLabel, FieldSupport, FieldSupportProps } from "./Field";
|
|
14
|
+
|
|
15
|
+
export interface RadioGroupProps
|
|
16
|
+
extends Omit<RACRadioGroupProps, "className" | "style">,
|
|
17
|
+
FieldSupportProps {
|
|
18
|
+
/**
|
|
19
|
+
* Visible label for the group (Chakra's FormLabel above it). Use
|
|
20
|
+
* `aria-label` instead where the design has none.
|
|
21
|
+
*/
|
|
22
|
+
label?: ReactNode;
|
|
23
|
+
/** Label style overrides. */
|
|
24
|
+
labelCss?: SystemStyleObject;
|
|
25
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
26
|
+
css?: SystemStyleObject;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Beyond
|
|
32
|
+
* the optional field chrome (label/helperText/errorMessage — Chakra's
|
|
33
|
+
* FormControl parts) it carries no styling of its own: compose with Stack for
|
|
34
|
+
* layout, as Chakra call sites did.
|
|
35
|
+
*/
|
|
36
|
+
export const RadioGroup = ({
|
|
37
|
+
label,
|
|
38
|
+
labelCss,
|
|
39
|
+
helperText,
|
|
40
|
+
errorMessage,
|
|
41
|
+
helperTextCss,
|
|
42
|
+
css: cssProp,
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...rest
|
|
46
|
+
}: RadioGroupProps) => {
|
|
47
|
+
return (
|
|
48
|
+
<RACRadioGroup
|
|
49
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
50
|
+
{...rest}
|
|
51
|
+
>
|
|
52
|
+
{(renderProps) => (
|
|
53
|
+
<>
|
|
54
|
+
{label != null && (
|
|
55
|
+
<FieldLabel isRequired={rest.isRequired} css={labelCss}>
|
|
56
|
+
{label}
|
|
57
|
+
</FieldLabel>
|
|
58
|
+
)}
|
|
59
|
+
{typeof children === "function" ? children(renderProps) : children}
|
|
60
|
+
<FieldSupport
|
|
61
|
+
helperText={helperText}
|
|
62
|
+
errorMessage={errorMessage}
|
|
63
|
+
helperTextCss={helperTextCss}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</RACRadioGroup>
|
|
68
|
+
);
|
|
69
|
+
};
|