@cdx-ui/primitives 0.0.1-beta.141 → 0.0.1-beta.142
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/lib/commonjs/checkbox/CheckboxRoot.web.js +91 -28
- package/lib/commonjs/checkbox/CheckboxRoot.web.js.map +1 -1
- package/lib/commonjs/checkbox/useCheckboxRoot.js +33 -19
- package/lib/commonjs/checkbox/useCheckboxRoot.js.map +1 -1
- package/lib/commonjs/radio/RadioGroup.js +65 -16
- package/lib/commonjs/radio/RadioGroup.js.map +1 -1
- package/lib/commonjs/radio/RadioRoot.web.js +114 -26
- package/lib/commonjs/radio/RadioRoot.web.js.map +1 -1
- package/lib/commonjs/radio/context.js.map +1 -1
- package/lib/commonjs/radio/useRadioGroupNavigation.js +79 -0
- package/lib/commonjs/radio/useRadioGroupNavigation.js.map +1 -0
- package/lib/commonjs/radio/useRadioRoot.js +14 -13
- package/lib/commonjs/radio/useRadioRoot.js.map +1 -1
- package/lib/module/checkbox/CheckboxRoot.web.js +92 -29
- package/lib/module/checkbox/CheckboxRoot.web.js.map +1 -1
- package/lib/module/checkbox/useCheckboxRoot.js +34 -20
- package/lib/module/checkbox/useCheckboxRoot.js.map +1 -1
- package/lib/module/radio/RadioGroup.js +68 -19
- package/lib/module/radio/RadioGroup.js.map +1 -1
- package/lib/module/radio/RadioRoot.web.js +117 -29
- package/lib/module/radio/RadioRoot.web.js.map +1 -1
- package/lib/module/radio/context.js.map +1 -1
- package/lib/module/radio/useRadioGroupNavigation.js +74 -0
- package/lib/module/radio/useRadioGroupNavigation.js.map +1 -0
- package/lib/module/radio/useRadioRoot.js +14 -13
- package/lib/module/radio/useRadioRoot.js.map +1 -1
- package/lib/typescript/checkbox/CheckboxRoot.web.d.ts +8 -0
- package/lib/typescript/checkbox/CheckboxRoot.web.d.ts.map +1 -1
- package/lib/typescript/checkbox/useCheckboxRoot.d.ts +7 -18
- package/lib/typescript/checkbox/useCheckboxRoot.d.ts.map +1 -1
- package/lib/typescript/radio/RadioGroup.d.ts.map +1 -1
- package/lib/typescript/radio/RadioRoot.web.d.ts +8 -0
- package/lib/typescript/radio/RadioRoot.web.d.ts.map +1 -1
- package/lib/typescript/radio/context.d.ts +6 -1
- package/lib/typescript/radio/context.d.ts.map +1 -1
- package/lib/typescript/radio/types.d.ts +7 -0
- package/lib/typescript/radio/types.d.ts.map +1 -1
- package/lib/typescript/radio/useRadioGroupNavigation.d.ts +17 -0
- package/lib/typescript/radio/useRadioGroupNavigation.d.ts.map +1 -0
- package/lib/typescript/radio/useRadioRoot.d.ts +4 -18
- package/lib/typescript/radio/useRadioRoot.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkbox/CheckboxRoot.web.tsx +105 -29
- package/src/checkbox/useCheckboxRoot.ts +37 -15
- package/src/radio/RadioGroup.tsx +84 -20
- package/src/radio/RadioRoot.web.tsx +136 -31
- package/src/radio/context.tsx +6 -1
- package/src/radio/types.ts +6 -0
- package/src/radio/useRadioGroupNavigation.ts +104 -0
- package/src/radio/useRadioRoot.ts +18 -11
|
@@ -4,8 +4,10 @@ interface UseRadioRootOptions {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function useRadioRoot(props: IRadioProps, ref?: React.Ref<unknown>, { useInputRefForAria }?: UseRadioRootOptions): {
|
|
6
6
|
combinedProps: {
|
|
7
|
+
isInvalid: boolean | undefined;
|
|
8
|
+
isReadOnly: boolean;
|
|
9
|
+
isDisabled: boolean;
|
|
7
10
|
value: string;
|
|
8
|
-
isDisabled?: boolean | undefined;
|
|
9
11
|
isHovered?: boolean;
|
|
10
12
|
isFocused?: boolean;
|
|
11
13
|
isPressed?: boolean;
|
|
@@ -68,7 +70,7 @@ export declare function useRadioRoot(props: IRadioProps, ref?: React.Ref<unknown
|
|
|
68
70
|
accessibilityShowsLargeContentViewer?: boolean | undefined;
|
|
69
71
|
accessibilityLargeContentTitle?: string | undefined;
|
|
70
72
|
accessibilityRespondsToUserInteraction?: boolean | undefined;
|
|
71
|
-
id?: string | undefined;
|
|
73
|
+
id?: string | undefined | undefined;
|
|
72
74
|
onLayout?: ((event: import("react-native").LayoutChangeEvent) => void) | undefined | undefined;
|
|
73
75
|
testID?: string | undefined | undefined;
|
|
74
76
|
nativeID?: string | undefined | undefined;
|
|
@@ -116,22 +118,6 @@ export declare function useRadioRoot(props: IRadioProps, ref?: React.Ref<unknown
|
|
|
116
118
|
onPointerDownCapture?: ((event: import("react-native").PointerEvent) => void) | undefined | undefined;
|
|
117
119
|
onPointerUp?: ((event: import("react-native").PointerEvent) => void) | undefined | undefined;
|
|
118
120
|
onPointerUpCapture?: ((event: import("react-native").PointerEvent) => void) | undefined | undefined;
|
|
119
|
-
hasFeedbackText?: boolean | undefined;
|
|
120
|
-
hasHelpText?: boolean | undefined;
|
|
121
|
-
setHasFeedbackText?: import("react").Dispatch<import("react").SetStateAction<boolean>> | undefined;
|
|
122
|
-
setHasHelpText?: import("react").Dispatch<import("react").SetStateAction<boolean>> | undefined;
|
|
123
|
-
name?: string | undefined;
|
|
124
|
-
labelId?: string | undefined;
|
|
125
|
-
feedbackId?: string | undefined;
|
|
126
|
-
helpTextId?: string | undefined;
|
|
127
|
-
htmlProps?: {
|
|
128
|
-
[x: string]: unknown;
|
|
129
|
-
} | undefined;
|
|
130
|
-
inputRef?: React.RefObject<import("@cdx-ui/utils").Focusable | null> | undefined;
|
|
131
|
-
focusInput?: (() => void) | undefined;
|
|
132
|
-
isLabelFocused?: boolean | undefined;
|
|
133
|
-
setIsLabelFocused?: React.Dispatch<React.SetStateAction<boolean>> | undefined;
|
|
134
|
-
isRequired?: boolean | undefined;
|
|
135
121
|
};
|
|
136
122
|
isInvalid: boolean;
|
|
137
123
|
isReadOnly: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRadioRoot.d.ts","sourceRoot":"","sources":["../../../src/radio/useRadioRoot.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,UAAU,mBAAmB;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,EAClB,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EACxB,EAAE,kBAA0B,EAAE,GAAE,mBAAwB
|
|
1
|
+
{"version":3,"file":"useRadioRoot.d.ts","sourceRoot":"","sources":["../../../src/radio/useRadioRoot.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,UAAU,mBAAmB;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,EAClB,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EACxB,EAAE,kBAA0B,EAAE,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DzD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdx-ui/primitives",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.142",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"@react-stately/checkbox": "3.7.4",
|
|
62
62
|
"@react-stately/radio": "3.12.0",
|
|
63
63
|
"@react-stately/toggle": "3.9.4",
|
|
64
|
-
"@cdx-ui/formatters": "0.0.1-beta.
|
|
65
|
-
"@cdx-ui/utils": "0.0.1-beta.
|
|
64
|
+
"@cdx-ui/formatters": "0.0.1-beta.133",
|
|
65
|
+
"@cdx-ui/utils": "0.0.1-beta.142"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/react": "19.1.17",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef } from 'react';
|
|
2
2
|
import { Pressable, type PressableProps } from 'react-native';
|
|
3
3
|
import { VisuallyHidden } from '@react-aria/visually-hidden';
|
|
4
|
+
import { composeEventHandlers } from '@cdx-ui/utils';
|
|
4
5
|
import { useFocusRing } from '@react-native-aria/focus';
|
|
5
6
|
import { Slot } from '../slot';
|
|
6
7
|
import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
|
|
@@ -12,6 +13,14 @@ export interface CheckboxRootProps extends ICheckboxProps {
|
|
|
12
13
|
asChild?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Web Checkbox root.
|
|
18
|
+
*
|
|
19
|
+
* On web, `@react-native-aria/checkbox` re-exports `@react-aria/checkbox`'s
|
|
20
|
+
* `useCheckbox`, whose `inputProps` target a native `<input type="checkbox">`
|
|
21
|
+
* (selection via `onChange`). Toggle must be applied explicitly on the Pressable —
|
|
22
|
+
* `inputProps.onPress` does **not** call `toggleState.toggle()`.
|
|
23
|
+
*/
|
|
15
24
|
export const CheckboxRoot = forwardRef<React.ComponentRef<typeof Pressable>, CheckboxRootProps>(
|
|
16
25
|
({ asChild, children, ...props }, ref) => {
|
|
17
26
|
const {
|
|
@@ -37,18 +46,55 @@ export const CheckboxRoot = forwardRef<React.ComponentRef<typeof Pressable>, Che
|
|
|
37
46
|
isHovered,
|
|
38
47
|
mergedRef,
|
|
39
48
|
inputRef,
|
|
49
|
+
toggle,
|
|
40
50
|
} = useCheckboxRoot(props, ref, { useInputRefForAria: true });
|
|
41
51
|
|
|
42
52
|
const { focusProps, isFocusVisible } = useFocusRing();
|
|
43
53
|
|
|
54
|
+
// Strip native-input / internal props — they must not land on Pressable.
|
|
55
|
+
const {
|
|
56
|
+
onPress: _onPress,
|
|
57
|
+
onChange: _onChange,
|
|
58
|
+
role: _role,
|
|
59
|
+
tabIndex: _tabIndex,
|
|
60
|
+
disabled: _disabled,
|
|
61
|
+
checked: _checked,
|
|
62
|
+
value: _value,
|
|
63
|
+
type: _type,
|
|
64
|
+
name: _name,
|
|
65
|
+
form: _form,
|
|
66
|
+
required: _required,
|
|
67
|
+
...inputAriaProps
|
|
68
|
+
} = groupItemInputProps;
|
|
69
|
+
|
|
70
|
+
const resolvedChecked = !!(isChecked || isCheckedProp);
|
|
71
|
+
const resolvedIndeterminate = !!(isIndeterminate || isIndeterminateProp);
|
|
72
|
+
const resolvedDisabled = !!(isDisabled || isDisabledProp);
|
|
73
|
+
const resolvedReadOnly = !!(isReadOnly || isReadOnlyProp);
|
|
74
|
+
const ariaChecked = resolvedIndeterminate ? 'mixed' : resolvedChecked;
|
|
75
|
+
|
|
76
|
+
const handleToggle = () => {
|
|
77
|
+
if (resolvedDisabled || resolvedReadOnly) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
toggle();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleKeyDown = (event: { key: string; preventDefault: () => void }) => {
|
|
84
|
+
if (event.key === ' ' || event.key === 'Spacebar') {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
handleToggle();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
44
90
|
const contextValue: ICheckboxContextValue = {
|
|
45
|
-
isChecked:
|
|
46
|
-
isDisabled:
|
|
91
|
+
isChecked: resolvedChecked,
|
|
92
|
+
isDisabled: resolvedDisabled,
|
|
47
93
|
isFocusVisible: isFocusVisible || isFocusVisibleProp,
|
|
48
94
|
isHovered: isHovered || isHoveredProp,
|
|
49
95
|
isInvalid: isInvalid || isInvalidProp,
|
|
50
|
-
isReadOnly:
|
|
51
|
-
isIndeterminate:
|
|
96
|
+
isReadOnly: resolvedReadOnly,
|
|
97
|
+
isIndeterminate: resolvedIndeterminate,
|
|
52
98
|
isPressed,
|
|
53
99
|
isFocused,
|
|
54
100
|
};
|
|
@@ -57,33 +103,63 @@ export const CheckboxRoot = forwardRef<React.ComponentRef<typeof Pressable>, Che
|
|
|
57
103
|
|
|
58
104
|
return (
|
|
59
105
|
<CheckboxProvider value={contextValue}>
|
|
60
|
-
|
|
61
|
-
{...(combinedProps as PressableProps)}
|
|
62
|
-
ref={mergedRef}
|
|
63
|
-
tabIndex={-1}
|
|
64
|
-
role="label"
|
|
65
|
-
// eslint-disable-next-line react-native-a11y/has-valid-accessibility-role
|
|
66
|
-
accessibilityRole="label"
|
|
67
|
-
{...mergeDataAttributes(
|
|
68
|
-
props,
|
|
69
|
-
dataAttributes({
|
|
70
|
-
checked: isChecked || isCheckedProp,
|
|
71
|
-
disabled: isDisabled || isDisabledProp,
|
|
72
|
-
hover: isHovered || isHoveredProp,
|
|
73
|
-
invalid: isInvalid || isInvalidProp,
|
|
74
|
-
readonly: isReadOnly || isReadOnlyProp,
|
|
75
|
-
active: isPressed,
|
|
76
|
-
focus: isFocused,
|
|
77
|
-
indeterminate: isIndeterminate || isIndeterminateProp,
|
|
78
|
-
focusVisible: isFocusVisible,
|
|
79
|
-
}),
|
|
80
|
-
)}
|
|
81
|
-
>
|
|
106
|
+
<>
|
|
82
107
|
<VisuallyHidden>
|
|
83
|
-
<input
|
|
108
|
+
<input
|
|
109
|
+
ref={inputRef as React.RefObject<HTMLInputElement>}
|
|
110
|
+
type="checkbox"
|
|
111
|
+
value={combinedProps.value}
|
|
112
|
+
checked={resolvedChecked}
|
|
113
|
+
disabled={resolvedDisabled}
|
|
114
|
+
tabIndex={-1}
|
|
115
|
+
aria-hidden
|
|
116
|
+
readOnly
|
|
117
|
+
style={{ pointerEvents: 'none' }}
|
|
118
|
+
/>
|
|
84
119
|
</VisuallyHidden>
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
<Comp
|
|
121
|
+
{...(combinedProps as PressableProps)}
|
|
122
|
+
{...inputAriaProps}
|
|
123
|
+
ref={mergedRef}
|
|
124
|
+
role="checkbox"
|
|
125
|
+
aria-checked={ariaChecked}
|
|
126
|
+
aria-disabled={resolvedDisabled || undefined}
|
|
127
|
+
accessibilityRole="checkbox"
|
|
128
|
+
accessibilityState={{
|
|
129
|
+
checked: resolvedIndeterminate ? 'mixed' : resolvedChecked,
|
|
130
|
+
disabled: resolvedDisabled,
|
|
131
|
+
}}
|
|
132
|
+
tabIndex={resolvedDisabled ? -1 : 0}
|
|
133
|
+
disabled={resolvedDisabled}
|
|
134
|
+
{...focusProps}
|
|
135
|
+
onPress={handleToggle}
|
|
136
|
+
onClick={handleToggle}
|
|
137
|
+
onKeyDown={composeEventHandlers(
|
|
138
|
+
(
|
|
139
|
+
combinedProps as {
|
|
140
|
+
onKeyDown?: (e: { key: string; preventDefault: () => void }) => void;
|
|
141
|
+
}
|
|
142
|
+
).onKeyDown,
|
|
143
|
+
handleKeyDown,
|
|
144
|
+
)}
|
|
145
|
+
{...mergeDataAttributes(
|
|
146
|
+
props,
|
|
147
|
+
dataAttributes({
|
|
148
|
+
checked: resolvedChecked,
|
|
149
|
+
disabled: resolvedDisabled,
|
|
150
|
+
hover: isHovered || isHoveredProp,
|
|
151
|
+
invalid: isInvalid || isInvalidProp,
|
|
152
|
+
readonly: resolvedReadOnly,
|
|
153
|
+
active: isPressed,
|
|
154
|
+
focus: isFocused,
|
|
155
|
+
indeterminate: resolvedIndeterminate,
|
|
156
|
+
focusVisible: isFocusVisible || isFocusVisibleProp,
|
|
157
|
+
}),
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</Comp>
|
|
162
|
+
</>
|
|
87
163
|
</CheckboxProvider>
|
|
88
164
|
);
|
|
89
165
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useRef } from 'react';
|
|
1
|
+
import { useCallback, useContext, useRef } from 'react';
|
|
2
2
|
import { mergeRefs, useFormControlContext } from '@cdx-ui/utils';
|
|
3
3
|
import { useCheckbox } from '@react-native-aria/checkbox';
|
|
4
4
|
import { useHover } from '@react-native-aria/interactions';
|
|
@@ -16,29 +16,43 @@ export function useCheckboxRoot(
|
|
|
16
16
|
{ useInputRefForAria = false }: UseCheckboxRootOptions = {},
|
|
17
17
|
) {
|
|
18
18
|
const formControlContext = useFormControlContext();
|
|
19
|
+
const checkboxGroupContext = useContext(CheckboxGroupContext);
|
|
20
|
+
|
|
21
|
+
// Only pull field-state flags from FormControl — never spread the whole context
|
|
22
|
+
// (ids, setters, htmlProps) onto the host element.
|
|
23
|
+
const isInvalid = props.isInvalid ?? formControlContext.isInvalid;
|
|
24
|
+
const isReadOnly =
|
|
25
|
+
props.isReadOnly ?? formControlContext.isReadOnly ?? checkboxGroupContext?.state.isReadOnly;
|
|
26
|
+
const isDisabled =
|
|
27
|
+
props.isDisabled ?? formControlContext.isDisabled ?? checkboxGroupContext?.state.isDisabled;
|
|
28
|
+
const isIndeterminate = props.isIndeterminate;
|
|
19
29
|
|
|
20
|
-
const
|
|
21
|
-
...formControlContext,
|
|
30
|
+
const combinedProps = {
|
|
22
31
|
...props,
|
|
32
|
+
isInvalid,
|
|
33
|
+
isReadOnly,
|
|
34
|
+
isDisabled,
|
|
35
|
+
isIndeterminate,
|
|
23
36
|
};
|
|
24
37
|
|
|
25
|
-
const checkboxGroupContext = useContext(CheckboxGroupContext);
|
|
26
|
-
|
|
27
38
|
const state = useToggleState({
|
|
28
|
-
...combinedProps,
|
|
29
39
|
defaultSelected: props.defaultIsChecked,
|
|
30
40
|
isSelected: props.isChecked,
|
|
41
|
+
onChange: props.onChange,
|
|
42
|
+
isReadOnly,
|
|
43
|
+
isDisabled,
|
|
31
44
|
});
|
|
32
45
|
|
|
33
46
|
const rootRef = useRef(null);
|
|
34
47
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
35
48
|
const mergedRootRef = mergeRefs(ref as any, rootRef as any);
|
|
36
|
-
const ariaLabel =
|
|
49
|
+
const ariaLabel = props['aria-label'] || props.value || 'Checkbox';
|
|
37
50
|
|
|
38
51
|
const groupToggleState = useToggleState(
|
|
39
52
|
checkboxGroupContext
|
|
40
53
|
? {
|
|
41
|
-
isReadOnly: isReadOnly
|
|
54
|
+
isReadOnly: !!isReadOnly,
|
|
55
|
+
isDisabled: !!isDisabled,
|
|
42
56
|
isSelected: checkboxGroupContext.state.isSelected(combinedProps.value),
|
|
43
57
|
onChange(isSelected: boolean) {
|
|
44
58
|
if (isSelected) {
|
|
@@ -52,23 +66,30 @@ export function useCheckboxRoot(
|
|
|
52
66
|
: { isSelected: false },
|
|
53
67
|
);
|
|
54
68
|
|
|
69
|
+
const toggleState = checkboxGroupContext ? groupToggleState : state;
|
|
70
|
+
|
|
55
71
|
const { inputProps: groupItemInputProps } = useCheckbox(
|
|
56
72
|
{
|
|
57
73
|
...combinedProps,
|
|
58
74
|
'aria-label': ariaLabel,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
isDisabled: combinedProps.isDisabled || checkboxGroupContext.state.isDisabled,
|
|
62
|
-
}),
|
|
75
|
+
isReadOnly: !!isReadOnly,
|
|
76
|
+
isDisabled: !!isDisabled,
|
|
63
77
|
} as any,
|
|
64
|
-
|
|
78
|
+
toggleState,
|
|
65
79
|
(useInputRefForAria ? inputRef : rootRef) as any,
|
|
66
80
|
);
|
|
67
81
|
|
|
68
|
-
const { checked: isChecked, disabled:
|
|
82
|
+
const { checked: isChecked, disabled: isDisabledFromInput } = groupItemInputProps;
|
|
69
83
|
|
|
70
84
|
const { hoverProps, isHovered } = useHover({}, rootRef);
|
|
71
85
|
|
|
86
|
+
const toggle = useCallback(() => {
|
|
87
|
+
if (isDisabled || isReadOnly) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
toggleState.toggle();
|
|
91
|
+
}, [isDisabled, isReadOnly, toggleState]);
|
|
92
|
+
|
|
72
93
|
return {
|
|
73
94
|
combinedProps,
|
|
74
95
|
isInvalid,
|
|
@@ -76,10 +97,11 @@ export function useCheckboxRoot(
|
|
|
76
97
|
isIndeterminate,
|
|
77
98
|
groupItemInputProps,
|
|
78
99
|
isChecked,
|
|
79
|
-
isDisabled,
|
|
100
|
+
isDisabled: isDisabledFromInput,
|
|
80
101
|
isHovered,
|
|
81
102
|
hoverProps,
|
|
82
103
|
mergedRef: mergedRootRef,
|
|
83
104
|
inputRef,
|
|
105
|
+
toggle,
|
|
84
106
|
};
|
|
85
107
|
}
|
package/src/radio/RadioGroup.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { forwardRef } from 'react';
|
|
2
|
-
import { View, type ViewProps } from 'react-native';
|
|
3
|
-
import {
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Platform, View, type ViewProps } from 'react-native';
|
|
3
|
+
import { composeEventHandlers, useFormControl } from '@cdx-ui/utils';
|
|
4
4
|
import { useRadioGroup } from '@react-native-aria/radio';
|
|
5
5
|
import { useRadioGroupState } from '@react-stately/radio';
|
|
6
6
|
import { Slot } from '../slot';
|
|
7
7
|
import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
|
|
8
8
|
import { RadioGroupContext } from './context';
|
|
9
|
-
import type { IRadioGroupProps } from './types';
|
|
9
|
+
import type { IRadioGroupProps, RegisteredRadio } from './types';
|
|
10
|
+
import { useRadioGroupNavigation } from './useRadioGroupNavigation';
|
|
10
11
|
|
|
11
12
|
export interface RadioGroupRootProps extends IRadioGroupProps {
|
|
12
13
|
asChild?: boolean;
|
|
@@ -16,30 +17,86 @@ export interface RadioGroupRootProps extends IRadioGroupProps {
|
|
|
16
17
|
|
|
17
18
|
export const RadioGroup = forwardRef<View, RadioGroupRootProps>(
|
|
18
19
|
({ asChild, children, className, style, ...props }, ref) => {
|
|
19
|
-
const
|
|
20
|
+
const formControlProps = useFormControl({
|
|
21
|
+
isDisabled: props.isDisabled,
|
|
22
|
+
isRequired: props.isRequired,
|
|
23
|
+
isInvalid: props.isInvalid,
|
|
24
|
+
isReadOnly: props.isReadOnly,
|
|
25
|
+
});
|
|
20
26
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const isDisabled = props.isDisabled ?? Boolean(formControlProps.disabled);
|
|
28
|
+
const isInvalid = props.isInvalid ?? Boolean(formControlProps['aria-invalid']);
|
|
29
|
+
const isRequired = props.isRequired ?? Boolean(formControlProps.required);
|
|
30
|
+
const isReadOnly = props.isReadOnly ?? Boolean(formControlProps.readOnly);
|
|
31
|
+
const name = props.name ?? formControlProps.name;
|
|
32
|
+
const ariaLabel = props['aria-label'];
|
|
33
|
+
const ariaLabelledBy = props['aria-labelledby'] ?? formControlProps['aria-labelledby'];
|
|
34
|
+
const ariaDescribedBy = formControlProps['aria-describedby'];
|
|
25
35
|
|
|
26
36
|
const radioGroupState = useRadioGroupState({
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
name,
|
|
38
|
+
value: props.value,
|
|
39
|
+
defaultValue: props.defaultValue,
|
|
40
|
+
onChange: props.onChange,
|
|
41
|
+
isDisabled,
|
|
42
|
+
isInvalid,
|
|
43
|
+
isRequired,
|
|
44
|
+
isReadOnly,
|
|
45
|
+
validationState: isInvalid ? 'invalid' : 'valid',
|
|
29
46
|
});
|
|
30
47
|
|
|
31
48
|
const { radioGroupProps } = useRadioGroup(
|
|
32
49
|
{
|
|
33
|
-
|
|
34
|
-
'aria-
|
|
50
|
+
'aria-label': ariaLabel,
|
|
51
|
+
'aria-labelledby': ariaLabelledBy,
|
|
52
|
+
isDisabled,
|
|
35
53
|
},
|
|
36
54
|
radioGroupState,
|
|
37
55
|
);
|
|
38
56
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
57
|
+
const [focusedValue, setFocusedValue] = useState<string | undefined>(undefined);
|
|
58
|
+
// Identity of getRadios depends on this so roving tabIndex recomputes after register/unregister.
|
|
59
|
+
const [registrationEpoch, setRegistrationEpoch] = useState(0);
|
|
60
|
+
const radiosRef = useRef<Map<string, RegisteredRadio>>(new Map());
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setFocusedValue((prev) =>
|
|
64
|
+
prev !== undefined && prev !== radioGroupState.selectedValue ? undefined : prev,
|
|
65
|
+
);
|
|
66
|
+
}, [radioGroupState.selectedValue]);
|
|
67
|
+
|
|
68
|
+
const registerRadio = useCallback((value: string, entry: RegisteredRadio) => {
|
|
69
|
+
radiosRef.current.set(value, entry);
|
|
70
|
+
setRegistrationEpoch((epoch) => epoch + 1);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const unregisterRadio = useCallback((value: string) => {
|
|
74
|
+
radiosRef.current.delete(value);
|
|
75
|
+
setRegistrationEpoch((epoch) => epoch + 1);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const getRadios = useCallback(
|
|
79
|
+
() => Array.from(radiosRef.current.entries()),
|
|
80
|
+
[registrationEpoch],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const { handleKeyDown } = useRadioGroupNavigation({
|
|
84
|
+
getRadios,
|
|
85
|
+
focusedValue,
|
|
86
|
+
setFocusedValue,
|
|
87
|
+
selectedValue: radioGroupState.selectedValue,
|
|
88
|
+
selectValue: (value: string) => radioGroupState.setSelectedValue(value),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const webProps =
|
|
92
|
+
Platform.OS === 'web'
|
|
93
|
+
? {
|
|
94
|
+
onKeyDown: composeEventHandlers(
|
|
95
|
+
(props as { onKeyDown?: (e: KeyboardEvent) => void }).onKeyDown,
|
|
96
|
+
handleKeyDown,
|
|
97
|
+
),
|
|
98
|
+
}
|
|
99
|
+
: {};
|
|
43
100
|
|
|
44
101
|
const Comp = asChild ? Slot : View;
|
|
45
102
|
|
|
@@ -55,18 +112,25 @@ export const RadioGroup = forwardRef<View, RadioGroupRootProps>(
|
|
|
55
112
|
selectValue: (value: string) => radioGroupState.setSelectedValue(value),
|
|
56
113
|
},
|
|
57
114
|
radioGroupState,
|
|
58
|
-
name
|
|
115
|
+
name,
|
|
116
|
+
focusedValue,
|
|
117
|
+
setFocusedValue,
|
|
118
|
+
registerRadio,
|
|
119
|
+
unregisterRadio,
|
|
120
|
+
getRadios,
|
|
59
121
|
}}
|
|
60
122
|
>
|
|
61
123
|
<Comp
|
|
62
124
|
{...radioGroupProps}
|
|
63
|
-
aria-label={
|
|
64
|
-
aria-labelledby={
|
|
125
|
+
aria-label={ariaLabel}
|
|
126
|
+
aria-labelledby={ariaLabelledBy}
|
|
127
|
+
aria-describedby={ariaDescribedBy}
|
|
65
128
|
aria-required={isRequired || undefined}
|
|
66
129
|
aria-readonly={isReadOnly || undefined}
|
|
67
130
|
className={className}
|
|
68
131
|
style={style}
|
|
69
132
|
ref={ref}
|
|
133
|
+
{...webProps}
|
|
70
134
|
{...mergeDataAttributes(
|
|
71
135
|
props,
|
|
72
136
|
dataAttributes({
|