@microbit/ui 0.1.0-alpha.17 → 0.1.0-alpha.18
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/package.json +1 -1
- package/src/Checkbox.tsx +30 -2
- package/src/CheckboxGroup.tsx +2 -11
- package/src/ComboBox.tsx +26 -15
- package/src/Field.recipe.ts +113 -0
- package/src/Field.tsx +189 -0
- package/src/NativeSelectField.tsx +81 -0
- package/src/NumberField.recipe.ts +25 -7
- package/src/NumberField.tsx +39 -22
- package/src/Radio.tsx +1 -63
- package/src/RadioGroup.tsx +64 -0
- package/src/Select.recipe.ts +22 -15
- package/src/Select.tsx +23 -13
- package/src/Switch.recipe.ts +19 -0
- package/src/Switch.tsx +34 -3
- package/src/TextField.tsx +14 -8
- package/src/base-preset.ts +3 -1
- package/src/index.ts +3 -1
- package/src/FieldSupport.tsx +0 -68
- package/src/TextField.recipe.ts +0 -58
package/src/NumberField.tsx
CHANGED
|
@@ -8,33 +8,41 @@ 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";
|
|
15
14
|
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
15
|
import { css, cx } from "styled-system/css";
|
|
17
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
field,
|
|
18
|
+
input,
|
|
19
|
+
InputVariantProps,
|
|
20
|
+
numberField,
|
|
21
|
+
} from "styled-system/recipes";
|
|
18
22
|
import { SystemStyleObject } from "styled-system/types";
|
|
19
23
|
import {
|
|
20
|
-
|
|
24
|
+
FieldLabel,
|
|
25
|
+
FieldLayoutProps,
|
|
21
26
|
FieldSupport,
|
|
22
27
|
FieldSupportProps,
|
|
23
|
-
} from "./
|
|
28
|
+
} from "./Field";
|
|
24
29
|
import { Icon } from "./Icon";
|
|
25
30
|
|
|
26
31
|
export interface NumberFieldProps
|
|
27
32
|
extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
|
|
28
|
-
|
|
33
|
+
InputVariantProps,
|
|
34
|
+
FieldSupportProps,
|
|
35
|
+
FieldLayoutProps {
|
|
29
36
|
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
30
37
|
label?: ReactNode;
|
|
31
|
-
/** Root style overrides (
|
|
32
|
-
|
|
38
|
+
/** Root style overrides (for a label-beside-field row, prefer
|
|
39
|
+
* `labelPosition="side"`). */
|
|
40
|
+
rootCss?: SystemStyleObject;
|
|
33
41
|
/** Label style overrides. */
|
|
34
42
|
labelCss?: SystemStyleObject;
|
|
35
43
|
/** Group (input + steppers) style overrides — set `width` here. */
|
|
36
44
|
groupCss?: SystemStyleObject;
|
|
37
|
-
/** Input style overrides (
|
|
45
|
+
/** Input style overrides (for sizing, prefer the `size` prop). */
|
|
38
46
|
inputCss?: SystemStyleObject;
|
|
39
47
|
}
|
|
40
48
|
|
|
@@ -51,31 +59,38 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
51
59
|
helperText,
|
|
52
60
|
errorMessage,
|
|
53
61
|
helperTextCss,
|
|
54
|
-
|
|
62
|
+
rootCss,
|
|
55
63
|
labelCss,
|
|
56
64
|
groupCss,
|
|
57
65
|
inputCss,
|
|
58
|
-
|
|
66
|
+
labelPosition,
|
|
67
|
+
...props
|
|
59
68
|
},
|
|
60
69
|
ref,
|
|
61
70
|
) {
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
// As Input: forward every recipe variant group, not just `size`, so a
|
|
72
|
+
// preset that adds one keeps working.
|
|
73
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
74
|
+
const slots = numberField({ size: variantProps.size });
|
|
75
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
64
76
|
return (
|
|
65
77
|
<RACNumberField
|
|
66
78
|
{...rest}
|
|
67
|
-
className={cx(
|
|
79
|
+
className={cx(
|
|
80
|
+
fieldSlots.root,
|
|
81
|
+
slots.root,
|
|
82
|
+
rootCss ? css(rootCss) : undefined,
|
|
83
|
+
)}
|
|
68
84
|
>
|
|
69
85
|
{label != null && (
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
<FieldLabel
|
|
87
|
+
size={variantProps.size}
|
|
88
|
+
labelPosition={labelPosition}
|
|
89
|
+
isRequired={rest.isRequired}
|
|
90
|
+
css={labelCss}
|
|
75
91
|
>
|
|
76
92
|
{label}
|
|
77
|
-
|
|
78
|
-
</RACLabel>
|
|
93
|
+
</FieldLabel>
|
|
79
94
|
)}
|
|
80
95
|
<RACGroup
|
|
81
96
|
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
@@ -83,8 +98,9 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
83
98
|
<RACInput
|
|
84
99
|
ref={ref}
|
|
85
100
|
className={cx(
|
|
86
|
-
input(),
|
|
87
|
-
// Room for the stepper column
|
|
101
|
+
input(variantProps),
|
|
102
|
+
// Room for the stepper column (constant across sizes, as the
|
|
103
|
+
// stepper's width is).
|
|
88
104
|
css({ paddingEnd: "6" }, inputCss),
|
|
89
105
|
)}
|
|
90
106
|
/>
|
|
@@ -101,6 +117,7 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
101
117
|
helperText={helperText}
|
|
102
118
|
errorMessage={errorMessage}
|
|
103
119
|
helperTextCss={helperTextCss}
|
|
120
|
+
labelPosition={labelPosition}
|
|
104
121
|
/>
|
|
105
122
|
</RACNumberField>
|
|
106
123
|
);
|
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,64 @@
|
|
|
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
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
24
|
+
css?: SystemStyleObject;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Beyond
|
|
30
|
+
* the optional field chrome (label/helperText/errorMessage — Chakra's
|
|
31
|
+
* FormControl parts) it carries no styling of its own: compose with Stack for
|
|
32
|
+
* layout, as Chakra call sites did.
|
|
33
|
+
*/
|
|
34
|
+
export const RadioGroup = ({
|
|
35
|
+
label,
|
|
36
|
+
helperText,
|
|
37
|
+
errorMessage,
|
|
38
|
+
helperTextCss,
|
|
39
|
+
css: cssProp,
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
...rest
|
|
43
|
+
}: RadioGroupProps) => {
|
|
44
|
+
return (
|
|
45
|
+
<RACRadioGroup
|
|
46
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
{(renderProps) => (
|
|
50
|
+
<>
|
|
51
|
+
{label != null && (
|
|
52
|
+
<FieldLabel isRequired={rest.isRequired}>{label}</FieldLabel>
|
|
53
|
+
)}
|
|
54
|
+
{typeof children === "function" ? children(renderProps) : children}
|
|
55
|
+
<FieldSupport
|
|
56
|
+
helperText={helperText}
|
|
57
|
+
errorMessage={errorMessage}
|
|
58
|
+
helperTextCss={helperTextCss}
|
|
59
|
+
/>
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
</RACRadioGroup>
|
|
63
|
+
);
|
|
64
|
+
};
|
package/src/Select.recipe.ts
CHANGED
|
@@ -23,13 +23,16 @@ const transitionCommon =
|
|
|
23
23
|
* Apps restyle it through the `variant` group — classroom's `classroom`
|
|
24
24
|
* variant is the rounded pill its join form uses.
|
|
25
25
|
*
|
|
26
|
+
* The label is not a slot here: it comes from the `field` recipe, as every
|
|
27
|
+
* other labelled field's does. The only thing that costs is a per-`variant`
|
|
28
|
+
* label style — restyle the control and let the label match the family.
|
|
29
|
+
*
|
|
26
30
|
* Registered in the base preset (base-preset.ts).
|
|
27
31
|
*/
|
|
28
32
|
export const select = defineSlotRecipe({
|
|
29
33
|
className: "select",
|
|
30
34
|
slots: [
|
|
31
35
|
"root",
|
|
32
|
-
"label",
|
|
33
36
|
"trigger",
|
|
34
37
|
"value",
|
|
35
38
|
"indicator",
|
|
@@ -40,17 +43,11 @@ export const select = defineSlotRecipe({
|
|
|
40
43
|
"empty",
|
|
41
44
|
],
|
|
42
45
|
base: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
label: {
|
|
49
|
-
fontSize: "md",
|
|
50
|
-
fontWeight: "medium",
|
|
51
|
-
marginEnd: "3",
|
|
52
|
-
mb: "2",
|
|
53
|
-
},
|
|
46
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
47
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
48
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
49
|
+
// slot stays for apps and variants to target.
|
|
50
|
+
root: {},
|
|
54
51
|
trigger: {
|
|
55
52
|
display: "flex",
|
|
56
53
|
alignItems: "center",
|
|
@@ -67,11 +64,8 @@ export const select = defineSlotRecipe({
|
|
|
67
64
|
transitionDuration: "normal",
|
|
68
65
|
border: "1px solid",
|
|
69
66
|
borderColor: "gray.200",
|
|
70
|
-
borderRadius: "md",
|
|
71
67
|
bg: "white",
|
|
72
68
|
color: "inherit",
|
|
73
|
-
h: "10",
|
|
74
|
-
px: "4",
|
|
75
69
|
// As the input recipe, so a Select, a NativeSelect and a TextField in one
|
|
76
70
|
// form all tint together on hover. (react-aria's TextField has no hover
|
|
77
71
|
// effect, but matching the family beats matching their docs.)
|
|
@@ -207,4 +201,17 @@ export const select = defineSlotRecipe({
|
|
|
207
201
|
color: "gray.600",
|
|
208
202
|
},
|
|
209
203
|
},
|
|
204
|
+
variants: {
|
|
205
|
+
// The `input` recipe's size ladder, step for step, so a Select sits level
|
|
206
|
+
// with a TextField beside it at every size. The indicator is em-sized and
|
|
207
|
+
// scales free; the dropdown card keeps one density across sizes.
|
|
208
|
+
size: {
|
|
209
|
+
lg: { trigger: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" } },
|
|
210
|
+
md: { trigger: { fontSize: "md", px: "4", h: "10", borderRadius: "md" } },
|
|
211
|
+
sm: { trigger: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" } },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
defaultVariants: {
|
|
215
|
+
size: "md",
|
|
216
|
+
},
|
|
210
217
|
});
|
package/src/Select.tsx
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { createContext, ReactNode, useContext } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Button as RACButton,
|
|
9
|
-
Label as RACLabel,
|
|
10
9
|
ListBox as RACListBox,
|
|
11
10
|
ListBoxItem as RACListBoxItem,
|
|
12
11
|
ListBoxItemProps as RACListBoxItemProps,
|
|
@@ -18,13 +17,14 @@ import {
|
|
|
18
17
|
} from "react-aria-components";
|
|
19
18
|
import { RiArrowDownSLine } from "react-icons/ri";
|
|
20
19
|
import { css, cx } from "styled-system/css";
|
|
21
|
-
import { select, SelectVariantProps } from "styled-system/recipes";
|
|
20
|
+
import { field, select, SelectVariantProps } from "styled-system/recipes";
|
|
22
21
|
import { SystemStyleObject } from "styled-system/types";
|
|
23
22
|
import {
|
|
24
|
-
|
|
23
|
+
FieldLabel,
|
|
24
|
+
FieldLayoutProps,
|
|
25
25
|
FieldSupport,
|
|
26
26
|
FieldSupportProps,
|
|
27
|
-
} from "./
|
|
27
|
+
} from "./Field";
|
|
28
28
|
import { Icon } from "./Icon";
|
|
29
29
|
|
|
30
30
|
export type SelectSlots = ReturnType<typeof select>;
|
|
@@ -43,7 +43,8 @@ export interface SelectProps<T extends object>
|
|
|
43
43
|
"className" | "children" | "style" | "placeholder"
|
|
44
44
|
>,
|
|
45
45
|
SelectVariantProps,
|
|
46
|
-
FieldSupportProps
|
|
46
|
+
FieldSupportProps,
|
|
47
|
+
FieldLayoutProps {
|
|
47
48
|
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
48
49
|
label?: ReactNode;
|
|
49
50
|
/** Shown in the trigger while nothing is chosen (Chakra's placeholder). */
|
|
@@ -65,8 +66,8 @@ export interface SelectProps<T extends object>
|
|
|
65
66
|
* while positioning, which beats any class.
|
|
66
67
|
*/
|
|
67
68
|
maxHeight?: number;
|
|
68
|
-
/** Per-instance overrides for the trigger. */
|
|
69
|
-
|
|
69
|
+
/** Per-instance overrides for the trigger (the button the value sits in). */
|
|
70
|
+
triggerCss?: SystemStyleObject;
|
|
70
71
|
/** Per-instance overrides for the dropdown card. */
|
|
71
72
|
contentCss?: SystemStyleObject;
|
|
72
73
|
className?: string;
|
|
@@ -87,7 +88,8 @@ export const Select = <T extends object>({
|
|
|
87
88
|
helperText,
|
|
88
89
|
errorMessage,
|
|
89
90
|
helperTextCss,
|
|
90
|
-
|
|
91
|
+
labelPosition,
|
|
92
|
+
triggerCss,
|
|
91
93
|
contentCss,
|
|
92
94
|
className,
|
|
93
95
|
...props
|
|
@@ -96,20 +98,27 @@ export const Select = <T extends object>({
|
|
|
96
98
|
// groups to the recipe and they have to reach it (playbook gotcha #37).
|
|
97
99
|
const [variantProps, rest] = select.splitVariantProps(props);
|
|
98
100
|
const slots = select(variantProps);
|
|
101
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
99
102
|
return (
|
|
100
103
|
<SelectSlotProvider value={slots}>
|
|
101
104
|
<RACSelect
|
|
102
105
|
{...(rest as RACSelectProps<T>)}
|
|
103
|
-
className={cx(slots.root, className)}
|
|
106
|
+
className={cx(fieldSlots.root, slots.root, className)}
|
|
104
107
|
>
|
|
105
108
|
{label != null && (
|
|
106
|
-
<
|
|
109
|
+
<FieldLabel
|
|
110
|
+
size={variantProps.size}
|
|
111
|
+
labelPosition={labelPosition}
|
|
112
|
+
isRequired={props.isRequired}
|
|
113
|
+
>
|
|
107
114
|
{label}
|
|
108
|
-
|
|
109
|
-
</RACLabel>
|
|
115
|
+
</FieldLabel>
|
|
110
116
|
)}
|
|
111
117
|
<RACButton
|
|
112
|
-
className={cx(
|
|
118
|
+
className={cx(
|
|
119
|
+
slots.trigger,
|
|
120
|
+
triggerCss ? css(triggerCss) : undefined,
|
|
121
|
+
)}
|
|
113
122
|
>
|
|
114
123
|
<SelectValue className={slots.value}>
|
|
115
124
|
{({ isPlaceholder, defaultChildren }) =>
|
|
@@ -136,6 +145,7 @@ export const Select = <T extends object>({
|
|
|
136
145
|
helperText={helperText}
|
|
137
146
|
errorMessage={errorMessage}
|
|
138
147
|
helperTextCss={helperTextCss}
|
|
148
|
+
labelPosition={labelPosition}
|
|
139
149
|
/>
|
|
140
150
|
</RACSelect>
|
|
141
151
|
</SelectSlotProvider>
|
package/src/Switch.recipe.ts
CHANGED
|
@@ -72,6 +72,24 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
variants: {
|
|
75
|
+
// `start` puts the label first and the control at the row's end — the
|
|
76
|
+
// settings-row pattern (a preference name beside its switch), the toggle
|
|
77
|
+
// counterpart of the field recipe's `labelPosition="side"`. Values are
|
|
78
|
+
// start/end rather than top/side because the default label is already
|
|
79
|
+
// beside the control, after it. The label keeps an end margin so a long
|
|
80
|
+
// translation can't butt against the track (the SelectFormControl
|
|
81
|
+
// lesson — see the field-chrome roadmap bullet).
|
|
82
|
+
labelPosition: {
|
|
83
|
+
end: {},
|
|
84
|
+
start: {
|
|
85
|
+
root: {
|
|
86
|
+
flexDirection: "row-reverse",
|
|
87
|
+
justifyContent: "space-between",
|
|
88
|
+
width: "100%",
|
|
89
|
+
},
|
|
90
|
+
label: { marginStart: "0", marginEnd: "3" },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
75
93
|
// Chakra's Switch size scale (track width x height; the selected-thumb
|
|
76
94
|
// translate is the difference between them).
|
|
77
95
|
size: {
|
|
@@ -103,5 +121,6 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
103
121
|
},
|
|
104
122
|
defaultVariants: {
|
|
105
123
|
size: "md",
|
|
124
|
+
labelPosition: "end",
|
|
106
125
|
},
|
|
107
126
|
});
|
package/src/Switch.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import { ReactNode } from "react";
|
|
6
|
+
import { ReactNode, useId } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Switch as RACSwitch,
|
|
9
9
|
SwitchProps as RACSwitchProps,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { css, cx } from "styled-system/css";
|
|
12
12
|
import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
import { FieldHelperText } from "./Field";
|
|
14
15
|
|
|
15
16
|
export interface SwitchProps
|
|
16
17
|
extends Omit<RACSwitchProps, "className" | "children" | "style">,
|
|
@@ -19,24 +20,43 @@ export interface SwitchProps
|
|
|
19
20
|
css?: SystemStyleObject;
|
|
20
21
|
className?: string;
|
|
21
22
|
children?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Help text below the switch, wired to its `aria-describedby` — the same
|
|
25
|
+
* chrome the labelled fields' `helperText` renders. With it the component
|
|
26
|
+
* gains a wrapping `<div>`, so the switch-plus-text moves as one block.
|
|
27
|
+
*/
|
|
28
|
+
helperText?: ReactNode;
|
|
29
|
+
/** Per-instance style overrides for the helper text. */
|
|
30
|
+
helperTextCss?: SystemStyleObject;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Switch — react-aria-components <Switch> styled like Chakra's switch.
|
|
26
35
|
* Children render as the label; pass `aria-label` for label-less switches.
|
|
36
|
+
* `labelPosition="start"` is the settings-row layout: label first, switch at
|
|
37
|
+
* the row's end.
|
|
27
38
|
*/
|
|
28
39
|
export const Switch = ({
|
|
29
40
|
size,
|
|
41
|
+
labelPosition,
|
|
30
42
|
css: cssProp,
|
|
31
43
|
className,
|
|
32
44
|
children,
|
|
45
|
+
helperText,
|
|
46
|
+
helperTextCss,
|
|
33
47
|
...rest
|
|
34
48
|
}: SwitchProps) => {
|
|
35
|
-
const slots = switchRecipe({ size });
|
|
36
|
-
|
|
49
|
+
const slots = switchRecipe({ size, labelPosition });
|
|
50
|
+
const helperId = useId();
|
|
51
|
+
const describedBy =
|
|
52
|
+
[rest["aria-describedby"], helperText != null ? helperId : undefined]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(" ") || undefined;
|
|
55
|
+
const switchElement = (
|
|
37
56
|
<RACSwitch
|
|
38
57
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
39
58
|
{...rest}
|
|
59
|
+
aria-describedby={describedBy}
|
|
40
60
|
>
|
|
41
61
|
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
42
62
|
const state = {
|
|
@@ -59,4 +79,15 @@ export const Switch = ({
|
|
|
59
79
|
}}
|
|
60
80
|
</RACSwitch>
|
|
61
81
|
);
|
|
82
|
+
if (helperText == null) {
|
|
83
|
+
return switchElement;
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
{switchElement}
|
|
88
|
+
<FieldHelperText id={helperId} css={helperTextCss}>
|
|
89
|
+
{helperText}
|
|
90
|
+
</FieldHelperText>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
62
93
|
};
|
package/src/TextField.tsx
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
import { FocusEvent, forwardRef, ReactNode } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Input as RACInput,
|
|
9
|
-
Label as RACLabel,
|
|
10
9
|
TextField as RACTextField,
|
|
11
10
|
TextFieldProps as RACTextFieldProps,
|
|
12
11
|
} from "react-aria-components";
|
|
13
12
|
import { field, input, InputVariantProps } from "styled-system/recipes";
|
|
14
13
|
import {
|
|
15
|
-
|
|
14
|
+
FieldLabel,
|
|
15
|
+
FieldLayoutProps,
|
|
16
16
|
FieldSupport,
|
|
17
17
|
FieldSupportProps,
|
|
18
|
-
} from "./
|
|
18
|
+
} from "./Field";
|
|
19
19
|
|
|
20
20
|
export interface TextFieldProps
|
|
21
21
|
extends Omit<
|
|
@@ -23,7 +23,8 @@ export interface TextFieldProps
|
|
|
23
23
|
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
24
24
|
>,
|
|
25
25
|
InputVariantProps,
|
|
26
|
-
FieldSupportProps
|
|
26
|
+
FieldSupportProps,
|
|
27
|
+
FieldLayoutProps {
|
|
27
28
|
/** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
|
|
28
29
|
label: ReactNode;
|
|
29
30
|
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
@@ -43,6 +44,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
43
44
|
helperText,
|
|
44
45
|
errorMessage,
|
|
45
46
|
helperTextCss,
|
|
47
|
+
labelPosition,
|
|
46
48
|
onFocus,
|
|
47
49
|
autoCapitalize,
|
|
48
50
|
...props
|
|
@@ -52,13 +54,16 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
52
54
|
// As Input: forward every recipe variant group, not just `size`, so a
|
|
53
55
|
// preset that adds one keeps working.
|
|
54
56
|
const [variantProps, rest] = input.splitVariantProps(props);
|
|
55
|
-
const slots = field();
|
|
57
|
+
const slots = field({ size: variantProps.size, labelPosition });
|
|
56
58
|
return (
|
|
57
59
|
<RACTextField {...rest} className={slots.root}>
|
|
58
|
-
<
|
|
60
|
+
<FieldLabel
|
|
61
|
+
size={variantProps.size}
|
|
62
|
+
labelPosition={labelPosition}
|
|
63
|
+
isRequired={rest.isRequired}
|
|
64
|
+
>
|
|
59
65
|
{label}
|
|
60
|
-
|
|
61
|
-
</RACLabel>
|
|
66
|
+
</FieldLabel>
|
|
62
67
|
<RACInput
|
|
63
68
|
ref={ref}
|
|
64
69
|
className={input(variantProps)}
|
|
@@ -69,6 +74,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
69
74
|
helperText={helperText}
|
|
70
75
|
errorMessage={errorMessage}
|
|
71
76
|
helperTextCss={helperTextCss}
|
|
77
|
+
labelPosition={labelPosition}
|
|
72
78
|
/>
|
|
73
79
|
</RACTextField>
|
|
74
80
|
);
|
package/src/base-preset.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { switchRecipe } from "./Switch.recipe";
|
|
|
42
42
|
import { dialog } from "./Modal.recipe";
|
|
43
43
|
import { text } from "./Text.recipe";
|
|
44
44
|
import { tooltip } from "./Tooltip.recipe";
|
|
45
|
-
import { field } from "./
|
|
45
|
+
import { field } from "./Field.recipe";
|
|
46
46
|
import { toast } from "./Toast.recipe";
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -304,9 +304,11 @@ export const basePreset = definePreset({
|
|
|
304
304
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
305
305
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
306
306
|
drawer: ["*"],
|
|
307
|
+
field: ["*"],
|
|
307
308
|
gridList: ["*"],
|
|
308
309
|
listBox: ["*"],
|
|
309
310
|
input: ["*"],
|
|
311
|
+
numberField: ["*"],
|
|
310
312
|
radio: ["*"],
|
|
311
313
|
select: ["*"],
|
|
312
314
|
switchRecipe: ["*"],
|
package/src/index.ts
CHANGED
|
@@ -17,16 +17,18 @@ export * from "./ButtonGroup";
|
|
|
17
17
|
export * from "./Card";
|
|
18
18
|
export * from "./Checkbox";
|
|
19
19
|
export * from "./CheckboxGroup";
|
|
20
|
-
export * from "./
|
|
20
|
+
export * from "./Field";
|
|
21
21
|
export * from "./IconButton";
|
|
22
22
|
export * from "./Image";
|
|
23
23
|
export * from "./Input";
|
|
24
24
|
export * from "./InputGroup";
|
|
25
25
|
export * from "./LinkBox";
|
|
26
26
|
export * from "./NativeSelect";
|
|
27
|
+
export * from "./NativeSelectField";
|
|
27
28
|
export * from "./NumberField";
|
|
28
29
|
export * from "./ProgressBar";
|
|
29
30
|
export * from "./Radio";
|
|
31
|
+
export * from "./RadioGroup";
|
|
30
32
|
export * from "./Skeleton";
|
|
31
33
|
export * from "./Slide";
|
|
32
34
|
export * from "./Slider";
|