@hjmds/react-native 1.12.1 → 1.13.1
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 +15 -0
- package/dist/actions.d.ts +30 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +28 -5
- package/dist/actions.js.map +1 -1
- package/dist/agreement.d.ts +8 -1
- package/dist/agreement.d.ts.map +1 -1
- package/dist/agreement.js +16 -3
- package/dist/agreement.js.map +1 -1
- package/dist/asset.d.ts +15 -2
- package/dist/asset.d.ts.map +1 -1
- package/dist/asset.js +7 -4
- package/dist/asset.js.map +1 -1
- package/dist/bottom-info.d.ts +8 -1
- package/dist/bottom-info.d.ts.map +1 -1
- package/dist/bottom-info.js +4 -2
- package/dist/bottom-info.js.map +1 -1
- package/dist/calendar.d.ts +3 -1
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +2 -2
- package/dist/calendar.js.map +1 -1
- package/dist/carousel.d.ts +8 -1
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.js +4 -2
- package/dist/carousel.js.map +1 -1
- package/dist/collapsible.d.ts +8 -1
- package/dist/collapsible.d.ts.map +1 -1
- package/dist/collapsible.js +9 -3
- package/dist/collapsible.js.map +1 -1
- package/dist/data-display.d.ts +146 -13
- package/dist/data-display.d.ts.map +1 -1
- package/dist/data-display.js +58 -29
- package/dist/data-display.js.map +1 -1
- package/dist/date-picker.d.ts +8 -1
- package/dist/date-picker.d.ts.map +1 -1
- package/dist/date-picker.js +10 -3
- package/dist/date-picker.js.map +1 -1
- package/dist/duration-field.d.ts +4 -1
- package/dist/duration-field.d.ts.map +1 -1
- package/dist/duration-field.js +2 -2
- package/dist/duration-field.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/feedback.d.ts +89 -14
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +25 -16
- package/dist/feedback.js.map +1 -1
- package/dist/file-picker.d.ts +8 -1
- package/dist/file-picker.d.ts.map +1 -1
- package/dist/file-picker.js +4 -2
- package/dist/file-picker.js.map +1 -1
- package/dist/forms.d.ts +28 -4
- package/dist/forms.d.ts.map +1 -1
- package/dist/forms.js +19 -8
- package/dist/forms.js.map +1 -1
- package/dist/heading.d.ts +8 -1
- package/dist/heading.d.ts.map +1 -1
- package/dist/heading.js +4 -2
- package/dist/heading.js.map +1 -1
- package/dist/image-viewer.d.ts.map +1 -1
- package/dist/image-viewer.js +11 -3
- package/dist/image-viewer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inputs.d.ts +66 -3
- package/dist/inputs.d.ts.map +1 -1
- package/dist/inputs.js +142 -51
- package/dist/inputs.js.map +1 -1
- package/dist/internal/deprecated-style.d.ts +8 -0
- package/dist/internal/deprecated-style.d.ts.map +1 -0
- package/dist/internal/deprecated-style.js +34 -0
- package/dist/internal/deprecated-style.js.map +1 -0
- package/dist/internal/field-frame.d.ts +2 -1
- package/dist/internal/field-frame.d.ts.map +1 -1
- package/dist/internal/field-frame.js +4 -2
- package/dist/internal/field-frame.js.map +1 -1
- package/dist/internal/field-private.d.ts +11 -0
- package/dist/internal/field-private.d.ts.map +1 -0
- package/dist/internal/field-private.js +2 -0
- package/dist/internal/field-private.js.map +1 -0
- package/dist/internal/list-row-private.d.ts +10 -0
- package/dist/internal/list-row-private.d.ts.map +1 -0
- package/dist/internal/list-row-private.js +2 -0
- package/dist/internal/list-row-private.js.map +1 -0
- package/dist/internal/message-reactions.d.ts +21 -0
- package/dist/internal/message-reactions.d.ts.map +1 -0
- package/dist/internal/message-reactions.js +49 -0
- package/dist/internal/message-reactions.js.map +1 -0
- package/dist/internal/recipe-button.d.ts +6 -5
- package/dist/internal/recipe-button.d.ts.map +1 -1
- package/dist/internal/recipe-button.js +22 -7
- package/dist/internal/recipe-button.js.map +1 -1
- package/dist/internal/spinner.d.ts +15 -0
- package/dist/internal/spinner.d.ts.map +1 -0
- package/dist/internal/spinner.js +12 -0
- package/dist/internal/spinner.js.map +1 -0
- package/dist/masonry.d.ts +3 -1
- package/dist/masonry.d.ts.map +1 -1
- package/dist/masonry.js +3 -3
- package/dist/masonry.js.map +1 -1
- package/dist/mentions.d.ts +4 -0
- package/dist/mentions.d.ts.map +1 -1
- package/dist/mentions.js +8 -2
- package/dist/mentions.js.map +1 -1
- package/dist/navigation.d.ts +72 -4
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +39 -21
- package/dist/navigation.js.map +1 -1
- package/dist/number-field.d.ts +21 -0
- package/dist/number-field.d.ts.map +1 -1
- package/dist/number-field.js +7 -2
- package/dist/number-field.js.map +1 -1
- package/dist/overlays.d.ts +30 -2
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +108 -19
- package/dist/overlays.js.map +1 -1
- package/dist/primitives.d.ts +19 -3
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +12 -6
- package/dist/primitives.js.map +1 -1
- package/dist/provider-button.d.ts +8 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +4 -1
- package/dist/provider-button.js.map +1 -1
- package/dist/qr-code.d.ts +4 -1
- package/dist/qr-code.d.ts.map +1 -1
- package/dist/qr-code.js +2 -2
- package/dist/qr-code.js.map +1 -1
- package/dist/reaction-picker.d.ts +4 -2
- package/dist/reaction-picker.d.ts.map +1 -1
- package/dist/reaction-picker.js +18 -6
- package/dist/reaction-picker.js.map +1 -1
- package/dist/saved-items.d.ts +21 -0
- package/dist/saved-items.d.ts.map +1 -0
- package/dist/saved-items.js +16 -0
- package/dist/saved-items.js.map +1 -0
- package/dist/screen-flows.d.ts +303 -0
- package/dist/screen-flows.d.ts.map +1 -0
- package/dist/screen-flows.js +172 -0
- package/dist/screen-flows.js.map +1 -0
- package/dist/screens.d.ts +114 -0
- package/dist/screens.d.ts.map +1 -0
- package/dist/screens.js +80 -0
- package/dist/screens.js.map +1 -0
- package/dist/slider.d.ts +21 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -2
- package/dist/slider.js.map +1 -1
- package/dist/steps.d.ts +8 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +4 -2
- package/dist/steps.js.map +1 -1
- package/dist/tags-input.d.ts +8 -1
- package/dist/tags-input.d.ts.map +1 -1
- package/dist/tags-input.js +11 -5
- package/dist/tags-input.js.map +1 -1
- package/dist/toggle-group.d.ts +8 -1
- package/dist/toggle-group.d.ts.map +1 -1
- package/dist/toggle-group.js +4 -2
- package/dist/toggle-group.js.map +1 -1
- package/dist/top.d.ts +8 -1
- package/dist/top.d.ts.map +1 -1
- package/dist/top.js +4 -1
- package/dist/top.js.map +1 -1
- package/dist/transfer-list.d.ts +8 -1
- package/dist/transfer-list.d.ts.map +1 -1
- package/dist/transfer-list.js +4 -2
- package/dist/transfer-list.js.map +1 -1
- package/dist/upload-item.d.ts +8 -1
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +4 -2
- package/dist/upload-item.js.map +1 -1
- package/package.json +21 -3
package/dist/inputs.js
CHANGED
|
@@ -9,13 +9,14 @@ import { visibleControlHeight } from "@hjmds/design-contracts/components/design-
|
|
|
9
9
|
import { passwordFieldRecipe, resolvePasswordFieldDescriptor, } from "@hjmds/design-contracts/components/password-field";
|
|
10
10
|
import { getOtpFieldSlotValues, otpFieldRecipe, resolveOtpFieldValue, } from "@hjmds/design-contracts/components/otp-field";
|
|
11
11
|
import { getCheckboxNextState, reconcileCheckboxSelection, resolveControlAccessibleName, resolveInitialRadioValue, resolveInitialTabValue, reconcileRadioSelection, selectionGroupBehaviorDefaults, toggleCheckboxSelection, validateCheckboxSelection, validateRadioSelection, validateSelectionItems, } from "@hjmds/design-contracts/behaviors";
|
|
12
|
-
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState, } from "react";
|
|
12
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
|
|
13
13
|
import { ActivityIndicator, Platform, Pressable, Switch as NativeSwitch, Text as NativeText, TextInput, View, } from "react-native";
|
|
14
14
|
import { mixedCheckboxState, useControllableState } from "./internal/state.js";
|
|
15
15
|
import { webChoiceProps, webOnly } from "./internal/web-a11y.js";
|
|
16
16
|
import { logicalTextAlign, minimumTargetStyle, resolveNativeTextScaleProps, } from "./internal/styles.js";
|
|
17
17
|
import { Text } from "./primitives.js";
|
|
18
18
|
import { useHjmNativeTheme } from "./provider.js";
|
|
19
|
+
import { warnDeprecatedStyleProps } from "./internal/deprecated-style.js";
|
|
19
20
|
function resolveFieldAccessibleName(label, accessibilityLabel) {
|
|
20
21
|
const visibleLabel = label?.trim();
|
|
21
22
|
const explicitAccessibleName = accessibilityLabel?.trim();
|
|
@@ -28,10 +29,20 @@ function resolveFieldAccessibleName(label, accessibilityLabel) {
|
|
|
28
29
|
...(visibleLabel ? { visibleLabel } : {}),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultValue = "", onValueChange, description, error, required = false, disabled = false, busy = false, variant = fieldRecipe.defaults.variant, shape, accessibilityLabel, layoutStyle, allowFontScaling, multiline, maxVisibleLines, minVisibleLines, align = fieldRecipe.defaults.align, search, recipeInputStyle, searchSize = searchFieldRecipe.defaults.size, leading, trailing, onBlur, onFocus, ...props }, ref) {
|
|
32
|
+
const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultValue = "", onValueChange, description, error, required = false, disabled = false, busy = false, variant = fieldRecipe.defaults.variant, shape, accessibilityLabel, layoutStyle, allowFontScaling, multiline, maxVisibleLines, minVisibleLines, align = fieldRecipe.defaults.align, search, recipeInputStyle, disabledOpacity, searchSize = searchFieldRecipe.defaults.size, leading, trailing, leadingAction, hjmCompactMultiline = false, onBlur, onFocus, onContentSizeChange, onSelectionChange, ...props }, ref) {
|
|
33
|
+
const resolvedDisabledOpacity = disabledOpacity ?? (search ? searchFieldRecipe.states.disabledOpacity : fieldRecipe.disabledOpacity);
|
|
32
34
|
const theme = useHjmNativeTheme();
|
|
33
35
|
const { colors, environment, textScaling } = theme;
|
|
34
36
|
const [focused, setFocused] = useState(false);
|
|
37
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
38
|
+
const inputRef = useRef(null);
|
|
39
|
+
const restoreFocusRef = useRef(false);
|
|
40
|
+
const selectionRef = useRef(null);
|
|
41
|
+
const attachInput = useCallback((node) => {
|
|
42
|
+
if (node === null)
|
|
43
|
+
restoreFocusRef.current = inputRef.current?.isFocused?.() ?? false;
|
|
44
|
+
inputRef.current = node;
|
|
45
|
+
}, []);
|
|
35
46
|
const [currentValue, setCurrentValue] = useControllableState({
|
|
36
47
|
...(value === undefined ? {} : { value }),
|
|
37
48
|
defaultValue,
|
|
@@ -65,16 +76,39 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
65
76
|
? resolveColorReference(searchFieldRecipe.colors.placeholder, theme.palette)
|
|
66
77
|
: colors[fieldRecipe.placeholder.color];
|
|
67
78
|
const textStyle = typography[search ? searchSizing.textVariant : fieldRecipe.textVariant];
|
|
79
|
+
// Native TextInput scales its text without enlarging a fixed frame (BT-QA-020).
|
|
80
|
+
// Size the one-line frame for the same scale instead of capping accessible text.
|
|
81
|
+
const frameTextScale = textScaling.mode === "controlled"
|
|
82
|
+
? textScaling.scale
|
|
83
|
+
: allowFontScaling === false
|
|
84
|
+
? 1
|
|
85
|
+
: Math.min(textScaling.scale, props.maxFontSizeMultiplier && props.maxFontSizeMultiplier > 0 ? props.maxFontSizeMultiplier : Infinity);
|
|
86
|
+
const singleLineMinHeight = Math.ceil(textStyle.lineHeight * frameTextScale + fieldRecipe.paddingVertical * 2 + borderWidth * 2);
|
|
87
|
+
// iOS RCTUITextView updates typingAttributes/placeholder but leaves existing
|
|
88
|
+
// attributed text at its old scale (BT-QA-025). Refresh only the native editor,
|
|
89
|
+
// keeping the field's draft state and restoring focus/selection instead of capping text.
|
|
90
|
+
const editorKey = Platform.OS === "ios" && multiline
|
|
91
|
+
? `multiline-${frameTextScale}`
|
|
92
|
+
: "field";
|
|
93
|
+
// Deps: only an editorKey remount changes the handle; none re-attached callback refs every render.
|
|
94
|
+
useImperativeHandle(ref, () => inputRef.current, [editorKey]);
|
|
95
|
+
useLayoutEffect(() => {
|
|
96
|
+
if (restoreFocusRef.current) {
|
|
97
|
+
inputRef.current?.focus?.();
|
|
98
|
+
const selection = props.selection ?? selectionRef.current;
|
|
99
|
+
if (selection)
|
|
100
|
+
inputRef.current?.setNativeProps?.({ selection });
|
|
101
|
+
restoreFocusRef.current = false;
|
|
102
|
+
}
|
|
103
|
+
}, [editorKey, props.selection]);
|
|
68
104
|
// A composer that should open several lines tall asks in lines, not pixels,
|
|
69
|
-
// so the recipe keeps ownership of line height and vertical padding.
|
|
105
|
+
// so the recipe keeps ownership of line height and vertical padding. Compact: see field-private.ts.
|
|
70
106
|
const minHeight = multiline
|
|
71
107
|
? minVisibleLines === undefined
|
|
72
108
|
? fieldRecipe.multilineMinHeight
|
|
73
|
-
: Math.max(fieldRecipe.multilineMinHeight, textStyle.lineHeight * minVisibleLines +
|
|
109
|
+
: Math.max(hjmCompactMultiline ? fieldRecipe.minHeight : fieldRecipe.multilineMinHeight, textStyle.lineHeight * minVisibleLines +
|
|
74
110
|
fieldRecipe.paddingVertical * 2)
|
|
75
|
-
: search
|
|
76
|
-
? searchSizing.minHeight
|
|
77
|
-
: fieldRecipe.minHeight;
|
|
111
|
+
: Math.max(search ? searchSizing.minHeight : fieldRecipe.minHeight, singleLineMinHeight);
|
|
78
112
|
const controlRadius = radius[search
|
|
79
113
|
? searchFieldRecipe.shapes[resolvedShape]
|
|
80
114
|
: fieldRecipe.shapes[resolvedShape]];
|
|
@@ -88,6 +122,8 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
88
122
|
fontWeight: textStyle.fontWeight,
|
|
89
123
|
lineHeight: textStyle.lineHeight,
|
|
90
124
|
minHeight: minHeight - borderWidth * 2,
|
|
125
|
+
// Explicit line bounds opt into content-driven composer sizing, without changing ordinary editors.
|
|
126
|
+
...(multiline && minVisibleLines !== undefined ? { height: Math.max(minHeight - borderWidth * 2, Math.min(currentValue ? contentHeight : 0, textStyle.lineHeight * (resolvedMaxVisibleLines ?? 1000) + fieldRecipe.paddingVertical * 2)) } : {}),
|
|
91
127
|
...(multiline &&
|
|
92
128
|
resolvedMaxVisibleLines !== null &&
|
|
93
129
|
resolvedMaxVisibleLines !== undefined
|
|
@@ -105,16 +141,8 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
105
141
|
},
|
|
106
142
|
recipeInputStyle,
|
|
107
143
|
], allowFontScaling);
|
|
108
|
-
return (_jsx(NativeFieldFrame, { ...(visibleLabel === undefined ? {} : { label: visibleLabel }), required: required, ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { description: supportText }), style:
|
|
109
|
-
|
|
110
|
-
opacity: disabled
|
|
111
|
-
? search
|
|
112
|
-
? searchFieldRecipe.states.disabledOpacity
|
|
113
|
-
: fieldRecipe.disabledOpacity
|
|
114
|
-
: 1,
|
|
115
|
-
},
|
|
116
|
-
layoutStyle,
|
|
117
|
-
], children: _jsxs(View, { style: {
|
|
144
|
+
return (_jsx(NativeFieldFrame, { ...(visibleLabel === undefined ? {} : { label: visibleLabel }), required: required, ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { description: supportText }), ...(disabled ? { disabledOpacity: resolvedDisabledOpacity } : {}), style: layoutStyle, children: _jsxs(View, { style: {
|
|
145
|
+
opacity: disabled ? resolvedDisabledOpacity : 1,
|
|
118
146
|
alignItems: multiline ? "stretch" : "center",
|
|
119
147
|
backgroundColor,
|
|
120
148
|
borderColor,
|
|
@@ -127,13 +155,25 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
127
155
|
paddingHorizontal: search
|
|
128
156
|
? searchSizing.paddingHorizontal
|
|
129
157
|
: fieldRecipe.paddingHorizontal,
|
|
130
|
-
}, children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(TextInput, { ...props, ...inputTextScaleProps, ref:
|
|
158
|
+
}, children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, leadingAction ? _jsx(View, { style: { alignSelf: "center" }, children: leadingAction }) : null, _jsx(TextInput, { ...props, ...inputTextScaleProps, ref: attachInput, accessibilityHint: hint, accessibilityLabel: accessibleName, accessibilityRole: search ? "search" : undefined, accessibilityState: { busy, disabled },
|
|
159
|
+
// SearchField `busy` means "results are loading", not "this value is being committed":
|
|
160
|
+
// locking the editor dropped every keystroke typed while suggestions refreshed, while the
|
|
161
|
+
// Web SearchField keeps typing under `aria-busy` (2026-10-06 parity follow-up). Plain
|
|
162
|
+
// fields keep the lock because their busy means a pending save of the current value.
|
|
163
|
+
editable: !disabled && (search || !busy), multiline: multiline, onBlur: (event) => {
|
|
131
164
|
setFocused(false);
|
|
132
165
|
onBlur?.(event);
|
|
166
|
+
}, onContentSizeChange: event => {
|
|
167
|
+
if (multiline && minVisibleLines !== undefined)
|
|
168
|
+
setContentHeight(event.nativeEvent.contentSize.height);
|
|
169
|
+
onContentSizeChange?.(event);
|
|
170
|
+
}, onSelectionChange: event => {
|
|
171
|
+
selectionRef.current = event.nativeEvent.selection;
|
|
172
|
+
onSelectionChange?.(event);
|
|
133
173
|
}, onChangeText: setCurrentValue, onFocus: (event) => {
|
|
134
174
|
setFocused(true);
|
|
135
175
|
onFocus?.(event);
|
|
136
|
-
}, placeholderTextColor: placeholderColor, value: currentValue }), trailing] }) }));
|
|
176
|
+
}, placeholderTextColor: placeholderColor, value: currentValue }, editorKey), trailing] }) }));
|
|
137
177
|
});
|
|
138
178
|
export const TextField = forwardRef(function TextField(props, ref) {
|
|
139
179
|
return _jsx(FieldRenderer, { ...props, ref: ref, multiline: false, search: false });
|
|
@@ -142,6 +182,9 @@ export const TextArea = forwardRef(function TextArea(props, ref) {
|
|
|
142
182
|
return _jsx(FieldRenderer, { ...props, ref: ref, multiline: true, search: false });
|
|
143
183
|
});
|
|
144
184
|
export const SearchField = forwardRef(function SearchField({ clearLabel, busyLabel, onClear, leading: leadingNode, trailing: trailingNode, renderLeading, renderClearIcon, renderBusyIndicator, value, defaultValue, onValueChange, size = searchFieldRecipe.defaults.size, busy = false, disabled = false, ...props }, ref) {
|
|
185
|
+
const inputRef = useRef(null);
|
|
186
|
+
// Single-line child editor never remounts.
|
|
187
|
+
useImperativeHandle(ref, () => inputRef.current, []);
|
|
145
188
|
const theme = useHjmNativeTheme();
|
|
146
189
|
const searchSizing = searchFieldRecipe.sizes[size];
|
|
147
190
|
const iconProps = {
|
|
@@ -163,14 +206,16 @@ export const SearchField = forwardRef(function SearchField({ clearLabel, busyLab
|
|
|
163
206
|
}, children: renderBusyIndicator?.(iconProps) ?? (_jsx(ActivityIndicator, { color: iconProps.color, size: iconProps.size })) })) : searchValue.length > 0 ? (_jsx(Pressable, { accessibilityLabel: clearLabel, accessibilityRole: "button", disabled: disabled, hitSlop: searchSizing.clearHitSlop, onPress: () => {
|
|
164
207
|
setSearchValue("");
|
|
165
208
|
onClear?.();
|
|
209
|
+
inputRef.current?.focus();
|
|
166
210
|
}, style: {
|
|
167
211
|
alignItems: "center",
|
|
168
212
|
height: searchSizing.clearDiameter,
|
|
169
213
|
justifyContent: "center",
|
|
170
214
|
width: searchSizing.clearDiameter,
|
|
171
215
|
}, children: renderClearIcon?.(iconProps) ?? (_jsx(Text, { align: "center", style: { fontSize: iconProps.size, lineHeight: iconProps.size }, tone: "muted", children: "\u00D7" })) })) : trailingNode;
|
|
172
|
-
return (_jsx(FieldRenderer, { ...props, ref:
|
|
173
|
-
|
|
216
|
+
return (_jsx(FieldRenderer, { ...props, ref: inputRef, busy: busy, disabled: disabled, leading: leading, multiline: false, onValueChange: (next) => {
|
|
217
|
+
// Typing continues while busy (same as Web `loading`); only disabled ignores input.
|
|
218
|
+
if (!disabled)
|
|
174
219
|
setSearchValue(next);
|
|
175
220
|
}, search: true, searchSize: size, trailing: trailing, value: searchValue }));
|
|
176
221
|
});
|
|
@@ -209,7 +254,8 @@ function DefaultPasswordToggleIcon({ color, revealed, size, }) {
|
|
|
209
254
|
export const PasswordField = forwardRef(function PasswordField({ revealed: revealedProp, defaultRevealed = false, onRevealedChange, autofillHint, revealLabel, concealLabel, size = passwordFieldRecipe.defaults.size, renderToggleIcon, disabled = false, onSelectionChange, ...props }, forwardedRef) {
|
|
210
255
|
const theme = useHjmNativeTheme();
|
|
211
256
|
const inputRef = useRef(null);
|
|
212
|
-
|
|
257
|
+
// Single-line child editor never remounts.
|
|
258
|
+
useImperativeHandle(forwardedRef, () => inputRef.current, []);
|
|
213
259
|
const selectionRef = useRef({ start: 0, end: 0 });
|
|
214
260
|
const [revealed, setRevealed] = useControllableState({
|
|
215
261
|
...(revealedProp === undefined ? {} : { value: revealedProp }),
|
|
@@ -236,7 +282,9 @@ export const PasswordField = forwardRef(function PasswordField({ revealed: revea
|
|
|
236
282
|
}, 0);
|
|
237
283
|
return () => clearTimeout(timeout);
|
|
238
284
|
}, [revealed]);
|
|
239
|
-
return (_jsx(FieldRenderer, { ...props, ref: inputRef, autoComplete: autofillHint === "current" ? "current-password" : "new-password", disabled: disabled,
|
|
285
|
+
return (_jsx(FieldRenderer, { ...props, ref: inputRef, autoComplete: autofillHint === "current" ? "current-password" : "new-password", disabled: disabled,
|
|
286
|
+
// passwordFieldRecipe.states owns the amount; both renderers used the field default 0.6 until 2026-10-06.
|
|
287
|
+
disabledOpacity: passwordFieldRecipe.states.disabledOpacity, recipeInputStyle: [
|
|
240
288
|
size === "large"
|
|
241
289
|
? {
|
|
242
290
|
fontSize: typography.bodyLarge.fontSize,
|
|
@@ -247,7 +295,11 @@ export const PasswordField = forwardRef(function PasswordField({ revealed: revea
|
|
|
247
295
|
], multiline: false, onSelectionChange: (event) => {
|
|
248
296
|
selectionRef.current = event.nativeEvent.selection;
|
|
249
297
|
onSelectionChange?.(event);
|
|
250
|
-
}, search: false, secureTextEntry: resolved.nativeSecureTextEntry, textContentType: autofillHint === "current" ? "password" : "newPassword", trailing: (_jsx(Pressable, { accessibilityLabel: resolved.toggleAccessibleName, accessibilityRole: "button",
|
|
298
|
+
}, search: false, secureTextEntry: resolved.nativeSecureTextEntry, textContentType: autofillHint === "current" ? "password" : "newPassword", trailing: (_jsx(Pressable, { accessibilityLabel: resolved.toggleAccessibleName, accessibilityRole: "button",
|
|
299
|
+
// No `selected`: the label already names the next action, and VoiceOver
|
|
300
|
+
// would read "Selected, Hide password" — the Native twin of the
|
|
301
|
+
// `aria-pressed` the contract rules out (password-field.md, rationale 1).
|
|
302
|
+
accessibilityState: { disabled }, disabled: disabled, onPress: () => setRevealed(!revealed), style: ({ pressed }) => ({
|
|
251
303
|
alignItems: "center",
|
|
252
304
|
height: metrics.toggleDiameter,
|
|
253
305
|
justifyContent: "center",
|
|
@@ -256,9 +308,13 @@ export const PasswordField = forwardRef(function PasswordField({ revealed: revea
|
|
|
256
308
|
}), children: renderToggleIcon?.(appearance) ?? _jsx(DefaultPasswordToggleIcon, { ...appearance }) })) }));
|
|
257
309
|
});
|
|
258
310
|
/** One accessible numeric TextInput rendered through decorative OTP slots. */
|
|
259
|
-
export const OtpField = forwardRef(function OtpField({ label, accessibilityLabel, description, error, required = false, disabled = false, busy = false, readOnly = false, length, value: valueProp, defaultValue = "", onValueChange, onComplete, size = otpFieldRecipe.defaults.size, presentation = "boxes", slotStyle, slotTextStyle,
|
|
311
|
+
export const OtpField = forwardRef(function OtpField({ label, accessibilityLabel, description, error, required = false, disabled = false, busy = false, readOnly = false, length, value: valueProp, defaultValue = "", onValueChange, onComplete, size = otpFieldRecipe.defaults.size, presentation = "boxes", slotStyle, slotTextStyle,
|
|
312
|
+
// Inherited from BaseFieldProps; before 1.13 it fell into `...props` and was spread onto the
|
|
313
|
+
// hidden TextInput, so OtpField silently ignored its own canonical placement prop.
|
|
314
|
+
layoutStyle, allowFontScaling, onBlur, onFocus, ...props }, ref) {
|
|
260
315
|
const supportText = description;
|
|
261
316
|
const theme = useHjmNativeTheme();
|
|
317
|
+
warnDeprecatedStyleProps("OtpField", { slotStyle, slotTextStyle }, "size/presentation for slot appearance and layoutStyle for placement");
|
|
262
318
|
const { accessibleName, visibleLabel } = resolveFieldAccessibleName(label, accessibilityLabel);
|
|
263
319
|
const [focused, setFocused] = useState(false);
|
|
264
320
|
const [value, setValue] = useControllableState({
|
|
@@ -282,17 +338,20 @@ export const OtpField = forwardRef(function OtpField({ label, accessibilityLabel
|
|
|
282
338
|
const invalidBorder = resolveColorReference(otpFieldRecipe.slot.invalidBorder, theme.palette);
|
|
283
339
|
const filledBorder = resolveColorReference(otpFieldRecipe.slot.filledBorder, theme.palette);
|
|
284
340
|
const contentColor = resolveColorReference(otpFieldRecipe.slot.content, theme.palette);
|
|
341
|
+
// fieldRecipe.disabledScope: label and slots fade; the hint/error below keep full contrast.
|
|
342
|
+
// The whole column used to fade (2026-10-06 change, same as Web).
|
|
343
|
+
const disabledOpacity = disabled || busy ? otpFieldRecipe.states.disabledOpacity : 1;
|
|
285
344
|
return (_jsxs(View, { style: [
|
|
286
|
-
{
|
|
287
|
-
|
|
288
|
-
opacity: disabled || busy ? otpFieldRecipe.states.disabledOpacity : 1,
|
|
289
|
-
},
|
|
345
|
+
{ gap: fieldRecipe.label.gap },
|
|
346
|
+
layoutStyle,
|
|
290
347
|
], children: [visibleLabel ? (_jsxs(Text, { style: {
|
|
291
348
|
color: theme.colors[fieldRecipe.label.color],
|
|
292
349
|
fontWeight: fieldRecipe.label.fontWeight,
|
|
350
|
+
opacity: disabledOpacity,
|
|
293
351
|
}, tone: "body", variant: fieldRecipe.label.textVariant, children: [visibleLabel, required ? " *" : ""] })) : null, _jsxs(View, { style: { gap: otpFieldRecipe.support.gap }, children: [_jsxs(View, { style: {
|
|
294
352
|
direction: "ltr",
|
|
295
353
|
maxWidth: metrics.slotSize * length + metrics.gap * (length - 1),
|
|
354
|
+
opacity: disabledOpacity,
|
|
296
355
|
position: "relative",
|
|
297
356
|
width: "100%",
|
|
298
357
|
}, children: [_jsx(TextInput, { ...props, ref: ref, accessibilityHint: error ?? supportText, accessibilityLabel: accessibleName, accessibilityState: { busy, disabled: disabled || readOnly }, allowFontScaling: allowFontScaling, autoComplete: "one-time-code", caretHidden: true, editable: !disabled && !busy && !readOnly, keyboardType: "number-pad", maxLength: length, onBlur: (event) => {
|
|
@@ -341,7 +400,20 @@ export const OtpField = forwardRef(function OtpField({ label, accessibilityLabel
|
|
|
341
400
|
], children: _jsx(Text, { accessible: false, align: "center", allowFontScaling: allowFontScaling, style: [{ color: contentColor }, slotTextStyle], variant: metrics.textVariant, children: digit }) }, index));
|
|
342
401
|
}) })] }), _jsx(FieldMessage, { ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { supportText }) })] })] }));
|
|
343
402
|
});
|
|
344
|
-
|
|
403
|
+
const choiceVisualReplacement = "layoutStyle for placement and presentation/size/indicator/renderIndicator/renderLeading for appearance";
|
|
404
|
+
/** Public choice components share one deprecated slot list; ChoiceRow itself stays silent. */
|
|
405
|
+
function warnChoiceVisualStyles(component, visual) {
|
|
406
|
+
warnDeprecatedStyleProps(component, {
|
|
407
|
+
style: visual.style,
|
|
408
|
+
controlStyle: visual.controlStyle,
|
|
409
|
+
indicatorStyle: visual.indicatorStyle,
|
|
410
|
+
leadingStyle: visual.leadingStyle,
|
|
411
|
+
contentStyle: visual.contentStyle,
|
|
412
|
+
labelStyle: visual.labelStyle,
|
|
413
|
+
descriptionStyle: visual.descriptionStyle,
|
|
414
|
+
}, choiceVisualReplacement);
|
|
415
|
+
}
|
|
416
|
+
function ChoiceRow({ kind, label, description, checked, disabled, readOnly, required, invalid, readOnlyLabel, requiredLabel, invalidLabel, accessibilityHint, presentation = selectionControlRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", leading, renderLeading, renderIndicator, onActivate, webTabIndex, layoutStyle, style, controlStyle, indicatorStyle, leadingStyle, contentStyle, labelStyle, descriptionStyle, }) {
|
|
345
417
|
const theme = useHjmNativeTheme();
|
|
346
418
|
const metrics = selectionControlRecipe.sizes[size];
|
|
347
419
|
const plate = selectionControlRecipe.presentations[presentation];
|
|
@@ -418,6 +490,7 @@ function ChoiceRow({ kind, label, description, checked, disabled, readOnly, requ
|
|
|
418
490
|
paddingVertical: plate.useSizePadding ? metrics.paddingVertical : 0,
|
|
419
491
|
},
|
|
420
492
|
style,
|
|
493
|
+
layoutStyle,
|
|
421
494
|
], children: [indicator === "default" ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: [
|
|
422
495
|
{
|
|
423
496
|
alignItems: "center",
|
|
@@ -448,6 +521,7 @@ function ChoiceRow({ kind, label, description, checked, disabled, readOnly, requ
|
|
|
448
521
|
], variant: metrics.descriptionVariant, children: description })) : null] })] }));
|
|
449
522
|
}
|
|
450
523
|
export function Checkbox({ label, checked, defaultChecked = false, onCheckedChange, disabled = false, readOnly = false, required = false, invalid = false, description, readOnlyLabel, requiredLabel, invalidLabel, leading, renderLeading, renderIndicator, accessibilityHint, ...visual }) {
|
|
524
|
+
warnChoiceVisualStyles("Checkbox", visual);
|
|
451
525
|
const [selected, setSelected] = useControllableState({
|
|
452
526
|
...(checked === undefined ? {} : { value: checked }),
|
|
453
527
|
defaultValue: defaultChecked,
|
|
@@ -459,6 +533,7 @@ export function Checkbox({ label, checked, defaultChecked = false, onCheckedChan
|
|
|
459
533
|
}
|
|
460
534
|
/** Standalone native radio item. Prefer RadioGroup when group state is owned here. */
|
|
461
535
|
export function Radio({ label, checked, defaultChecked = false, onCheckedChange, disabled = false, readOnly = false, required = false, invalid = false, description, readOnlyLabel, requiredLabel, invalidLabel, leading, renderLeading, renderIndicator, accessibilityHint, ...visual }) {
|
|
536
|
+
warnChoiceVisualStyles("Radio", visual);
|
|
462
537
|
const [selected, setSelected] = useControllableState({
|
|
463
538
|
...(checked === undefined ? {} : { value: checked }),
|
|
464
539
|
defaultValue: defaultChecked,
|
|
@@ -468,7 +543,7 @@ export function Radio({ label, checked, defaultChecked = false, onCheckedChange,
|
|
|
468
543
|
});
|
|
469
544
|
return (_jsx(ChoiceRow, { ...visual, accessibilityHint: accessibilityHint, checked: selected, description: description, disabled: disabled, indicator: visual.indicator ?? "default", invalid: invalid, invalidLabel: invalidLabel, kind: "radio", label: label, leading: leading, onActivate: () => setSelected(true), readOnly: readOnly, readOnlyLabel: readOnlyLabel, renderIndicator: renderIndicator, renderLeading: renderLeading, required: required, requiredLabel: requiredLabel }));
|
|
470
545
|
}
|
|
471
|
-
function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel, readOnly, readOnlyLabel, disabled, description, error, role, orientation, presentation, style, children, }) {
|
|
546
|
+
function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel, readOnly, readOnlyLabel, disabled, description, error, role, orientation, presentation, style, layoutStyle, children, }) {
|
|
472
547
|
const theme = useHjmNativeTheme();
|
|
473
548
|
const id = useId().replaceAll(":", "");
|
|
474
549
|
const labelId = `${id}-label`;
|
|
@@ -480,16 +555,18 @@ function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel,
|
|
|
480
555
|
].filter(Boolean).join(", ");
|
|
481
556
|
const gap = selectionGroupRecipe.orientations[orientation].gap[presentation];
|
|
482
557
|
return (_jsxs(View, { accessibilityHint: [error ?? description, readOnly ? readOnlyLabel : undefined]
|
|
483
|
-
.filter(Boolean).join(". ") || undefined, accessibilityLabel: announcedName, accessibilityLabelledBy: label ? labelId : undefined, accessibilityRole: role, accessibilityState: { disabled: disabled || readOnly }, accessibilityValue: error ? { text: error } : undefined, style: [{ direction: theme.environment.direction, gap: selectionGroupRecipe.supportGap }, style], children: [label ? (_jsxs(Text, { nativeID: labelId, tone: "primary", variant: selectionGroupRecipe.label.textVariant, children: [label, required ? requiredLabel ? ` (${requiredLabel})` : " *" : ""] })) : null, _jsx(View, { style: {
|
|
558
|
+
.filter(Boolean).join(". ") || undefined, accessibilityLabel: announcedName, accessibilityLabelledBy: label ? labelId : undefined, accessibilityRole: role, accessibilityState: { disabled: disabled || readOnly }, accessibilityValue: error ? { text: error } : undefined, style: [{ direction: theme.environment.direction, gap: selectionGroupRecipe.supportGap }, style, layoutStyle], children: [label ? (_jsxs(Text, { nativeID: labelId, tone: "primary", variant: selectionGroupRecipe.label.textVariant, children: [label, required ? requiredLabel ? ` (${requiredLabel})` : " *" : ""] })) : null, description && !error ? (_jsx(Text, { tone: "muted", variant: selectionGroupRecipe.description.textVariant, children: description })) : null, _jsx(View, { style: {
|
|
484
559
|
direction: theme.environment.direction,
|
|
485
560
|
flexDirection: orientation === "horizontal" && theme.environment.textScale < 1.6
|
|
486
561
|
? "row"
|
|
487
562
|
: "column",
|
|
488
563
|
gap,
|
|
489
|
-
}, children: children }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", tone: "danger", variant: selectionGroupRecipe.error.textVariant, children: error })) :
|
|
564
|
+
}, children: children }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", tone: "danger", variant: selectionGroupRecipe.error.textVariant, children: error })) : null] }));
|
|
490
565
|
}
|
|
491
566
|
export function RadioGroup(props) {
|
|
492
|
-
const { label, accessibilityLabel, items, value, defaultValue, onValueChange, required = false, disabled = false, readOnly = false, invalid = false, description, error, requiredLabel, readOnlyLabel, invalidLabel, orientation = selectionGroupBehaviorDefaults.orientation, presentation = selectionGroupRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, style, ...slotStyles } = props;
|
|
567
|
+
const { label, accessibilityLabel, items, value, defaultValue, onValueChange, required = false, disabled = false, readOnly = false, invalid = false, description, error, requiredLabel, readOnlyLabel, invalidLabel, orientation = selectionGroupBehaviorDefaults.orientation, presentation = selectionGroupRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, layoutStyle, style, ...slotStyles } = props;
|
|
568
|
+
// Group `style` paints the frame; the remaining slot styles reach every row.
|
|
569
|
+
warnChoiceVisualStyles("RadioGroup", { style, ...slotStyles });
|
|
493
570
|
// Removed aliases must not silently change the selected collection in JavaScript callers.
|
|
494
571
|
if ("options" in props || !Array.isArray(items))
|
|
495
572
|
throw new TypeError("RadioGroup requires items; options was removed");
|
|
@@ -521,14 +598,15 @@ export function RadioGroup(props) {
|
|
|
521
598
|
setSelected(selected);
|
|
522
599
|
}, [selected, setSelected, storedValue, value]);
|
|
523
600
|
const hasError = invalid || error !== undefined;
|
|
524
|
-
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, role: "radiogroup", style: style, children: resolvedItems.map((item) => {
|
|
601
|
+
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, role: "radiogroup", style: style, layoutStyle: layoutStyle, children: resolvedItems.map((item) => {
|
|
525
602
|
const optionDisabled = disabled || item.disabled === true;
|
|
526
603
|
const optionSelected = selected === item.value;
|
|
527
604
|
return (_createElement(ChoiceRow, { ...slotStyles, key: item.value, accessibilityHint: item.accessibilityHint, checked: optionSelected, description: item.description, disabled: optionDisabled, indicator: indicator, invalid: hasError, invalidLabel: invalidLabel ?? error, kind: "radio", label: item.label, leading: item.leading, onActivate: () => setSelected(item.value), webTabIndex: !optionDisabled && !readOnly && item.value === webTabStop ? 0 : -1, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, renderIndicator: renderIndicator ? (props) => renderIndicator(item, props) : undefined, renderLeading: renderLeading ? (props) => renderLeading(item, props) : undefined, required: required, requiredLabel: requiredLabel, size: size }));
|
|
528
605
|
}) }));
|
|
529
606
|
}
|
|
530
607
|
/** Validated controlled/uncontrolled checkbox collection using immutable Sets. */
|
|
531
|
-
export function CheckboxGroup({ label, accessibilityLabel, items, value, defaultValue = new Set(), onValueChange, required = false, disabled = false, readOnly = false, invalid = false, description, error, requiredLabel, readOnlyLabel, invalidLabel, orientation = selectionGroupBehaviorDefaults.orientation, presentation = selectionGroupRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, style, ...slotStyles }) {
|
|
608
|
+
export function CheckboxGroup({ label, accessibilityLabel, items, value, defaultValue = new Set(), onValueChange, required = false, disabled = false, readOnly = false, invalid = false, description, error, requiredLabel, readOnlyLabel, invalidLabel, orientation = selectionGroupBehaviorDefaults.orientation, presentation = selectionGroupRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, layoutStyle, style, ...slotStyles }) {
|
|
609
|
+
warnChoiceVisualStyles("CheckboxGroup", { style, ...slotStyles });
|
|
532
610
|
validateSelectionItems(items);
|
|
533
611
|
if (value !== undefined)
|
|
534
612
|
validateCheckboxSelection(items, value);
|
|
@@ -543,7 +621,7 @@ export function CheckboxGroup({ label, accessibilityLabel, items, value, default
|
|
|
543
621
|
setSelected(selected);
|
|
544
622
|
}, [selected, setSelected, storedValue, value]);
|
|
545
623
|
const hasError = invalid || error !== undefined;
|
|
546
|
-
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, style: style, children: items.map((item) => {
|
|
624
|
+
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, style: style, layoutStyle: layoutStyle, children: items.map((item) => {
|
|
547
625
|
const optionDisabled = disabled || item.disabled === true;
|
|
548
626
|
const optionSelected = selected.has(item.id);
|
|
549
627
|
return (_createElement(ChoiceRow, { ...slotStyles, key: item.id, checked: optionSelected, description: item.description, disabled: optionDisabled, indicator: indicator, invalid: hasError, invalidLabel: invalidLabel ?? error, kind: "checkbox", label: item.label, onActivate: () => setSelected(toggleCheckboxSelection(items, selected, item.id)), presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, renderIndicator: renderIndicator ? (props) => renderIndicator(item, props) : undefined, renderLeading: renderLeading ? (props) => renderLeading(item, props) : undefined, required: required, requiredLabel: requiredLabel, size: size }));
|
|
@@ -554,6 +632,7 @@ export function Switch({ label, labelVisibility = "visible", presentation = swit
|
|
|
554
632
|
if (["value", "defaultValue", "onValueChange"].some(key => key in props)) {
|
|
555
633
|
throw new TypeError("Switch no longer accepts value/defaultValue/onValueChange; use checked/defaultChecked/onCheckedChange");
|
|
556
634
|
}
|
|
635
|
+
warnDeprecatedStyleProps("Switch", { style }, "layoutStyle for placement and presentation/size for appearance");
|
|
557
636
|
const { colors, environment, ...nativeTheme } = useHjmNativeTheme();
|
|
558
637
|
const dimensions = switchRecipe.sizes[size];
|
|
559
638
|
const stacked = presentation === "row" && labelVisibility === "visible"
|
|
@@ -610,15 +689,19 @@ export function Switch({ label, labelVisibility = "visible", presentation = swit
|
|
|
610
689
|
: { height: dimensions.height, width: dimensions.width }, thumbColor: thumb, trackColor: { false: trackOff, true: trackOn }, value: enabled })] }));
|
|
611
690
|
}
|
|
612
691
|
export function SegmentedControl(props) {
|
|
613
|
-
const { label, items, value, defaultValue, onValueChange, size = segmentedControlRecipe.defaults.size, disabled = false, style, } = props;
|
|
692
|
+
const { label, items, value, defaultValue, onValueChange, size = segmentedControlRecipe.defaults.size, presentation = "connected", disabled = false, layoutStyle, style, } = props;
|
|
614
693
|
// Removed aliases must not silently change the selected collection in JavaScript callers.
|
|
615
694
|
if ("options" in props || !Array.isArray(items))
|
|
616
695
|
throw new TypeError("SegmentedControl requires items; options was removed");
|
|
696
|
+
warnDeprecatedStyleProps("SegmentedControl", { style }, "layoutStyle for placement and size for appearance");
|
|
697
|
+
const pills = presentation === "pills";
|
|
617
698
|
const resolvedItems = items;
|
|
618
699
|
const theme = useHjmNativeTheme();
|
|
619
700
|
const { environment } = theme;
|
|
620
701
|
const sizeContract = segmentedControlRecipe.sizes[size];
|
|
621
|
-
|
|
702
|
+
// Pills keep their row at large text (segmentedControlRecipe.pills.largeTextLayout, 1.13.1): they wrap in a
|
|
703
|
+
// block and stay one line in a rail. Stacking them made a filter rail a ~440pt column (utilverse, 2026-10-06).
|
|
704
|
+
const stacked = !pills && segmentedControlRecipe.adaptive.largeTextLayout === "stacked"
|
|
622
705
|
&& environment.textScale >= segmentedControlRecipe.adaptive.stackAtFontScale;
|
|
623
706
|
const descriptors = resolvedItems.map((item) => ({
|
|
624
707
|
id: item.value,
|
|
@@ -651,21 +734,23 @@ export function SegmentedControl(props) {
|
|
|
651
734
|
}, [collectionFallback, controlled, setSelected, storedValueValid]);
|
|
652
735
|
return (_jsx(View, { accessibilityLabel: label, accessibilityRole: "radiogroup", style: [
|
|
653
736
|
{
|
|
654
|
-
backgroundColor: resolveColorReference(segmentedControlRecipe.container.background, theme.palette),
|
|
737
|
+
backgroundColor: pills ? "transparent" : resolveColorReference(segmentedControlRecipe.container.background, theme.palette),
|
|
655
738
|
borderColor: resolveColorReference(segmentedControlRecipe.container.border, theme.palette),
|
|
656
739
|
borderRadius: radius[segmentedControlRecipe.container.radius],
|
|
657
|
-
borderWidth: segmentedControlRecipe.container.borderWidth,
|
|
740
|
+
borderWidth: pills ? 0 : segmentedControlRecipe.container.borderWidth,
|
|
658
741
|
direction: environment.direction,
|
|
659
742
|
flexDirection: stacked ? "column" : "row",
|
|
660
|
-
|
|
661
|
-
|
|
743
|
+
flexWrap: pills ? "wrap" : "nowrap",
|
|
744
|
+
gap: pills ? segmentedControlRecipe.pills.gap : segmentedControlRecipe.container.gap,
|
|
745
|
+
padding: pills ? 0 : segmentedControlRecipe.container.padding,
|
|
662
746
|
},
|
|
663
747
|
style,
|
|
748
|
+
layoutStyle,
|
|
664
749
|
], children: resolvedItems.map((item) => {
|
|
665
750
|
const isSelected = item.value === selected;
|
|
666
751
|
const optionDisabled = disabled || item.disabled === true;
|
|
667
752
|
const contentColor = resolveColorReference(isSelected
|
|
668
|
-
? segmentedControlRecipe.item.selectedContent
|
|
753
|
+
? (pills ? segmentedControlRecipe.pills.selectedContent : segmentedControlRecipe.item.selectedContent)
|
|
669
754
|
: segmentedControlRecipe.item.idleContent, theme.palette);
|
|
670
755
|
const leading = item.leading ?? item.renderLeading?.({
|
|
671
756
|
selected: isSelected,
|
|
@@ -676,29 +761,31 @@ export function SegmentedControl(props) {
|
|
|
676
761
|
return (_jsxs(Pressable, { accessibilityLabel: item.label, accessibilityRole: "radio", accessibilityState: { checked: isSelected, disabled: optionDisabled }, disabled: optionDisabled, hitSlop: sizeContract.hitSlop, onPress: () => setSelected(item.value), style: ({ pressed }) => [
|
|
677
762
|
{
|
|
678
763
|
alignItems: "center",
|
|
679
|
-
backgroundColor: isSelected
|
|
764
|
+
backgroundColor: !pills && isSelected
|
|
680
765
|
? resolveColorReference(segmentedControlRecipe.item.selectedBackground, theme.palette)
|
|
681
766
|
: "transparent",
|
|
682
767
|
borderColor: isSelected
|
|
683
768
|
? resolveColorReference(segmentedControlRecipe.item.selectedBorder, theme.palette)
|
|
684
769
|
: "transparent",
|
|
685
770
|
borderRadius: radius[segmentedControlRecipe.item.radius],
|
|
686
|
-
borderWidth: isSelected
|
|
771
|
+
borderWidth: !pills && isSelected
|
|
687
772
|
? segmentedControlRecipe.item.selectedBorderWidth
|
|
688
773
|
: 0,
|
|
689
|
-
flex: stacked ? undefined : 1,
|
|
774
|
+
flex: stacked || pills ? undefined : 1,
|
|
775
|
+
maxWidth: pills ? "100%" : undefined,
|
|
690
776
|
gap: segmentedControlRecipe.item.gap,
|
|
691
777
|
justifyContent: "center",
|
|
692
|
-
minHeight: sizeContract.minHeight,
|
|
778
|
+
minHeight: pills ? segmentedControlRecipe.pills.minHeight : sizeContract.minHeight,
|
|
693
779
|
opacity: optionDisabled
|
|
694
780
|
? segmentedControlRecipe.item.disabledOpacity
|
|
695
781
|
: pressed
|
|
696
782
|
? segmentedControlRecipe.item.pressedOpacity
|
|
697
783
|
: 1,
|
|
698
|
-
paddingHorizontal: sizeContract.paddingHorizontal,
|
|
784
|
+
paddingHorizontal: pills ? spacing.md : sizeContract.paddingHorizontal,
|
|
785
|
+
paddingVertical: pills ? segmentedControlRecipe.pills.inset : undefined,
|
|
699
786
|
width: stacked ? "100%" : undefined,
|
|
700
787
|
},
|
|
701
|
-
], children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(Text, { align: "center", style: {
|
|
788
|
+
], children: [pills ? _jsx(View, { pointerEvents: "none", accessible: false, style: { position: "absolute", left: 0, right: 0, top: segmentedControlRecipe.pills.inset, bottom: segmentedControlRecipe.pills.inset, borderRadius: segmentedControlRecipe.pills.radius, backgroundColor: isSelected ? resolveColorReference(segmentedControlRecipe.pills.selectedBackground, theme.palette) : theme.colors.surfaceAlt } }) : null, leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(Text, { align: "center", style: {
|
|
702
789
|
color: contentColor,
|
|
703
790
|
fontWeight: isSelected
|
|
704
791
|
? segmentedControlRecipe.item.selectedFontWeight
|
|
@@ -709,6 +796,7 @@ export function SegmentedControl(props) {
|
|
|
709
796
|
/** Action/filter chip with role-specific, controlled selection semantics. */
|
|
710
797
|
export function Chip({ label, size = chipRecipe.defaults.size, disabled = false, leading, trailing, accessibilityLabel, accessibilityHint, layoutStyle, leadingStyle, indicatorStyle, labelStyle, trailingStyle, renderSelectionIndicator, selectionMode = "action", selected, onPress, }) {
|
|
711
798
|
const theme = useHjmNativeTheme();
|
|
799
|
+
warnDeprecatedStyleProps("Chip", { labelStyle }, "size/selected for label typography");
|
|
712
800
|
const selectable = selectionMode !== "action";
|
|
713
801
|
const active = selectable && selected === true;
|
|
714
802
|
const metrics = chipRecipe.sizes[size];
|
|
@@ -746,7 +834,10 @@ export function Chip({ label, size = chipRecipe.defaults.size, disabled = false,
|
|
|
746
834
|
direction: theme.environment.direction,
|
|
747
835
|
flexDirection: "row",
|
|
748
836
|
gap: metrics.gap,
|
|
749
|
-
|
|
837
|
+
// A floor, not a fixed height, as Button does for text content and as Web `.hjm-chip` already does
|
|
838
|
+
// (min-block-size): the fixed 36 clipped the filter-trigger and suggested-query labels at accessibility-large
|
|
839
|
+
// (utilverse 1.13.0 adoption, 2026-10-06). At 1x the label fits, so the chip is still 36.
|
|
840
|
+
minHeight: visibleControlHeight(metrics.height, theme.environment.minimumVisualTarget),
|
|
750
841
|
opacity: disabled
|
|
751
842
|
? chipRecipe.states.disabledOpacity
|
|
752
843
|
: pressed
|