@oxy.so/bloom 3.0.1 → 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/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/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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../text-field/shared';
|
|
25
25
|
import { TYPE_SCALE } from '../typography/scale';
|
|
26
26
|
import type { InputOtpProps } from './types';
|
|
27
|
+
import { useFieldMembership } from '../field/membership';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* `InputOtp`: one box per digit, side by side, behaving as ONE value.
|
|
@@ -185,13 +186,25 @@ export function InputOtp({
|
|
|
185
186
|
disabled: disabledAlias,
|
|
186
187
|
groupEvery,
|
|
187
188
|
autoFocus = false,
|
|
188
|
-
accessibilityLabel
|
|
189
|
+
accessibilityLabel,
|
|
189
190
|
style,
|
|
190
191
|
testID,
|
|
191
192
|
}: InputOtpProps) {
|
|
192
193
|
const theme = useTheme();
|
|
193
194
|
const palette = useMemo(() => resolveInputOtpPalette(theme), [theme]);
|
|
194
|
-
|
|
195
|
+
// SEVERAL controls behind one name: the boxes are the field's `multiple` case
|
|
196
|
+
// (`docs/field.mdx`), so the GROUP takes the field's name and description and
|
|
197
|
+
// each box keeps its own name and its own `aria-invalid`. A caller's own
|
|
198
|
+
// `accessibilityLabel` still wins; `'One-time code'` is the last resort rather
|
|
199
|
+
// than a default that would outrank the field's label.
|
|
200
|
+
const membership = useFieldMembership({
|
|
201
|
+
accessibilityLabel,
|
|
202
|
+
disabled: isDisabled ?? disabledAlias ?? false,
|
|
203
|
+
invalid: isInvalid,
|
|
204
|
+
});
|
|
205
|
+
const disabled = membership.disabled;
|
|
206
|
+
const invalid = membership.invalid;
|
|
207
|
+
const groupName = membership.accessibilityLabel ?? 'One-time code';
|
|
195
208
|
|
|
196
209
|
const inputsRef = useRef<Array<TextInput | null>>([]);
|
|
197
210
|
const [internal, setInternal] = useState(() => clean(defaultValue, length));
|
|
@@ -259,7 +272,8 @@ export function InputOtp({
|
|
|
259
272
|
return (
|
|
260
273
|
<View
|
|
261
274
|
role="group"
|
|
262
|
-
accessibilityLabel={
|
|
275
|
+
accessibilityLabel={groupName}
|
|
276
|
+
aria-describedby={membership.describedBy}
|
|
263
277
|
testID={testID}
|
|
264
278
|
// No `aria-invalid` here: `group` does not support it. Each box carries
|
|
265
279
|
// its own, which is where a screen reader looks anyway.
|
|
@@ -270,7 +284,7 @@ export function InputOtp({
|
|
|
270
284
|
const paint = resolveInputOtpBoxPaint(palette, {
|
|
271
285
|
hovered: hoveredIndex === index,
|
|
272
286
|
focused: focusedIndex === index,
|
|
273
|
-
invalid
|
|
287
|
+
invalid,
|
|
274
288
|
disabled,
|
|
275
289
|
});
|
|
276
290
|
|
|
@@ -302,7 +316,7 @@ export function InputOtp({
|
|
|
302
316
|
caretHidden={false}
|
|
303
317
|
editable={!disabled}
|
|
304
318
|
accessibilityLabel={`Digit ${index + 1} of ${length}`}
|
|
305
|
-
aria-invalid={
|
|
319
|
+
aria-invalid={invalid || undefined}
|
|
306
320
|
aria-disabled={disabled || undefined}
|
|
307
321
|
value={digit === ' ' ? '' : digit}
|
|
308
322
|
onChangeText={(text) => writeFrom(index, text)}
|
package/src/input-otp/types.ts
CHANGED
|
@@ -20,7 +20,13 @@ export interface InputOtpProps extends ViewStyleProp {
|
|
|
20
20
|
groupEvery?: number;
|
|
21
21
|
/** Focus the first box on mount. */
|
|
22
22
|
autoFocus?: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The group's accessible name.
|
|
25
|
+
*
|
|
26
|
+
* Inside a `Field` the field's label supplies it. `"One-time code"` is the
|
|
27
|
+
* last resort when neither is given — a default here would outrank the
|
|
28
|
+
* field's label, which is the one thing on screen the user can read.
|
|
29
|
+
*/
|
|
24
30
|
accessibilityLabel?: string;
|
|
25
31
|
testID?: string;
|
|
26
32
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* `edge` answers "how far from the edge"; `bottom-edge` and `top-edge` answer
|
|
6
6
|
* "what is already parked there"; `scroll-offset` answers "which scroller am I
|
|
7
7
|
* following". A surface that floats at an edge needs the first two; chrome that
|
|
8
|
-
* reacts to scrolling needs the third.
|
|
8
|
+
* reacts to scrolling needs the third. `screen-scope` answers "am I my own
|
|
9
|
+
* screen" — a modal or a sheet, whose chrome must claim and read its own edges
|
|
10
|
+
* rather than the screen's underneath.
|
|
9
11
|
*/
|
|
10
12
|
export { EDGE_GAP, windowEdgeGap } from './edge';
|
|
11
13
|
export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
|
|
12
14
|
export { TopEdgeProvider, useClaimTopEdge, useTopEdgeInset } from './top-edge';
|
|
13
15
|
export { ScrollOffsetProvider, useScrollOffset } from './scroll-offset';
|
|
16
|
+
export { ScreenScope } from './screen-scope';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A surface that is its OWN screen, for the two contracts that are screen-scoped.
|
|
3
|
+
*
|
|
4
|
+
* ## What goes wrong without it
|
|
5
|
+
*
|
|
6
|
+
* `top-edge.tsx` and `scroll-offset.ts` are per-SCREEN by design: one registry
|
|
7
|
+
* of what is parked at the top, one scroller that the chrome follows. A modal,
|
|
8
|
+
* a bottom sheet or a detail panel is a second screen drawn over the first, and
|
|
9
|
+
* both contracts reach into it from outside:
|
|
10
|
+
*
|
|
11
|
+
* - A header inside a full-screen dialog CLAIMS the top edge of the screen
|
|
12
|
+
* underneath it. The claim combines with `max`, so the page behind the modal
|
|
13
|
+
* is padded for a header it cannot see — and when the modal closes the claim
|
|
14
|
+
* unregisters, so the padding appears and disappears with the modal.
|
|
15
|
+
* - Chrome inside the sheet READS the page's scroll offset and follows a
|
|
16
|
+
* scroller the user is not touching. The sheet's own list scrolls and the
|
|
17
|
+
* sheet's header does not react; the page behind scrolls and it does.
|
|
18
|
+
*
|
|
19
|
+
* Neither throws, neither warns, and both look like layout decisions.
|
|
20
|
+
*
|
|
21
|
+
* ## What it does
|
|
22
|
+
*
|
|
23
|
+
* Nests a fresh `TopEdgeProvider` (an empty registry the surface's own chrome
|
|
24
|
+
* claims against) and publishes a `null` scroll offset, which is the honest
|
|
25
|
+
* value: nothing inside has said which scroller it owns yet. A scroll owner
|
|
26
|
+
* INSIDE the surface publishes its own offset in the ordinary way and wins,
|
|
27
|
+
* because it is nearer.
|
|
28
|
+
*
|
|
29
|
+
* The BOTTOM edge is scoped too, and for the same reason read from the other
|
|
30
|
+
* side: the bottom edge is the tab bar and the FAB, and a sheet covers both. A
|
|
31
|
+
* form inside a sheet that padded itself for a tab bar it is drawn over ends in
|
|
32
|
+
* a band of empty space.
|
|
33
|
+
*
|
|
34
|
+
* Bloom's own overlay surfaces mount it (`DialogBody`, `BottomSheetBase`), so a
|
|
35
|
+
* consumer gets this by using them. It is exported for a surface Bloom does not
|
|
36
|
+
* own — an app's own full-screen route presented over another.
|
|
37
|
+
*/
|
|
38
|
+
import type { PropsWithChildren } from 'react';
|
|
39
|
+
|
|
40
|
+
import { BottomEdgeProvider } from './bottom-edge';
|
|
41
|
+
import { ScrollOffsetProvider } from './scroll-offset';
|
|
42
|
+
import { TopEdgeProvider } from './top-edge';
|
|
43
|
+
|
|
44
|
+
export function ScreenScope({ children }: PropsWithChildren) {
|
|
45
|
+
return (
|
|
46
|
+
<TopEdgeProvider>
|
|
47
|
+
<BottomEdgeProvider>
|
|
48
|
+
{/* `null`, not a zero SharedValue: the two mean different things to the
|
|
49
|
+
chrome reading it (`scroll-offset.ts`). */}
|
|
50
|
+
<ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider>
|
|
51
|
+
</BottomEdgeProvider>
|
|
52
|
+
</TopEdgeProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../text-field';
|
|
12
12
|
import { CountryCodeSelect } from './CountryCodeSelect';
|
|
13
13
|
import type { PhoneInputProps } from './types';
|
|
14
|
+
import { useFieldMembership } from '../field/membership';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A phone number field: `Input` with a country-code `Select` in its
|
|
@@ -29,7 +30,7 @@ export function PhoneInput({
|
|
|
29
30
|
hint,
|
|
30
31
|
required,
|
|
31
32
|
tooltip,
|
|
32
|
-
size
|
|
33
|
+
size,
|
|
33
34
|
isInvalid,
|
|
34
35
|
disabled,
|
|
35
36
|
value,
|
|
@@ -46,6 +47,17 @@ export function PhoneInput({
|
|
|
46
47
|
testID,
|
|
47
48
|
style,
|
|
48
49
|
}: PhoneInputProps) {
|
|
50
|
+
// The country select is a SECOND control inside this field, and it is the one
|
|
51
|
+
// a `Field disabled` used to miss: the number input reads the field context
|
|
52
|
+
// itself, the select does not, so the picker stayed operable inside a disabled
|
|
53
|
+
// field. Resolving membership here hands the same state to both halves.
|
|
54
|
+
const field = useFieldMembership({
|
|
55
|
+
accessibilityLabel,
|
|
56
|
+
label: label ?? 'Phone number',
|
|
57
|
+
disabled,
|
|
58
|
+
invalid: isInvalid,
|
|
59
|
+
required,
|
|
60
|
+
});
|
|
49
61
|
const [text, setText] = useControllableState({
|
|
50
62
|
value,
|
|
51
63
|
defaultValue,
|
|
@@ -55,14 +67,14 @@ export function PhoneInput({
|
|
|
55
67
|
return (
|
|
56
68
|
<View style={[{ width: '100%' }, style]} testID={testID}>
|
|
57
69
|
{label ? (
|
|
58
|
-
<TextFieldLabel required={required} tooltip={tooltip}>
|
|
70
|
+
<TextFieldLabel required={field.required} tooltip={tooltip}>
|
|
59
71
|
{label}
|
|
60
72
|
</TextFieldLabel>
|
|
61
73
|
) : null}
|
|
62
74
|
<TextField
|
|
63
75
|
size={size}
|
|
64
|
-
isInvalid={
|
|
65
|
-
disabled={disabled}
|
|
76
|
+
isInvalid={field.invalid}
|
|
77
|
+
disabled={field.disabled}
|
|
66
78
|
leadingAddon={
|
|
67
79
|
<CountryCodeSelect
|
|
68
80
|
value={country}
|
|
@@ -70,15 +82,15 @@ export function PhoneInput({
|
|
|
70
82
|
onValueChange={onCountryChange}
|
|
71
83
|
countries={countries}
|
|
72
84
|
label={countrySelectLabel}
|
|
73
|
-
disabled={disabled}
|
|
85
|
+
disabled={field.disabled}
|
|
74
86
|
/>
|
|
75
87
|
}>
|
|
76
88
|
<TextFieldInput
|
|
77
|
-
label={accessibilityLabel ??
|
|
89
|
+
label={field.accessibilityLabel ?? 'Phone number'}
|
|
78
90
|
placeholder={placeholder ?? null}
|
|
79
91
|
value={text}
|
|
80
92
|
onChangeText={setText}
|
|
81
|
-
isInvalid={
|
|
93
|
+
isInvalid={field.invalid}
|
|
82
94
|
keyboardType="phone-pad"
|
|
83
95
|
autoComplete="tel"
|
|
84
96
|
textContentType="telephoneNumber"
|
|
@@ -87,7 +99,7 @@ export function PhoneInput({
|
|
|
87
99
|
/>
|
|
88
100
|
{trailingIcon ? <TextFieldIcon icon={trailingIcon} position="trailing" /> : null}
|
|
89
101
|
</TextField>
|
|
90
|
-
{hint ? <TextFieldHint isInvalid={
|
|
102
|
+
{hint ? <TextFieldHint isInvalid={field.invalid}>{hint}</TextFieldHint> : null}
|
|
91
103
|
</View>
|
|
92
104
|
);
|
|
93
105
|
}
|
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.
|