@oxy.so/bloom 3.0.0 → 3.1.0
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/docs/adoption-matrix.mdx +266 -0
- package/docs/checkbox.mdx +16 -0
- package/docs/composition.mdx +88 -16
- package/docs/date-picker.mdx +11 -0
- package/docs/field.mdx +35 -4
- package/docs/file-upload.mdx +13 -0
- package/docs/glass.mdx +15 -0
- package/docs/input-group.mdx +22 -0
- package/docs/input-otp.mdx +14 -0
- package/docs/layout.mdx +34 -0
- package/docs/migrating-to-3.0.mdx +43 -4
- package/docs/page-header.mdx +46 -1
- package/docs/phone-input.mdx +17 -0
- package/docs/radio.mdx +16 -0
- package/docs/rating.mdx +14 -0
- package/docs/segmented-control.mdx +16 -4
- package/docs/select.mdx +22 -0
- package/docs/slider.mdx +14 -0
- package/docs/stepper.mdx +3 -1
- package/docs/switch.mdx +14 -0
- package/docs/text-field.mdx +14 -0
- package/docs/textarea.mdx +20 -0
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +11 -4
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/checkbox/Checkbox.js +24 -8
- package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
- package/lib/commonjs/date-picker/TimeField.js +18 -6
- package/lib/commonjs/date-picker/TimeField.js.map +1 -1
- package/lib/commonjs/dialog/DialogContent.js +36 -29
- package/lib/commonjs/dialog/DialogContent.js.map +1 -1
- package/lib/commonjs/field/index.js +7 -0
- package/lib/commonjs/field/index.js.map +1 -1
- package/lib/commonjs/field/membership.js +105 -0
- package/lib/commonjs/field/membership.js.map +1 -0
- package/lib/commonjs/file-upload/FileUpload.js +17 -3
- package/lib/commonjs/file-upload/FileUpload.js.map +1 -1
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +21 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/input-group/InputGroup.js +18 -5
- package/lib/commonjs/input-group/InputGroup.js.map +1 -1
- package/lib/commonjs/input-otp/InputOtp.js +20 -8
- package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
- package/lib/commonjs/layout/index.js +7 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/screen-scope.js +61 -0
- package/lib/commonjs/layout/screen-scope.js.map +1 -0
- package/lib/commonjs/phone-input/PhoneInput.js +20 -8
- package/lib/commonjs/phone-input/PhoneInput.js.map +1 -1
- package/lib/commonjs/radio/Radio.js +35 -9
- package/lib/commonjs/radio/Radio.js.map +1 -1
- package/lib/commonjs/rating/RatingInput.js +14 -6
- package/lib/commonjs/rating/RatingInput.js.map +1 -1
- package/lib/commonjs/segmented-control/SegmentedControl.js +38 -8
- package/lib/commonjs/segmented-control/SegmentedControl.js.map +1 -1
- package/lib/commonjs/select/Select.js +15 -3
- package/lib/commonjs/select/Select.js.map +1 -1
- package/lib/commonjs/select/Select.web.js +13 -5
- package/lib/commonjs/select/Select.web.js.map +1 -1
- package/lib/commonjs/slider/Slider.js +17 -3
- package/lib/commonjs/slider/Slider.js.map +1 -1
- package/lib/commonjs/stepper/Stepper.js +16 -7
- package/lib/commonjs/stepper/Stepper.js.map +1 -1
- package/lib/commonjs/styles/styled-primitives.js.map +1 -1
- package/lib/commonjs/switch/Switch.js +23 -8
- package/lib/commonjs/switch/Switch.js.map +1 -1
- package/lib/commonjs/text-field/TextField.js +19 -4
- package/lib/commonjs/text-field/TextField.js.map +1 -1
- package/lib/commonjs/textarea/Textarea.js +39 -15
- package/lib/commonjs/textarea/Textarea.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +8 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/checkbox/Checkbox.js +24 -8
- package/lib/module/checkbox/Checkbox.js.map +1 -1
- package/lib/module/date-picker/TimeField.js +19 -5
- package/lib/module/date-picker/TimeField.js.map +1 -1
- package/lib/module/dialog/DialogContent.js +37 -30
- package/lib/module/dialog/DialogContent.js.map +1 -1
- package/lib/module/field/index.js +1 -0
- package/lib/module/field/index.js.map +1 -1
- package/lib/module/field/membership.js +101 -0
- package/lib/module/field/membership.js.map +1 -0
- package/lib/module/file-upload/FileUpload.js +17 -3
- package/lib/module/file-upload/FileUpload.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -2
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/input-group/InputGroup.js +19 -4
- package/lib/module/input-group/InputGroup.js.map +1 -1
- package/lib/module/input-otp/InputOtp.js +21 -7
- package/lib/module/input-otp/InputOtp.js.map +1 -1
- package/lib/module/layout/index.js +4 -1
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/screen-scope.js +57 -0
- package/lib/module/layout/screen-scope.js.map +1 -0
- package/lib/module/phone-input/PhoneInput.js +22 -9
- package/lib/module/phone-input/PhoneInput.js.map +1 -1
- package/lib/module/radio/Radio.js +35 -9
- package/lib/module/radio/Radio.js.map +1 -1
- package/lib/module/rating/RatingInput.js +15 -5
- package/lib/module/rating/RatingInput.js.map +1 -1
- package/lib/module/segmented-control/SegmentedControl.js +38 -8
- package/lib/module/segmented-control/SegmentedControl.js.map +1 -1
- package/lib/module/select/Select.js +15 -3
- package/lib/module/select/Select.js.map +1 -1
- package/lib/module/select/Select.web.js +14 -4
- package/lib/module/select/Select.web.js.map +1 -1
- package/lib/module/slider/Slider.js +17 -3
- package/lib/module/slider/Slider.js.map +1 -1
- package/lib/module/stepper/Stepper.js +17 -6
- package/lib/module/stepper/Stepper.js.map +1 -1
- package/lib/module/styles/styled-primitives.js.map +1 -1
- package/lib/module/switch/Switch.js +23 -8
- package/lib/module/switch/Switch.js.map +1 -1
- package/lib/module/text-field/TextField.js +19 -4
- package/lib/module/text-field/TextField.js.map +1 -1
- package/lib/module/textarea/Textarea.js +39 -15
- package/lib/module/textarea/Textarea.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkbox/types.d.ts +5 -0
- package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts +1 -1
- package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -1
- package/lib/typescript/commonjs/date-picker/types.d.ts +6 -3
- package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/index.d.ts +2 -0
- package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/membership.d.ts +60 -0
- package/lib/typescript/commonjs/field/membership.d.ts.map +1 -0
- package/lib/typescript/commonjs/file-upload/FileUpload.d.ts +1 -1
- package/lib/typescript/commonjs/file-upload/FileUpload.d.ts.map +1 -1
- package/lib/typescript/commonjs/floating/types.d.ts +13 -0
- package/lib/typescript/commonjs/floating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +3 -3
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-group/InputGroup.d.ts +1 -1
- package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-otp/types.d.ts +7 -1
- package/lib/typescript/commonjs/input-otp/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/index.d.ts +4 -1
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/screen-scope.d.ts +40 -0
- package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -0
- package/lib/typescript/commonjs/phone-input/PhoneInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/radio/Radio.d.ts +1 -1
- package/lib/typescript/commonjs/radio/Radio.d.ts.map +1 -1
- package/lib/typescript/commonjs/radio/types.d.ts +11 -3
- package/lib/typescript/commonjs/radio/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/RatingInput.d.ts +1 -1
- package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +6 -3
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts +18 -2
- package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/Select.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/Select.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/types.d.ts +8 -1
- package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/commonjs/stepper/types.d.ts +8 -1
- package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/styled-primitives.d.ts +16 -0
- package/lib/typescript/commonjs/styles/styled-primitives.d.ts.map +1 -1
- package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
- package/lib/typescript/commonjs/switch/types.d.ts +7 -0
- package/lib/typescript/commonjs/switch/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/commonjs/textarea/Textarea.d.ts +1 -1
- package/lib/typescript/commonjs/textarea/Textarea.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
- package/lib/typescript/module/checkbox/types.d.ts +5 -0
- package/lib/typescript/module/checkbox/types.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/TimeField.d.ts +1 -1
- package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -1
- package/lib/typescript/module/date-picker/types.d.ts +6 -3
- package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -1
- package/lib/typescript/module/field/index.d.ts +2 -0
- package/lib/typescript/module/field/index.d.ts.map +1 -1
- package/lib/typescript/module/field/membership.d.ts +60 -0
- package/lib/typescript/module/field/membership.d.ts.map +1 -0
- package/lib/typescript/module/file-upload/FileUpload.d.ts +1 -1
- package/lib/typescript/module/file-upload/FileUpload.d.ts.map +1 -1
- package/lib/typescript/module/floating/types.d.ts +13 -0
- package/lib/typescript/module/floating/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +3 -3
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/input-group/InputGroup.d.ts +1 -1
- package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
- package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
- package/lib/typescript/module/input-otp/types.d.ts +7 -1
- package/lib/typescript/module/input-otp/types.d.ts.map +1 -1
- package/lib/typescript/module/layout/index.d.ts +4 -1
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/screen-scope.d.ts +40 -0
- package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -0
- package/lib/typescript/module/phone-input/PhoneInput.d.ts.map +1 -1
- package/lib/typescript/module/radio/Radio.d.ts +1 -1
- package/lib/typescript/module/radio/Radio.d.ts.map +1 -1
- package/lib/typescript/module/radio/types.d.ts +11 -3
- package/lib/typescript/module/radio/types.d.ts.map +1 -1
- package/lib/typescript/module/rating/RatingInput.d.ts +1 -1
- package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +6 -3
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/segmented-control/SegmentedControl.d.ts +18 -2
- package/lib/typescript/module/segmented-control/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/module/select/Select.d.ts.map +1 -1
- package/lib/typescript/module/select/Select.web.d.ts.map +1 -1
- package/lib/typescript/module/select/types.d.ts +8 -1
- package/lib/typescript/module/select/types.d.ts.map +1 -1
- package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
- package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
- package/lib/typescript/module/stepper/types.d.ts +8 -1
- package/lib/typescript/module/stepper/types.d.ts.map +1 -1
- package/lib/typescript/module/styles/styled-primitives.d.ts +16 -0
- package/lib/typescript/module/styles/styled-primitives.d.ts.map +1 -1
- package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
- package/lib/typescript/module/switch/types.d.ts +7 -0
- package/lib/typescript/module/switch/types.d.ts.map +1 -1
- package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/module/textarea/Textarea.d.ts +1 -1
- package/lib/typescript/module/textarea/Textarea.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/bottom-sheet/BottomSheetBase.tsx +6 -1
- package/src/checkbox/Checkbox.tsx +24 -8
- package/src/checkbox/types.ts +5 -0
- package/src/date-picker/TimeField.tsx +14 -3
- package/src/date-picker/types.ts +6 -3
- package/src/dialog/DialogContent.tsx +6 -2
- package/src/field/index.ts +2 -0
- package/src/field/membership.ts +164 -0
- package/src/file-upload/FileUpload.tsx +14 -3
- package/src/floating/types.ts +13 -0
- package/src/index.ts +9 -2
- package/src/index.web.ts +9 -2
- package/src/input-group/InputGroup.tsx +13 -2
- package/src/input-otp/InputOtp.tsx +19 -5
- package/src/input-otp/types.ts +7 -1
- package/src/layout/index.ts +4 -1
- package/src/layout/screen-scope.tsx +54 -0
- package/src/phone-input/PhoneInput.tsx +20 -8
- package/src/radio/Radio.tsx +32 -9
- package/src/radio/types.ts +11 -3
- package/src/rating/RatingInput.tsx +10 -3
- package/src/rating/types.ts +6 -3
- package/src/segmented-control/SegmentedControl.tsx +51 -8
- package/src/select/Select.tsx +14 -2
- package/src/select/Select.web.tsx +11 -2
- package/src/select/types.ts +8 -1
- package/src/slider/Slider.tsx +13 -3
- package/src/stepper/Stepper.tsx +12 -4
- package/src/stepper/types.ts +8 -1
- package/src/styles/styled-primitives.ts +16 -0
- package/src/switch/Switch.tsx +22 -9
- package/src/switch/types.ts +7 -0
- package/src/text-field/TextField.tsx +16 -4
- package/src/textarea/Textarea.tsx +38 -15
package/src/radio/Radio.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveButtonRamps } from '../button/shared';
|
|
|
12
12
|
import { focusRingShadow, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
|
|
13
13
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
14
14
|
import { RadioIndicator } from '../radio-indicator';
|
|
15
|
+
import { useFieldMembership } from '../field/membership';
|
|
15
16
|
import { RadioCard } from './RadioCard';
|
|
16
17
|
import type { RadioGroupProps, RadioProps } from './types';
|
|
17
18
|
|
|
@@ -96,9 +97,20 @@ const RadioComponent = function Radio<Value extends string = string>({
|
|
|
96
97
|
style,
|
|
97
98
|
labelStyle,
|
|
98
99
|
accessibilityLabel,
|
|
100
|
+
nativeID,
|
|
99
101
|
testID,
|
|
100
102
|
}: RadioProps<Value>) {
|
|
101
103
|
const theme = useTheme();
|
|
104
|
+
// ADJACENT label (see `field/membership.ts`); the field's `disabled` is a
|
|
105
|
+
// constraint, so a radio inside a disabled field cannot re-enable itself.
|
|
106
|
+
const field = useFieldMembership({
|
|
107
|
+
accessibilityLabel,
|
|
108
|
+
label,
|
|
109
|
+
labelPlacement: 'adjacent',
|
|
110
|
+
disabled,
|
|
111
|
+
nativeID,
|
|
112
|
+
});
|
|
113
|
+
const isDisabled = field.disabled;
|
|
102
114
|
const ringOffset = useRingOffsetStyle();
|
|
103
115
|
useInteractiveWebCss(STYLE_ID, BLOOM_RADIO_CSS);
|
|
104
116
|
const sizeConfig = SIZE_CONFIG[size];
|
|
@@ -109,16 +121,16 @@ const RadioComponent = function Radio<Value extends string = string>({
|
|
|
109
121
|
// Re-choosing the chosen option is a no-op. A radio, unlike a checkbox, has
|
|
110
122
|
// no "off" — firing here would make a group's `onValueChange` report a
|
|
111
123
|
// change that did not happen.
|
|
112
|
-
if (
|
|
124
|
+
if (isDisabled || selected) return;
|
|
113
125
|
onSelect(value);
|
|
114
|
-
}, [
|
|
126
|
+
}, [isDisabled, selected, onSelect, value]);
|
|
115
127
|
|
|
116
128
|
const rowStyle: WebCssStyle = {
|
|
117
129
|
flexDirection: 'row',
|
|
118
130
|
alignItems: 'flex-start',
|
|
119
131
|
gap: LABEL_GAP,
|
|
120
132
|
// `opacity-50` on the whole row.
|
|
121
|
-
opacity:
|
|
133
|
+
opacity: isDisabled ? DISABLED_OPACITY : 1,
|
|
122
134
|
// The `:focus-visible` ring colour, read by the adopted sheet.
|
|
123
135
|
'--bloom-radio-ring': color ?? accent[500],
|
|
124
136
|
...ringOffset,
|
|
@@ -129,7 +141,9 @@ const RadioComponent = function Radio<Value extends string = string>({
|
|
|
129
141
|
{...(IS_WEB ? ({ dataSet: { bloomRadio: '' } } as Record<string, unknown>) : {})}
|
|
130
142
|
style={[rowStyle, style]}
|
|
131
143
|
onPress={handlePress}
|
|
132
|
-
disabled={
|
|
144
|
+
disabled={isDisabled}
|
|
145
|
+
nativeID={field.nativeID}
|
|
146
|
+
aria-describedby={field.describedBy}
|
|
133
147
|
accessibilityRole="radio"
|
|
134
148
|
// `aria-checked`, not `accessibilityState`: react-native-web never reads
|
|
135
149
|
// the latter, so an option that set only it announced no state at all.
|
|
@@ -137,7 +151,7 @@ const RadioComponent = function Radio<Value extends string = string>({
|
|
|
137
151
|
// this one prop is the spelling both platforms honour, and `disabled`
|
|
138
152
|
// travels on the `disabled` prop, which both map.
|
|
139
153
|
aria-checked={selected}
|
|
140
|
-
accessibilityLabel={accessibilityLabel
|
|
154
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
141
155
|
hitSlop={HIT_SLOP}
|
|
142
156
|
testID={testID}
|
|
143
157
|
>
|
|
@@ -196,12 +210,21 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
|
|
|
196
210
|
variant = 'default',
|
|
197
211
|
testID,
|
|
198
212
|
}: RadioGroupProps<Value>) {
|
|
213
|
+
// The group is one control made of several, so a `Field` around it names the
|
|
214
|
+
// GROUP and disables every option — `multiple` is the field's side of that
|
|
215
|
+
// (`docs/field.mdx`), and each radio keeps its own id and its own name.
|
|
216
|
+
// `label` here is a NAME, not rendered text — the group draws no label of its
|
|
217
|
+
// own — so it goes in as the caller's name and outranks the field's.
|
|
218
|
+
const field = useFieldMembership({ accessibilityLabel: label, disabled });
|
|
219
|
+
const isDisabled = field.disabled;
|
|
199
220
|
return (
|
|
200
221
|
<View
|
|
201
222
|
style={[{ gap: space.sm }, style]}
|
|
202
223
|
accessibilityRole="radiogroup"
|
|
203
|
-
accessibilityLabel={
|
|
204
|
-
aria-label={
|
|
224
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
225
|
+
aria-label={field.accessibilityLabel}
|
|
226
|
+
aria-describedby={field.describedBy}
|
|
227
|
+
aria-invalid={field.invalid || undefined}
|
|
205
228
|
testID={testID}
|
|
206
229
|
>
|
|
207
230
|
{options.map((option) =>
|
|
@@ -213,7 +236,7 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
|
|
|
213
236
|
onSelect={onValueChange}
|
|
214
237
|
title={option.label ?? option.value}
|
|
215
238
|
description={option.description}
|
|
216
|
-
disabled={
|
|
239
|
+
disabled={isDisabled || option.disabled === true}
|
|
217
240
|
color={color}
|
|
218
241
|
testID={option.testID}
|
|
219
242
|
/>
|
|
@@ -226,7 +249,7 @@ const RadioGroupComponent = function RadioGroup<Value extends string = string>({
|
|
|
226
249
|
label={option.label}
|
|
227
250
|
description={option.description}
|
|
228
251
|
size={size}
|
|
229
|
-
disabled={
|
|
252
|
+
disabled={isDisabled || option.disabled === true}
|
|
230
253
|
color={color}
|
|
231
254
|
labelStyle={labelStyle}
|
|
232
255
|
testID={option.testID}
|
package/src/radio/types.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface RadioProps<Value extends string = string> {
|
|
|
30
30
|
style?: StyleProp<ViewStyle>;
|
|
31
31
|
labelStyle?: StyleProp<TextStyle>;
|
|
32
32
|
accessibilityLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The control's id. Inside a `Field` the field supplies one, so the label's
|
|
35
|
+
* `htmlFor` points at this control on web.
|
|
36
|
+
*/
|
|
37
|
+
nativeID?: string;
|
|
33
38
|
testID?: string;
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -44,10 +49,13 @@ export interface RadioOption<Value extends string = string> {
|
|
|
44
49
|
|
|
45
50
|
export interface RadioGroupProps<Value extends string = string> {
|
|
46
51
|
/**
|
|
47
|
-
* The group's accessible name.
|
|
48
|
-
*
|
|
52
|
+
* The group's accessible name.
|
|
53
|
+
*
|
|
54
|
+
* A `radiogroup` with no name announces a list of options and nothing about
|
|
55
|
+
* what is being chosen, so this or an enclosing `Field`'s label has to supply
|
|
56
|
+
* one.
|
|
49
57
|
*/
|
|
50
|
-
label
|
|
58
|
+
label?: string;
|
|
51
59
|
/** The selected value, or `undefined` for a group with nothing chosen yet. */
|
|
52
60
|
value: Value | undefined;
|
|
53
61
|
/** Called with the newly chosen value. */
|
|
@@ -11,6 +11,7 @@ import { webDataSet } from '../styles/web-data';
|
|
|
11
11
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
12
12
|
import { useTheme } from '../theme/use-theme';
|
|
13
13
|
import type { RatingInputProps, RatingInputSize } from './types';
|
|
14
|
+
import { useFieldMembership } from '../field/membership';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The star PICKER — `Rating`'s counterpart for a review form. `Rating` draws a
|
|
@@ -83,13 +84,17 @@ function RatingInputComponent({
|
|
|
83
84
|
onChange,
|
|
84
85
|
max = 5,
|
|
85
86
|
size = 'medium',
|
|
86
|
-
disabled = false,
|
|
87
|
+
disabled: disabledProp = false,
|
|
87
88
|
accessibilityLabel,
|
|
88
89
|
formatStarLabel = defaultStarLabel,
|
|
89
90
|
style,
|
|
90
91
|
testID,
|
|
91
92
|
}: RatingInputProps) {
|
|
92
93
|
const theme = useTheme();
|
|
94
|
+
// A row of stars draws no words, so the `radiogroup`'s name is a prop or the
|
|
95
|
+
// enclosing `Field`'s label; the field's `disabled` freezes the whole row.
|
|
96
|
+
const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
|
|
97
|
+
const disabled = field.disabled;
|
|
93
98
|
const ringOffset = useRingOffsetStyle();
|
|
94
99
|
useInteractiveWebCss(STYLE_ID, BLOOM_RATING_INPUT_CSS);
|
|
95
100
|
const { accent, neutral } = useMemo(() => resolveButtonRamps(theme), [theme]);
|
|
@@ -158,8 +163,10 @@ function RatingInputComponent({
|
|
|
158
163
|
<View
|
|
159
164
|
testID={testID}
|
|
160
165
|
accessibilityRole="radiogroup"
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
nativeID={field.nativeID}
|
|
167
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
168
|
+
aria-label={field.accessibilityLabel}
|
|
169
|
+
aria-describedby={field.describedBy}
|
|
163
170
|
aria-disabled={disabled || undefined}
|
|
164
171
|
style={[
|
|
165
172
|
{
|
package/src/rating/types.ts
CHANGED
|
@@ -51,10 +51,13 @@ export interface RatingInputProps {
|
|
|
51
51
|
disabled?: boolean;
|
|
52
52
|
/**
|
|
53
53
|
* The NAME of what is being rated ("Overall rating", "Cleanliness").
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* The group draws nothing but stars, so this or an enclosing `Field`'s label
|
|
56
|
+
* is the only thing that can tell a screen reader which of several ratings
|
|
57
|
+
* this one is. With neither it warns once in development
|
|
58
|
+
* (`hooks/use-accessible-name-warning.ts`).
|
|
56
59
|
*/
|
|
57
|
-
accessibilityLabel
|
|
60
|
+
accessibilityLabel?: string;
|
|
58
61
|
/**
|
|
59
62
|
* Each star's own name, which is English by default (`"1 star"`,
|
|
60
63
|
* `"4 stars"`). Pass a translated formatter.
|
|
@@ -27,6 +27,7 @@ import { borderRadius, DISABLED_OPACITY } from '../styles/tokens';
|
|
|
27
27
|
import { NOT_DISABLED, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
|
|
28
28
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
29
29
|
import { mixColor, resolveButtonRamps } from '../button/shared';
|
|
30
|
+
import { useFieldMembership } from '../field/membership';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* A segmented control. Colours come from Bloom's theme through
|
|
@@ -136,6 +137,8 @@ const InternalContext = createContext<{
|
|
|
136
137
|
size: SegmentedControlSize;
|
|
137
138
|
variant: SegmentedControlVariant;
|
|
138
139
|
palette: SegmentedPalette;
|
|
140
|
+
/** The group's disabled state. An item ORs it with its own. */
|
|
141
|
+
disabled: boolean;
|
|
139
142
|
selectedValue: string;
|
|
140
143
|
selectedPosition: { width: number; x: number } | null;
|
|
141
144
|
onSelectValue: (
|
|
@@ -169,9 +172,17 @@ export function SegmentedControl<T extends string>({
|
|
|
169
172
|
onChange,
|
|
170
173
|
children,
|
|
171
174
|
style,
|
|
175
|
+
disabled = false,
|
|
172
176
|
accessibilityHint,
|
|
177
|
+
nativeID,
|
|
178
|
+
testID,
|
|
173
179
|
}: {
|
|
174
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The control's accessible name — it is a `radiogroup`/`tablist`, and the
|
|
182
|
+
* segment texts name the OPTIONS rather than the choice. Inside a `Field` the
|
|
183
|
+
* field's label supplies it.
|
|
184
|
+
*/
|
|
185
|
+
label?: string;
|
|
175
186
|
type: 'tabs' | 'radio';
|
|
176
187
|
/** `medium` (default); `small` and `large` step around it. */
|
|
177
188
|
size?: SegmentedControlSize;
|
|
@@ -181,10 +192,28 @@ export function SegmentedControl<T extends string>({
|
|
|
181
192
|
onChange: (value: T) => void;
|
|
182
193
|
children: React.ReactNode;
|
|
183
194
|
style?: StyleProp<ViewStyle>;
|
|
195
|
+
/**
|
|
196
|
+
* Disables every segment.
|
|
197
|
+
*
|
|
198
|
+
* A CONSTRAINT the group owns: an item combines it with `||`, so a segment
|
|
199
|
+
* cannot re-enable itself, and a `Field disabled` around the control reaches
|
|
200
|
+
* the segments through it.
|
|
201
|
+
*/
|
|
202
|
+
disabled?: boolean;
|
|
184
203
|
accessibilityHint?: string;
|
|
204
|
+
/** The group's id. Supplied by an enclosing `Field` when there is one. */
|
|
205
|
+
nativeID?: string;
|
|
206
|
+
testID?: string;
|
|
185
207
|
}) {
|
|
186
208
|
const theme = useTheme();
|
|
187
209
|
useInteractiveWebCss(STYLE_ID, SEGMENTED_CSS);
|
|
210
|
+
// The group is ONE control made of several segments, so a `Field` names the
|
|
211
|
+
// group and disables all of it. `label` is the group's name rather than
|
|
212
|
+
// rendered text, so the field's label only fills in what is missing.
|
|
213
|
+
// `label` is a NAME rather than rendered text (the segments carry the words),
|
|
214
|
+
// so it goes in as the caller's own name and outranks the field's.
|
|
215
|
+
const field = useFieldMembership({ accessibilityLabel: label, disabled, nativeID });
|
|
216
|
+
const isDisabled = field.disabled;
|
|
188
217
|
const palette = useMemo(() => resolveSegmentedPalette(theme), [theme]);
|
|
189
218
|
const [selectedPosition, setSelectedPosition] = useState<{
|
|
190
219
|
width: number;
|
|
@@ -197,6 +226,7 @@ export function SegmentedControl<T extends string>({
|
|
|
197
226
|
size,
|
|
198
227
|
variant,
|
|
199
228
|
palette,
|
|
229
|
+
disabled: isDisabled,
|
|
200
230
|
selectedValue: value,
|
|
201
231
|
selectedPosition,
|
|
202
232
|
onSelectValue: (
|
|
@@ -219,7 +249,7 @@ export function SegmentedControl<T extends string>({
|
|
|
219
249
|
});
|
|
220
250
|
},
|
|
221
251
|
};
|
|
222
|
-
}, [value, selectedPosition, setSelectedPosition, onChange, type, size, variant, palette]);
|
|
252
|
+
}, [value, selectedPosition, setSelectedPosition, onChange, type, size, variant, palette, isDisabled]);
|
|
223
253
|
|
|
224
254
|
const solid = variant === 'solid';
|
|
225
255
|
const padding = solid ? TRACK_PADDING : 0;
|
|
@@ -231,8 +261,15 @@ export function SegmentedControl<T extends string>({
|
|
|
231
261
|
|
|
232
262
|
return (
|
|
233
263
|
<View
|
|
234
|
-
|
|
264
|
+
testID={testID}
|
|
265
|
+
nativeID={field.nativeID}
|
|
266
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
235
267
|
accessibilityHint={accessibilityHint ?? ''}
|
|
268
|
+
aria-describedby={field.describedBy}
|
|
269
|
+
aria-invalid={field.invalid || undefined}
|
|
270
|
+
// A `View`, so `aria-disabled` is the only spelling that reaches web —
|
|
271
|
+
// there is no `disabled` prop for react-native-web to derive it from.
|
|
272
|
+
aria-disabled={isDisabled || undefined}
|
|
236
273
|
style={[
|
|
237
274
|
{
|
|
238
275
|
position: 'relative',
|
|
@@ -320,10 +357,16 @@ export function SegmentedControlItem({
|
|
|
320
357
|
}
|
|
321
358
|
}, [needsUpdate]);
|
|
322
359
|
|
|
360
|
+
// The group's `disabled` is a constraint, so it is OR-ed with the item's own
|
|
361
|
+
// — a segment cannot re-enable itself inside a disabled group, and a `Field
|
|
362
|
+
// disabled` reaches every segment through the group.
|
|
363
|
+
const isDisabled = ctx.disabled || disabled === true;
|
|
364
|
+
|
|
323
365
|
const onPress = useCallback(() => {
|
|
366
|
+
if (isDisabled) return;
|
|
324
367
|
ctx.onSelectValue(value, position);
|
|
325
368
|
onPressProp?.();
|
|
326
|
-
}, [ctx, value, position, onPressProp]);
|
|
369
|
+
}, [isDisabled, ctx, value, position, onPressProp]);
|
|
327
370
|
|
|
328
371
|
// We render the segment as a flat `Pressable` (not Bloom's `Button`)
|
|
329
372
|
// for two reasons:
|
|
@@ -346,13 +389,13 @@ export function SegmentedControlItem({
|
|
|
346
389
|
height: geometry.height,
|
|
347
390
|
paddingHorizontal: geometry.paddingHorizontal,
|
|
348
391
|
borderRadius: borderRadius.full,
|
|
349
|
-
opacity:
|
|
392
|
+
opacity: isDisabled ? DISABLED_OPACITY : 1,
|
|
350
393
|
'--bloom-segmented-ring': ctx.palette.ring,
|
|
351
394
|
};
|
|
352
395
|
|
|
353
396
|
const itemContext = useMemo(
|
|
354
|
-
() => ({ active, hovered: hovered && !
|
|
355
|
-
[active, hovered,
|
|
397
|
+
() => ({ active, hovered: hovered && !isDisabled }),
|
|
398
|
+
[active, hovered, isDisabled],
|
|
356
399
|
);
|
|
357
400
|
|
|
358
401
|
return (
|
|
@@ -384,7 +427,7 @@ export function SegmentedControlItem({
|
|
|
384
427
|
? { 'aria-selected': active }
|
|
385
428
|
: { 'aria-checked': active })}
|
|
386
429
|
role={itemRole}
|
|
387
|
-
disabled={
|
|
430
|
+
disabled={isDisabled}
|
|
388
431
|
testID={testID}
|
|
389
432
|
style={[itemStyle, style]}>
|
|
390
433
|
<InternalItemContext.Provider value={itemContext}>
|
package/src/select/Select.tsx
CHANGED
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
SelectTriggerProps,
|
|
58
58
|
SelectValueProps,
|
|
59
59
|
} from './types';
|
|
60
|
+
import { useFieldMembership } from '../field/membership';
|
|
60
61
|
|
|
61
62
|
// ---------------------------------------------------------------------------
|
|
62
63
|
// Context
|
|
@@ -133,7 +134,15 @@ export function SelectTrigger({
|
|
|
133
134
|
}: SelectTriggerProps) {
|
|
134
135
|
const { control, size, disabled: rootDisabled } = useSelectContext();
|
|
135
136
|
const palette = useMenuPalette();
|
|
136
|
-
|
|
137
|
+
// The trigger IS the select's control, so it takes the enclosing `Field`'s id,
|
|
138
|
+
// name, description and invalid state — and the field's `disabled` on top of
|
|
139
|
+
// the two it already combines. A `Popover` trigger deliberately does not do
|
|
140
|
+
// this (`floating/types.ts`): it is not the field's control.
|
|
141
|
+
const membership = useFieldMembership({
|
|
142
|
+
accessibilityLabel: label,
|
|
143
|
+
disabled: disabled === true || rootDisabled === true,
|
|
144
|
+
});
|
|
145
|
+
const isDisabled = membership.disabled;
|
|
137
146
|
const t = palette.trigger;
|
|
138
147
|
|
|
139
148
|
// The same field the web fork draws — a bordered white select trigger
|
|
@@ -171,9 +180,12 @@ export function SelectTrigger({
|
|
|
171
180
|
handle={{
|
|
172
181
|
onPress: () => control.open(),
|
|
173
182
|
disabled: isDisabled,
|
|
174
|
-
accessibilityLabel:
|
|
183
|
+
accessibilityLabel: membership.accessibilityLabel,
|
|
175
184
|
accessibilityRole: 'button',
|
|
176
185
|
'aria-haspopup': SELECT_TRIGGER_POPUP,
|
|
186
|
+
nativeID: membership.nativeID,
|
|
187
|
+
'aria-describedby': membership.describedBy,
|
|
188
|
+
'aria-invalid': membership.invalid || undefined,
|
|
177
189
|
}}>
|
|
178
190
|
{asChild ? children : field}
|
|
179
191
|
</TriggerSlot>
|
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
SelectTriggerProps,
|
|
60
60
|
SelectValueProps,
|
|
61
61
|
} from './types';
|
|
62
|
+
import { useFieldMembership } from '../field/membership';
|
|
62
63
|
|
|
63
64
|
/** The trigger's `offset={4}`. */
|
|
64
65
|
const SELECT_OFFSET = 4;
|
|
@@ -209,7 +210,12 @@ export function SelectTrigger({
|
|
|
209
210
|
}: SelectTriggerProps) {
|
|
210
211
|
const ctx = useSelectContext();
|
|
211
212
|
const palette = useMenuPalette();
|
|
212
|
-
|
|
213
|
+
// The trigger IS the select's control — see the native fork.
|
|
214
|
+
const membership = useFieldMembership({
|
|
215
|
+
accessibilityLabel: label,
|
|
216
|
+
disabled: disabled === true || ctx.disabled === true,
|
|
217
|
+
});
|
|
218
|
+
const isDisabled = membership.disabled;
|
|
213
219
|
useEffect(() => {
|
|
214
220
|
adoptStyleSheet(TRIGGER_STYLE_ID, TRIGGER_CSS);
|
|
215
221
|
}, []);
|
|
@@ -261,10 +267,13 @@ export function SelectTrigger({
|
|
|
261
267
|
// it rather than reopening it.
|
|
262
268
|
onPress: () => (ctx.isOpen ? ctx.close() : ctx.open()),
|
|
263
269
|
disabled: isDisabled,
|
|
264
|
-
accessibilityLabel:
|
|
270
|
+
accessibilityLabel: membership.accessibilityLabel,
|
|
265
271
|
accessibilityRole: 'button',
|
|
266
272
|
'aria-haspopup': SELECT_TRIGGER_POPUP,
|
|
267
273
|
'aria-expanded': ctx.isOpen,
|
|
274
|
+
nativeID: membership.nativeID,
|
|
275
|
+
'aria-describedby': membership.describedBy,
|
|
276
|
+
'aria-invalid': membership.invalid || undefined,
|
|
268
277
|
}}
|
|
269
278
|
>
|
|
270
279
|
{asChild ? children : field}
|
package/src/select/types.ts
CHANGED
|
@@ -26,7 +26,14 @@ export type SelectTriggerProps = {
|
|
|
26
26
|
*/
|
|
27
27
|
asChild?: boolean;
|
|
28
28
|
disabled?: boolean;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The trigger's accessible name.
|
|
31
|
+
*
|
|
32
|
+
* The trigger draws the chosen value, which is not a name — "Medium" says
|
|
33
|
+
* nothing about what is medium. Inside a `Field` the field's label supplies
|
|
34
|
+
* it; with neither the trigger announces nothing.
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
30
37
|
/**
|
|
31
38
|
* Utility classes APPENDED to the part's own — never substituted for them, so
|
|
32
39
|
* a single layout class cannot strip the chrome.
|
package/src/slider/Slider.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { clamp } from '../styles/clamp';
|
|
|
22
22
|
import { BUTTON_SHADOW, mixColor, resolveButtonRamps } from '../button/shared';
|
|
23
23
|
import { useAccessibleNameWarning } from '../hooks/use-accessible-name-warning';
|
|
24
24
|
import type { RangeSliderProps, SliderProps } from './types';
|
|
25
|
+
import { useFieldMembership } from '../field/membership';
|
|
25
26
|
|
|
26
27
|
function quantize(raw: number, min: number, max: number, step: number): number {
|
|
27
28
|
if (step <= 0) return clamp(raw, min, max);
|
|
@@ -184,7 +185,7 @@ function SliderBase({
|
|
|
184
185
|
min,
|
|
185
186
|
max,
|
|
186
187
|
step,
|
|
187
|
-
disabled,
|
|
188
|
+
disabled: disabledProp,
|
|
188
189
|
trackHeight,
|
|
189
190
|
thumbSize,
|
|
190
191
|
minimumTrackTintColor,
|
|
@@ -199,6 +200,12 @@ function SliderBase({
|
|
|
199
200
|
testID,
|
|
200
201
|
}: SliderBaseProps) {
|
|
201
202
|
const theme = useTheme();
|
|
203
|
+
// A slider draws no text of its own — its `label` is rendered but hidden from
|
|
204
|
+
// assistive technology, which is why the name has to be a prop. Inside a
|
|
205
|
+
// `Field` the field's label is that name (STACKED placement), and the field's
|
|
206
|
+
// `disabled` constrains the slider whatever the caller passed.
|
|
207
|
+
const field = useFieldMembership({ accessibilityLabel, label, disabled: disabledProp });
|
|
208
|
+
const disabled = field.disabled;
|
|
202
209
|
const ringOffset = useRingOffsetStyle();
|
|
203
210
|
React.useEffect(() => {
|
|
204
211
|
adoptStyleSheet(STYLE_ID, BLOOM_SLIDER_CSS);
|
|
@@ -365,11 +372,14 @@ function SliderBase({
|
|
|
365
372
|
return (
|
|
366
373
|
<View
|
|
367
374
|
testID={testID}
|
|
375
|
+
nativeID={field.nativeID}
|
|
376
|
+
aria-describedby={field.describedBy}
|
|
377
|
+
aria-invalid={field.invalid || undefined}
|
|
368
378
|
{...(isRange
|
|
369
|
-
? { accessibilityLabel: accessibilityLabel
|
|
379
|
+
? { accessibilityLabel: field.accessibilityLabel }
|
|
370
380
|
: {
|
|
371
381
|
accessibilityRole: 'adjustable' as const,
|
|
372
|
-
accessibilityLabel: accessibilityLabel
|
|
382
|
+
accessibilityLabel: field.accessibilityLabel,
|
|
373
383
|
// react-native-web reads the FLAT `aria-value*` props; it has no
|
|
374
384
|
// handling for the `accessibilityValue` object at all, so this
|
|
375
385
|
// rendered a `role="slider"` carrying no value whatsoever. React
|
package/src/stepper/Stepper.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { useTheme } from '../theme/use-theme';
|
|
|
14
14
|
import { Text } from '../typography';
|
|
15
15
|
import type { TypeScaleVariant } from '../typography/scale';
|
|
16
16
|
import type { StepperProps, StepperSize } from './types';
|
|
17
|
+
import { useFieldMembership } from '../field/membership';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Bloom's numeric counter: a round `−` button, the value, a round `+` button.
|
|
@@ -77,7 +78,7 @@ function StepperComponent({
|
|
|
77
78
|
min = 0,
|
|
78
79
|
max,
|
|
79
80
|
step = 1,
|
|
80
|
-
disabled = false,
|
|
81
|
+
disabled: disabledProp = false,
|
|
81
82
|
size = 'medium',
|
|
82
83
|
formatValue,
|
|
83
84
|
accessibilityLabel,
|
|
@@ -88,7 +89,12 @@ function StepperComponent({
|
|
|
88
89
|
}: StepperProps) {
|
|
89
90
|
const theme = useTheme();
|
|
90
91
|
const ringOffset = useRingOffsetStyle();
|
|
91
|
-
|
|
92
|
+
// The digits are not a name, so the stepper is named by a prop or by the
|
|
93
|
+
// enclosing `Field` — and the warning has to see the resolved name, or a
|
|
94
|
+
// field-labelled stepper warns anyway.
|
|
95
|
+
const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
|
|
96
|
+
const disabled = field.disabled;
|
|
97
|
+
useAccessibleNameWarning('Stepper', field.accessibilityLabel);
|
|
92
98
|
useInteractiveWebCss(STYLE_ID, BLOOM_STEPPER_CSS);
|
|
93
99
|
const { accent } = useMemo(() => resolveButtonRamps(theme), [theme]);
|
|
94
100
|
const config = SIZE_CONFIG[size];
|
|
@@ -166,7 +172,9 @@ function StepperComponent({
|
|
|
166
172
|
<View
|
|
167
173
|
testID={testID}
|
|
168
174
|
role="group"
|
|
169
|
-
|
|
175
|
+
nativeID={field.nativeID}
|
|
176
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
177
|
+
aria-describedby={field.describedBy}
|
|
170
178
|
aria-disabled={disabled || undefined}
|
|
171
179
|
style={[{ flexDirection: 'row', alignItems: 'center', gap: config.gap }, style]}
|
|
172
180
|
>
|
|
@@ -183,7 +191,7 @@ function StepperComponent({
|
|
|
183
191
|
/>
|
|
184
192
|
<View
|
|
185
193
|
accessibilityRole="adjustable"
|
|
186
|
-
accessibilityLabel={accessibilityLabel}
|
|
194
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
187
195
|
aria-valuemin={min}
|
|
188
196
|
aria-valuemax={max}
|
|
189
197
|
aria-valuenow={value}
|
package/src/stepper/types.ts
CHANGED
|
@@ -26,7 +26,14 @@ export interface StepperProps {
|
|
|
26
26
|
* number and two glyphs, so nothing else can tell a screen reader which of
|
|
27
27
|
* several counters this is.
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The control's accessible name.
|
|
31
|
+
*
|
|
32
|
+
* Optional only because an enclosing `Field`'s label can supply it — with
|
|
33
|
+
* neither, the control announces nothing and warns once in development
|
|
34
|
+
* (`hooks/use-accessible-name-warning.ts`).
|
|
35
|
+
*/
|
|
36
|
+
accessibilityLabel?: string;
|
|
30
37
|
/** Name of the `−` button. Default `"Decrease"`. */
|
|
31
38
|
decrementLabel?: string;
|
|
32
39
|
/** Name of the `+` button. Default `"Increase"`. */
|
|
@@ -73,6 +73,22 @@ export interface WebAriaProps {
|
|
|
73
73
|
* currently needs; the union is the ARIA one, narrowed to what is spellable.
|
|
74
74
|
*/
|
|
75
75
|
'aria-haspopup'?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
|
|
76
|
+
/**
|
|
77
|
+
* The id list of the elements that DESCRIBE this control — a field's hint or
|
|
78
|
+
* its error message (`field/context.ts`).
|
|
79
|
+
*
|
|
80
|
+
* Here for the same reason as `aria-haspopup`: React Native types no such prop
|
|
81
|
+
* (there is no `accessibilityState` entry for it to fold into) while
|
|
82
|
+
* react-native-web forwards it, so web gets the association and native is
|
|
83
|
+
* unaffected. A control that needs the description READ on native carries it
|
|
84
|
+
* in its accessible name instead.
|
|
85
|
+
*/
|
|
86
|
+
'aria-describedby'?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the control's current value is invalid. Same platform split as
|
|
89
|
+
* `aria-describedby`.
|
|
90
|
+
*/
|
|
91
|
+
'aria-invalid'?: boolean;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
/**
|
package/src/switch/Switch.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useTheme } from '../theme/use-theme';
|
|
|
5
5
|
import { animation } from '../styles/tokens';
|
|
6
6
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
7
7
|
import { useAccessibleNameWarning } from '../hooks/use-accessible-name-warning';
|
|
8
|
+
import { useFieldMembership } from '../field/membership';
|
|
8
9
|
import type { SwitchProps } from './types';
|
|
9
10
|
|
|
10
11
|
const TRACK = { default: { w: 44, h: 26 }, sm: { w: 36, h: 22 } } as const;
|
|
@@ -13,9 +14,18 @@ const PADDING = 2;
|
|
|
13
14
|
const SQUEEZE_RATIO = 0.75; // thumb height shrinks to 75% when pressed
|
|
14
15
|
|
|
15
16
|
const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, SwitchProps>(
|
|
16
|
-
(
|
|
17
|
+
(
|
|
18
|
+
{ value, onValueChange, disabled, style, size = 'default', accessibilityLabel, nativeID, testID },
|
|
19
|
+
ref,
|
|
20
|
+
) => {
|
|
17
21
|
const theme = useTheme();
|
|
18
|
-
|
|
22
|
+
// A switch draws no text, so a `Field` is often the only thing that names
|
|
23
|
+
// it — and the warning has to see the RESOLVED name, or a correctly
|
|
24
|
+
// labelled switch inside a field warns anyway and the warning stops
|
|
25
|
+
// meaning anything.
|
|
26
|
+
const field = useFieldMembership({ accessibilityLabel, disabled, nativeID });
|
|
27
|
+
const isDisabled = field.disabled;
|
|
28
|
+
useAccessibleNameWarning('Switch', field.accessibilityLabel);
|
|
19
29
|
const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
|
|
20
30
|
const pressAnim = useRef(new Animated.Value(0)).current;
|
|
21
31
|
|
|
@@ -32,18 +42,18 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
|
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
const handlePress = useCallback(() => {
|
|
35
|
-
if (
|
|
45
|
+
if (isDisabled) return;
|
|
36
46
|
onValueChange(!value);
|
|
37
|
-
}, [
|
|
47
|
+
}, [isDisabled, value, onValueChange]);
|
|
38
48
|
|
|
39
49
|
const onPressIn = useCallback(() => {
|
|
40
|
-
if (
|
|
50
|
+
if (isDisabled) return;
|
|
41
51
|
Animated.spring(pressAnim, {
|
|
42
52
|
toValue: 1,
|
|
43
53
|
useNativeDriver: false,
|
|
44
54
|
...animation.spring.snappy,
|
|
45
55
|
}).start();
|
|
46
|
-
}, [
|
|
56
|
+
}, [isDisabled, pressAnim]);
|
|
47
57
|
|
|
48
58
|
const onPressOut = useCallback(() => {
|
|
49
59
|
Animated.spring(pressAnim, {
|
|
@@ -91,18 +101,21 @@ const SwitchComponent = React.forwardRef<React.ElementRef<typeof Pressable>, Swi
|
|
|
91
101
|
// `createDOMProps` emits `aria-label` from it, React Native reads it
|
|
92
102
|
// directly. That is NOT the rule for `accessibilityState` two lines up,
|
|
93
103
|
// which reaches native alone; the state and the name differ here.
|
|
94
|
-
accessibilityLabel={accessibilityLabel}
|
|
104
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
105
|
+
nativeID={field.nativeID}
|
|
106
|
+
aria-describedby={field.describedBy}
|
|
107
|
+
aria-invalid={field.invalid || undefined}
|
|
95
108
|
// The disabled state has to travel on this prop, not on `aria-disabled`:
|
|
96
109
|
// react-native-web's `Pressable` appends its own `aria-disabled` from
|
|
97
110
|
// `disabled` AFTER spreading the caller's props, so a caller-supplied
|
|
98
111
|
// one is overwritten. `handlePress` already no-ops when disabled, so
|
|
99
112
|
// this only adds what was missing — the announced state, and skipping
|
|
100
113
|
// the control in the tab order.
|
|
101
|
-
disabled={
|
|
114
|
+
disabled={isDisabled}
|
|
102
115
|
onPress={handlePress}
|
|
103
116
|
onPressIn={onPressIn}
|
|
104
117
|
onPressOut={onPressOut}
|
|
105
|
-
style={[
|
|
118
|
+
style={[isDisabled && styles.disabled, style]}
|
|
106
119
|
hitSlop={HIT_SLOP}
|
|
107
120
|
testID={testID}
|
|
108
121
|
>
|
package/src/switch/types.ts
CHANGED
|
@@ -26,7 +26,14 @@ export interface SwitchProps {
|
|
|
26
26
|
* are not the same rule.)
|
|
27
27
|
*
|
|
28
28
|
* Omitting it warns once in development; see `use-accessible-name-warning`.
|
|
29
|
+
* Inside a `Field` the field's label supplies the name, so a switch in a
|
|
30
|
+
* labelled field needs nothing here and does not warn.
|
|
29
31
|
*/
|
|
30
32
|
accessibilityLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The switch's id. Inside a `Field` the field supplies one, so the label's
|
|
35
|
+
* `htmlFor` points at the control on web.
|
|
36
|
+
*/
|
|
37
|
+
nativeID?: string;
|
|
31
38
|
testID?: string;
|
|
32
39
|
}
|