@oxy.so/bloom 3.0.1 → 3.2.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/README.md +2 -0
- 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/icons.mdx +58 -3
- 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 +521 -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
|
@@ -9,6 +9,8 @@ import { TYPE_SCALE } from '../typography';
|
|
|
9
9
|
import { resolveCalendarPalette } from './palette';
|
|
10
10
|
import { formatTime, parseTime, snapTime, stepTime } from './time';
|
|
11
11
|
import type { TimeFieldProps, TimeFieldSize } from './types';
|
|
12
|
+
import { useInheritedControl } from '../control-surface';
|
|
13
|
+
import { useFieldMembership } from '../field/membership';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* A time of day, typed. `DatePicker` is the day; this is the hour and minute,
|
|
@@ -60,8 +62,8 @@ function TimeFieldComponent({
|
|
|
60
62
|
max,
|
|
61
63
|
step = 1,
|
|
62
64
|
hourFormat = '24h',
|
|
63
|
-
size
|
|
64
|
-
disabled = false,
|
|
65
|
+
size: sizeProp,
|
|
66
|
+
disabled: disabledProp = false,
|
|
65
67
|
accessibilityLabel,
|
|
66
68
|
placeholder,
|
|
67
69
|
width,
|
|
@@ -69,6 +71,12 @@ function TimeFieldComponent({
|
|
|
69
71
|
testID,
|
|
70
72
|
}: TimeFieldProps) {
|
|
71
73
|
const theme = useTheme();
|
|
74
|
+
// `"--:--"` names nothing, so the name is a prop — or, inside a `Field`, the
|
|
75
|
+
// field's label. The density contract supplies the size the same way: a
|
|
76
|
+
// container can ask for `small` once instead of on every control.
|
|
77
|
+
const size = useInheritedControl('density', sizeProp, 'medium');
|
|
78
|
+
const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
|
|
79
|
+
const disabled = field.disabled;
|
|
72
80
|
const palette = useMemo(() => resolveCalendarPalette(theme), [theme]);
|
|
73
81
|
const config = SIZE_CONFIG[size];
|
|
74
82
|
|
|
@@ -150,7 +158,10 @@ function TimeFieldComponent({
|
|
|
150
158
|
placeholder={placeholder ?? (hourFormat === '12h' ? '--:-- --' : '--:--')}
|
|
151
159
|
placeholderTextColor={palette.secondaryText}
|
|
152
160
|
inputMode={hourFormat === '12h' ? 'text' : 'numeric'}
|
|
153
|
-
|
|
161
|
+
nativeID={field.nativeID}
|
|
162
|
+
accessibilityLabel={field.accessibilityLabel}
|
|
163
|
+
aria-describedby={field.describedBy}
|
|
164
|
+
aria-invalid={field.invalid || undefined}
|
|
154
165
|
aria-disabled={disabled || undefined}
|
|
155
166
|
style={[FIELD_TRANSITION, fieldStyle, style]}
|
|
156
167
|
testID={testID}
|
package/src/date-picker/types.ts
CHANGED
|
@@ -207,10 +207,13 @@ export interface TimeFieldProps {
|
|
|
207
207
|
size?: TimeFieldSize;
|
|
208
208
|
disabled?: boolean;
|
|
209
209
|
/**
|
|
210
|
-
* The NAME of the time ("Viewing time", "Check-in").
|
|
211
|
-
*
|
|
210
|
+
* The NAME of the time ("Viewing time", "Check-in").
|
|
211
|
+
*
|
|
212
|
+
* The field draws no label of its own and `"--:--"` names nothing, so this or
|
|
213
|
+
* an enclosing `Field`'s label is the only route to a name. With neither it
|
|
214
|
+
* warns once in development (`hooks/use-accessible-name-warning.ts`).
|
|
212
215
|
*/
|
|
213
|
-
accessibilityLabel
|
|
216
|
+
accessibilityLabel?: string;
|
|
214
217
|
/** The text while empty. Default `"--:--"`, or `"--:-- --"` in 12h. */
|
|
215
218
|
placeholder?: string;
|
|
216
219
|
/** Fixed width. Default: 104 at `medium`, 96 at `small`. */
|
|
@@ -4,6 +4,7 @@ import { View, type GestureResponderEvent } from 'react-native';
|
|
|
4
4
|
import { Text } from '../typography';
|
|
5
5
|
|
|
6
6
|
import { useTheme } from '../theme/use-theme';
|
|
7
|
+
import { ScreenScope } from '../layout';
|
|
7
8
|
import { Button } from '../button';
|
|
8
9
|
import type { ButtonVariant } from '../button/types';
|
|
9
10
|
import { useDialogContext } from './context';
|
|
@@ -40,7 +41,10 @@ export function DialogBody({
|
|
|
40
41
|
}) {
|
|
41
42
|
const theme = useTheme();
|
|
42
43
|
return (
|
|
43
|
-
|
|
44
|
+
// A dialog is its own screen: chrome inside it claims THIS surface's top
|
|
45
|
+
// edge and follows THIS surface's scroller, not the page's underneath
|
|
46
|
+
// (`layout/screen-scope.tsx`).
|
|
47
|
+
<ScreenScope>
|
|
44
48
|
{title ? (
|
|
45
49
|
// Bloom's `Text`, not react-native's: the raw one renders in whatever
|
|
46
50
|
// font the platform picks and takes no theme colour, so every dialog
|
|
@@ -74,7 +78,7 @@ export function DialogBody({
|
|
|
74
78
|
) : null}
|
|
75
79
|
{children}
|
|
76
80
|
{actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
|
|
77
|
-
|
|
81
|
+
</ScreenScope>
|
|
78
82
|
);
|
|
79
83
|
}
|
|
80
84
|
|
package/src/field/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Field } from './Field';
|
|
2
2
|
export { useFieldControl } from './context';
|
|
3
|
+
export { useFieldMembership } from './membership';
|
|
3
4
|
|
|
4
5
|
export type { FieldControlValue } from './context';
|
|
6
|
+
export type { FieldLabelPlacement, FieldMembership, FieldMembershipInput } from './membership';
|
|
5
7
|
export type { FieldProps } from './types';
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE resolution of field membership, for every control that opts in.
|
|
3
|
+
*
|
|
4
|
+
* `useFieldControl()` hands a control the field's ids and state; applying them
|
|
5
|
+
* is four separate decisions, and each has a direction that is silently wrong:
|
|
6
|
+
*
|
|
7
|
+
* the NAME defers to the field (`??`) — a control inside a labelled
|
|
8
|
+
* field must not announce nothing
|
|
9
|
+
* the CONSTRAINTS combine with the field (`||`) — `disabled` and `invalid`
|
|
10
|
+
* restrict, so a control cannot opt out
|
|
11
|
+
* the ID is the field's (`??` the caller's own, which wins)
|
|
12
|
+
* `describedBy` is a LIST, so the two are joined rather than one chosen
|
|
13
|
+
*
|
|
14
|
+
* Fifteen families resolving that by hand is fifteen chances to write `??` where
|
|
15
|
+
* `||` belongs — and the failure is silent in both directions: a control that
|
|
16
|
+
* re-enables itself inside a disabled field looks fine and a field-labelled
|
|
17
|
+
* control that announces nothing looks fine too. So it is a function, for the
|
|
18
|
+
* same reason `useInheritedControl` is one.
|
|
19
|
+
*
|
|
20
|
+
* `TextFieldInput` is the one family that resolves the same rule inline, because
|
|
21
|
+
* it also reconciles the text-field shell's own context and its standalone
|
|
22
|
+
* branch. It is not exempt from the RULE: `FieldMembership.test.tsx` asserts the
|
|
23
|
+
* observable properties for every family, so the two implementations cannot
|
|
24
|
+
* disagree about what a field means without going red.
|
|
25
|
+
*
|
|
26
|
+
* ## The name, and why placement decides it
|
|
27
|
+
*
|
|
28
|
+
* A control's own label is either STACKED above it (`TextField`, `Textarea`:
|
|
29
|
+
* the field's label and the control's label are two spellings of one thing, and
|
|
30
|
+
* a `Field` around it should win) or ADJACENT to it (a checkbox's caption, a
|
|
31
|
+
* radio's option text: the words the user reads AS the control, which the
|
|
32
|
+
* announced name must match). Getting that backwards makes a screen reader say
|
|
33
|
+
* one thing while the eye reads another, so it is a declared property of the
|
|
34
|
+
* family rather than a guess this hook could make.
|
|
35
|
+
*
|
|
36
|
+
* A control with neither — `Switch`, `Slider`, a progress bar — can be named by
|
|
37
|
+
* NOTHING but the field, which is the case this hook exists for.
|
|
38
|
+
*/
|
|
39
|
+
import { useMemo } from 'react';
|
|
40
|
+
|
|
41
|
+
import { useFieldControl } from './context';
|
|
42
|
+
|
|
43
|
+
/** Where the control renders its own label, which decides which name wins. */
|
|
44
|
+
export type FieldLabelPlacement = 'stacked' | 'adjacent';
|
|
45
|
+
|
|
46
|
+
export interface FieldMembershipInput {
|
|
47
|
+
/** The name the caller wrote on the control. Always wins. */
|
|
48
|
+
accessibilityLabel?: string;
|
|
49
|
+
/** The control's own label, when it has one. */
|
|
50
|
+
label?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether that label sits above the control (`stacked`, the default — the
|
|
53
|
+
* field's label is the same thing said twice, so the field wins) or beside it
|
|
54
|
+
* as the control's visible text (`adjacent` — it wins, so the announced name
|
|
55
|
+
* and the words on screen cannot disagree).
|
|
56
|
+
*/
|
|
57
|
+
labelPlacement?: FieldLabelPlacement;
|
|
58
|
+
/** The control's own `disabled`. Combined with the field's, never replaced. */
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
/** The control's own invalid state. Combined with the field's. */
|
|
61
|
+
invalid?: boolean;
|
|
62
|
+
/** The control's own `required`. Combined with the field's. */
|
|
63
|
+
required?: boolean;
|
|
64
|
+
/** An id the caller wrote on the control. Outranks the field's. */
|
|
65
|
+
nativeID?: string;
|
|
66
|
+
/** An `aria-describedby` the caller wrote. JOINED with the field's, not replaced. */
|
|
67
|
+
describedBy?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface FieldMembership {
|
|
71
|
+
/** Whether there is an enclosing `Field` at all. */
|
|
72
|
+
inField: boolean;
|
|
73
|
+
/** The id to render, or `undefined` when neither side has one. */
|
|
74
|
+
nativeID?: string;
|
|
75
|
+
/** The accessible name to render, or `undefined` when nothing names it. */
|
|
76
|
+
accessibilityLabel?: string;
|
|
77
|
+
/** The `aria-describedby` id list, or `undefined` when there is nothing to describe it. */
|
|
78
|
+
describedBy?: string;
|
|
79
|
+
/** The effective disabled state: the control's OR the field's. */
|
|
80
|
+
disabled: boolean;
|
|
81
|
+
/** The effective invalid state: the control's OR the field's. */
|
|
82
|
+
invalid: boolean;
|
|
83
|
+
/** The effective required state: the control's OR the field's. */
|
|
84
|
+
required: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve what an enclosing `Field` contributes to one control.
|
|
89
|
+
*
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const field = useFieldMembership({ accessibilityLabel, label, labelPlacement: 'adjacent', disabled });
|
|
92
|
+
* <Pressable
|
|
93
|
+
* nativeID={field.nativeID}
|
|
94
|
+
* accessibilityLabel={field.accessibilityLabel}
|
|
95
|
+
* aria-describedby={field.describedBy}
|
|
96
|
+
* aria-invalid={field.invalid || undefined}
|
|
97
|
+
* disabled={field.disabled}
|
|
98
|
+
* />
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* Outside a `Field` it returns the control's own values unchanged, so a control
|
|
102
|
+
* can call it unconditionally.
|
|
103
|
+
*/
|
|
104
|
+
export function useFieldMembership(input: FieldMembershipInput = {}): FieldMembership {
|
|
105
|
+
const field = useFieldControl();
|
|
106
|
+
const {
|
|
107
|
+
accessibilityLabel,
|
|
108
|
+
label,
|
|
109
|
+
labelPlacement = 'stacked',
|
|
110
|
+
disabled = false,
|
|
111
|
+
invalid = false,
|
|
112
|
+
required = false,
|
|
113
|
+
nativeID,
|
|
114
|
+
describedBy,
|
|
115
|
+
} = input;
|
|
116
|
+
|
|
117
|
+
const fieldLabel = field?.labelText;
|
|
118
|
+
const fieldDescribedBy = field?.describedBy;
|
|
119
|
+
const fieldControlId = field?.controlId;
|
|
120
|
+
const fieldDisabled = field?.disabled === true;
|
|
121
|
+
const fieldInvalid = field?.invalid === true;
|
|
122
|
+
const fieldRequired = field?.required === true;
|
|
123
|
+
|
|
124
|
+
return useMemo<FieldMembership>(() => {
|
|
125
|
+
const own = label ?? undefined;
|
|
126
|
+
const name =
|
|
127
|
+
accessibilityLabel ??
|
|
128
|
+
(labelPlacement === 'adjacent' ? (own ?? fieldLabel) : (fieldLabel ?? own));
|
|
129
|
+
|
|
130
|
+
// An id LIST: a control that already points at its own hint keeps it, and
|
|
131
|
+
// the field's error is added rather than swapped in. Duplicates are
|
|
132
|
+
// dropped — an id named twice is read twice by some screen readers.
|
|
133
|
+
const described = [describedBy, fieldDescribedBy].filter(
|
|
134
|
+
(id): id is string => typeof id === 'string' && id.length > 0,
|
|
135
|
+
);
|
|
136
|
+
const uniqueDescribed = Array.from(new Set(described.flatMap((ids) => ids.split(/\s+/))));
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
inField: field != null,
|
|
140
|
+
nativeID: nativeID ?? fieldControlId,
|
|
141
|
+
accessibilityLabel: name,
|
|
142
|
+
describedBy: uniqueDescribed.length > 0 ? uniqueDescribed.join(' ') : undefined,
|
|
143
|
+
disabled: disabled || fieldDisabled,
|
|
144
|
+
invalid: invalid || fieldInvalid,
|
|
145
|
+
required: required || fieldRequired,
|
|
146
|
+
};
|
|
147
|
+
}, [
|
|
148
|
+
field,
|
|
149
|
+
accessibilityLabel,
|
|
150
|
+
label,
|
|
151
|
+
labelPlacement,
|
|
152
|
+
disabled,
|
|
153
|
+
invalid,
|
|
154
|
+
required,
|
|
155
|
+
nativeID,
|
|
156
|
+
describedBy,
|
|
157
|
+
fieldLabel,
|
|
158
|
+
fieldDescribedBy,
|
|
159
|
+
fieldControlId,
|
|
160
|
+
fieldDisabled,
|
|
161
|
+
fieldInvalid,
|
|
162
|
+
fieldRequired,
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
@@ -92,6 +92,7 @@ type StaggerState = 'shown' | 'hiding' | 'hidden';
|
|
|
92
92
|
|
|
93
93
|
import { formatFileSize } from './shared';
|
|
94
94
|
import { DISABLED_OPACITY } from '../styles/tokens';
|
|
95
|
+
import { useFieldMembership } from '../field/membership';
|
|
95
96
|
|
|
96
97
|
export { formatFileSize };
|
|
97
98
|
|
|
@@ -445,12 +446,19 @@ const FileUploadComponent = function FileUpload({
|
|
|
445
446
|
maxBytes = DEFAULT_MAX_BYTES,
|
|
446
447
|
renderFileIcon,
|
|
447
448
|
labels: labelOverrides,
|
|
448
|
-
disabled = false,
|
|
449
|
-
accessibilityLabel
|
|
449
|
+
disabled: disabledProp = false,
|
|
450
|
+
accessibilityLabel,
|
|
450
451
|
style,
|
|
451
452
|
testID,
|
|
452
453
|
}: FileUploadProps) {
|
|
453
454
|
const theme = useTheme();
|
|
455
|
+
// The drop zone draws a prompt but names itself with a prop, so a `Field`
|
|
456
|
+
// around it is the other place the name can come from — and the field's
|
|
457
|
+
// `disabled` blocks the picker whatever the caller passed. `'Upload a file'`
|
|
458
|
+
// stays the last resort rather than a default that would outrank a label the
|
|
459
|
+
// user can read.
|
|
460
|
+
const field = useFieldMembership({ accessibilityLabel, disabled: disabledProp });
|
|
461
|
+
const disabled = field.disabled;
|
|
454
462
|
useInteractiveWebCss(STYLE_ID, BLOOM_FILE_UPLOAD_CSS);
|
|
455
463
|
const reducedMotion = useReducedMotion();
|
|
456
464
|
const paint = useMemo(() => resolveFileUploadPaint(theme), [theme]);
|
|
@@ -644,7 +652,10 @@ const FileUploadComponent = function FileUpload({
|
|
|
644
652
|
ref={rootRef}
|
|
645
653
|
{...webDataSet({ bloomFileUpload: busy ? 'busy' : 'idle' })}
|
|
646
654
|
accessibilityRole="button"
|
|
647
|
-
|
|
655
|
+
nativeID={field.nativeID}
|
|
656
|
+
accessibilityLabel={field.accessibilityLabel ?? 'Upload a file'}
|
|
657
|
+
aria-describedby={field.describedBy}
|
|
658
|
+
aria-invalid={field.invalid || undefined}
|
|
648
659
|
aria-busy={busy}
|
|
649
660
|
disabled={disabled}
|
|
650
661
|
focusable={!blocked}
|
package/src/floating/types.ts
CHANGED
|
@@ -162,6 +162,19 @@ export interface TriggerHandleProps {
|
|
|
162
162
|
accessibilityLabel?: string;
|
|
163
163
|
accessibilityRole?: 'button';
|
|
164
164
|
'aria-expanded'?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* The trigger's id, so an enclosing `Field`'s label can point at it.
|
|
167
|
+
*
|
|
168
|
+
* Only a trigger that IS a form control sets it — `Select` does; a `Popover`
|
|
169
|
+
* or a `DropdownMenu` inside a field is not the field's control and must not
|
|
170
|
+
* take its id. `TriggerSlot` therefore does not read the field context
|
|
171
|
+
* itself; each family decides whether it is the control.
|
|
172
|
+
*/
|
|
173
|
+
nativeID?: string;
|
|
174
|
+
/** The field's hint or error, for a trigger that is a form control. */
|
|
175
|
+
'aria-describedby'?: string;
|
|
176
|
+
/** The field's invalid state, for a trigger that is a form control. */
|
|
177
|
+
'aria-invalid'?: boolean;
|
|
165
178
|
/**
|
|
166
179
|
* What this trigger OPENS. Set by each family from its own constant in
|
|
167
180
|
* `floating/constants.ts`, never defaulted here — the five families open three
|
package/src/index.ts
CHANGED
|
@@ -167,6 +167,7 @@ export {
|
|
|
167
167
|
useTopEdgeInset,
|
|
168
168
|
ScrollOffsetProvider,
|
|
169
169
|
useScrollOffset,
|
|
170
|
+
ScreenScope,
|
|
170
171
|
} from './layout';
|
|
171
172
|
export { Fab } from './fab';
|
|
172
173
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab';
|
|
@@ -282,8 +283,14 @@ export * from './date-picker';
|
|
|
282
283
|
export { Search } from './search';
|
|
283
284
|
export { Label } from './label';
|
|
284
285
|
export type { LabelProps } from './label';
|
|
285
|
-
export { Field } from './field';
|
|
286
|
-
export type {
|
|
286
|
+
export { Field, useFieldControl, useFieldMembership } from './field';
|
|
287
|
+
export type {
|
|
288
|
+
FieldControlValue,
|
|
289
|
+
FieldLabelPlacement,
|
|
290
|
+
FieldMembership,
|
|
291
|
+
FieldMembershipInput,
|
|
292
|
+
FieldProps,
|
|
293
|
+
} from './field';
|
|
287
294
|
export { InputGroup, InputGroupAddon } from './input-group';
|
|
288
295
|
export type { InputGroupProps, InputGroupAddonProps } from './input-group';
|
|
289
296
|
export { RangeSlider, Slider } from './slider';
|
package/src/index.web.ts
CHANGED
|
@@ -172,6 +172,7 @@ export {
|
|
|
172
172
|
useTopEdgeInset,
|
|
173
173
|
ScrollOffsetProvider,
|
|
174
174
|
useScrollOffset,
|
|
175
|
+
ScreenScope,
|
|
175
176
|
} from './layout';
|
|
176
177
|
export { Fab } from './fab/index.web';
|
|
177
178
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab/index.web';
|
|
@@ -287,8 +288,14 @@ export * from './date-picker';
|
|
|
287
288
|
export { Search } from './search';
|
|
288
289
|
export { Label } from './label';
|
|
289
290
|
export type { LabelProps } from './label';
|
|
290
|
-
export { Field } from './field';
|
|
291
|
-
export type {
|
|
291
|
+
export { Field, useFieldControl, useFieldMembership } from './field';
|
|
292
|
+
export type {
|
|
293
|
+
FieldControlValue,
|
|
294
|
+
FieldLabelPlacement,
|
|
295
|
+
FieldMembership,
|
|
296
|
+
FieldMembershipInput,
|
|
297
|
+
FieldProps,
|
|
298
|
+
} from './field';
|
|
292
299
|
export { InputGroup, InputGroupAddon } from './input-group';
|
|
293
300
|
export type { InputGroupProps, InputGroupAddonProps } from './input-group';
|
|
294
301
|
export { RangeSlider, Slider } from './slider';
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../text-field/shared';
|
|
25
25
|
import { TextFieldGroupContext } from '../text-field/TextField';
|
|
26
26
|
import type { InputGroupAddonProps, InputGroupProps } from './types';
|
|
27
|
+
import { useFieldMembership } from '../field/membership';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* The group is the input shell (`base/input/input.tsx`) with its adornment
|
|
@@ -149,12 +150,21 @@ InputGroupAddon.displayName = 'InputGroupAddon';
|
|
|
149
150
|
*/
|
|
150
151
|
const InputGroupComponent = function InputGroup({
|
|
151
152
|
children,
|
|
152
|
-
isInvalid = false,
|
|
153
|
-
disabled = false,
|
|
153
|
+
isInvalid: isInvalidProp = false,
|
|
154
|
+
disabled: disabledProp = false,
|
|
154
155
|
size = 'md',
|
|
155
156
|
style,
|
|
156
157
|
testID,
|
|
157
158
|
}: InputGroupProps) {
|
|
159
|
+
// A group holding a control and its addons is ONE control as far as a `Field`
|
|
160
|
+
// is concerned: the field's `disabled` and invalid state reach every member
|
|
161
|
+
// through the group's own contexts, including an addon's `Button`, which reads
|
|
162
|
+
// neither the field nor the text-field group. The group publishes no name — it
|
|
163
|
+
// does not know which of its children is the control the label points at, so
|
|
164
|
+
// that stays the field's `nativeID` and the input's to apply.
|
|
165
|
+
const member = useFieldMembership({ disabled: disabledProp, invalid: isInvalidProp });
|
|
166
|
+
const disabled = member.disabled;
|
|
167
|
+
const isInvalid = member.invalid;
|
|
158
168
|
const cfg = SIZE_CONFIG[size];
|
|
159
169
|
const { state: focused, onIn: onFocus, onOut: onBlur } = useInteractionState();
|
|
160
170
|
const { state: hovered, onIn: onHoverIn, onOut: onHoverOut } =
|
|
@@ -211,6 +221,7 @@ const InputGroupComponent = function InputGroup({
|
|
|
211
221
|
// react-native-web has no `disabled` prop to derive the attribute from
|
|
212
222
|
// and never reads `accessibilityState`. React Native folds it back.
|
|
213
223
|
aria-disabled={disabled || undefined}
|
|
224
|
+
aria-invalid={isInvalid || undefined}
|
|
214
225
|
// Capture focus bubbling from a nested input so the whole chrome
|
|
215
226
|
// reflects focus — RN-web bubbles focus/blur, native does not but a
|
|
216
227
|
// nested TextInput's own focus ring is sufficient there.
|
|
@@ -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
|
}
|