@hjmds/react-native 1.41.2 → 1.42.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/dist/actions.d.ts +43 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +87 -43
- package/dist/actions.js.map +1 -1
- package/dist/alert-dialog-queue.d.ts +22 -0
- package/dist/alert-dialog-queue.d.ts.map +1 -0
- package/dist/alert-dialog-queue.js +30 -0
- package/dist/alert-dialog-queue.js.map +1 -0
- package/dist/chart.d.ts +6 -3
- package/dist/chart.d.ts.map +1 -1
- package/dist/chart.js +12 -5
- package/dist/chart.js.map +1 -1
- package/dist/choice-row.d.ts +11 -3
- package/dist/choice-row.d.ts.map +1 -1
- package/dist/choice-row.js +16 -3
- package/dist/choice-row.js.map +1 -1
- package/dist/collection-rail.d.ts.map +1 -1
- package/dist/collection-rail.js +4 -3
- package/dist/collection-rail.js.map +1 -1
- package/dist/data-display.d.ts +53 -16
- package/dist/data-display.d.ts.map +1 -1
- package/dist/data-display.js +121 -47
- package/dist/data-display.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +1 -1
- package/dist/feedback.js.map +1 -1
- package/dist/image-viewer.d.ts +1 -0
- package/dist/image-viewer.d.ts.map +1 -1
- package/dist/image-viewer.js +1 -0
- 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inputs.d.ts +19 -4
- package/dist/inputs.d.ts.map +1 -1
- package/dist/inputs.js +112 -39
- package/dist/inputs.js.map +1 -1
- package/dist/internal/image-viewer-trigger.d.ts +18 -0
- package/dist/internal/image-viewer-trigger.d.ts.map +1 -0
- package/dist/internal/image-viewer-trigger.js +21 -0
- package/dist/internal/image-viewer-trigger.js.map +1 -0
- package/dist/internal/list-row-private.d.ts +5 -0
- package/dist/internal/list-row-private.d.ts.map +1 -1
- package/dist/internal/styles.d.ts +10 -1
- package/dist/internal/styles.d.ts.map +1 -1
- package/dist/internal/styles.js +14 -1
- package/dist/internal/styles.js.map +1 -1
- package/dist/notification-bell.d.ts.map +1 -1
- package/dist/notification-bell.js +6 -2
- package/dist/notification-bell.js.map +1 -1
- package/dist/primitives.d.ts +7 -2
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +9 -14
- package/dist/primitives.js.map +1 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +2 -2
- package/dist/provider.js.map +1 -1
- package/dist/public-exports.generated.d.ts +5 -0
- package/dist/public-exports.generated.d.ts.map +1 -1
- package/dist/public-exports.generated.js +16 -0
- package/dist/public-exports.generated.js.map +1 -1
- package/dist/screen-flows.d.ts +5 -1
- package/dist/screen-flows.d.ts.map +1 -1
- package/dist/screen-flows.js +12 -4
- package/dist/screen-flows.js.map +1 -1
- package/dist/screens.d.ts +60 -6
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +74 -15
- package/dist/screens.js.map +1 -1
- package/dist/social-compositions.js +1 -1
- package/dist/social-compositions.js.map +1 -1
- package/dist/steps.d.ts +9 -10
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +53 -17
- package/dist/steps.js.map +1 -1
- package/dist/top.d.ts.map +1 -1
- package/dist/top.js +4 -4
- package/dist/top.js.map +1 -1
- package/package.json +9 -3
package/dist/inputs.js
CHANGED
|
@@ -14,9 +14,10 @@ import { chipRecipe, searchFieldRecipe, segmentedControlRecipe, selectionControl
|
|
|
14
14
|
import { visibleControlHeight } from "@hjmds/design-contracts/components/design-system-provider";
|
|
15
15
|
import { passwordFieldRecipe, resolvePasswordFieldDescriptor, } from "@hjmds/design-contracts/components/password-field";
|
|
16
16
|
import { getOtpFieldSlotValues, otpFieldRecipe, resolveOtpFieldValue, } from "@hjmds/design-contracts/components/otp-field";
|
|
17
|
-
import {
|
|
17
|
+
import { characterCountRecipe, resolveCharacterCount, resolveCharacterCountMaxLength, shouldAnnounceCharacterCount, } from "@hjmds/design-contracts/character-count";
|
|
18
|
+
import { getCheckboxNextState, reconcileCheckboxSelection, resolveControlAccessibleName, resolveInitialRadioValue, resolveRadioTabStop, resolveInitialTabValue, reconcileRadioSelection, resolveScrollRevealOffset, selectionGroupBehaviorDefaults, toggleCheckboxSelection, validateCheckboxSelection, validateRadioSelection, validateSelectionItems, } from "@hjmds/design-contracts/behaviors";
|
|
18
19
|
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
|
|
19
|
-
import { ActivityIndicator, Animated, AppState, Easing, Platform, Pressable, ScrollView, Switch as NativeSwitch, Text as NativeText, TextInput, View, } from "react-native";
|
|
20
|
+
import { AccessibilityInfo, ActivityIndicator, Animated, AppState, Easing, Platform, Pressable, ScrollView, Switch as NativeSwitch, Text as NativeText, TextInput, View, } from "react-native";
|
|
20
21
|
import { mixedCheckboxState, useControllableState } from "./internal/state.js";
|
|
21
22
|
import { isWebRenderer, webChoiceProps, webOnly } from "./internal/web-a11y.js";
|
|
22
23
|
import { logicalTextAlign, minimumTargetStyle, resolveNativeFontStyle, resolveNativeTextScaleProps, } from "./internal/styles.js";
|
|
@@ -34,7 +35,36 @@ function resolveFieldAccessibleName(label, accessibilityLabel) {
|
|
|
34
35
|
...(visibleLabel ? { visibleLabel } : {}),
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
/*
|
|
39
|
+
Character count (characterCountRecipe, 2026-10-23 BurnTok TextLengthHint). The row is hidden from the
|
|
40
|
+
accessibility tree and its sentence and counter are appended to the input hint, so they are read on focus without
|
|
41
|
+
a second swipe stop. Entering near/short/over is announced once; announcing each keystroke would talk over the
|
|
42
|
+
typed text. Both platforms use announceForAccessibility because an Android live region on the row would speak on
|
|
43
|
+
every counter change.
|
|
44
|
+
*/
|
|
45
|
+
function useCharacterCount(options, value, ownMaxLength) {
|
|
46
|
+
if (options && ownMaxLength !== undefined) {
|
|
47
|
+
throw new TypeError("Pass the limit through characterCount.max instead of maxLength");
|
|
48
|
+
}
|
|
49
|
+
const state = options ? resolveCharacterCount(value, options) : undefined;
|
|
50
|
+
const previous = useRef(undefined);
|
|
51
|
+
const status = state?.status;
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (state && shouldAnnounceCharacterCount(previous.current, state))
|
|
54
|
+
AccessibilityInfo.announceForAccessibility(state.message ?? "");
|
|
55
|
+
previous.current = status;
|
|
56
|
+
// Only a status change announces; the message inside one status changes with every keystroke.
|
|
57
|
+
}, [status]);
|
|
58
|
+
const maxLength = options ? resolveCharacterCountMaxLength(options) : ownMaxLength;
|
|
59
|
+
return { ...(state ? { state } : {}), ...(maxLength === undefined ? {} : { maxLength }) };
|
|
60
|
+
}
|
|
61
|
+
function CharacterCountRow({ state }) {
|
|
62
|
+
const { colors, environment } = useHjmNativeTheme();
|
|
63
|
+
const tone = state.invalid ? "invalid" : "idle";
|
|
64
|
+
const color = colors[characterCountRecipe.colors[tone]];
|
|
65
|
+
return (_jsxs(View, { accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", testID: "hjm-field-character-count", style: { flexDirection: "row", alignItems: "flex-start", gap: characterCountRecipe.gap, direction: environment.direction }, children: [state.message ? _jsx(Text, { variant: characterCountRecipe.textVariant, style: { color, flex: 1 }, children: state.message }) : null, _jsx(Text, { variant: characterCountRecipe.textVariant, style: { color, flexShrink: 0, marginStart: "auto", fontVariant: [characterCountRecipe.numeric], fontWeight: characterCountRecipe.counterFontWeight[tone] }, children: state.counter })] }));
|
|
66
|
+
}
|
|
67
|
+
const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultValue = "", onValueChange, description, error, invalid = false, accessibilityHint, required = false, disabled = false, busy = false, variant = fieldRecipe.defaults.variant, shape, accessibilityLabel, layoutStyle, allowFontScaling, multiline, maxVisibleLines, minVisibleLines, align = fieldRecipe.defaults.align, search, recipeMinHeight, recipeTextVariant, disabledOpacity, searchSize = searchFieldRecipe.defaults.size, leading, trailing, leadingAction, hjmCompactMultiline = false, hjmDescriptionAlign = "start", initialFocusTarget, onBlur, onFocus, onContentSizeChange, onSelectionChange, characterCount, maxLength: ownMaxLength, ...props }, ref) {
|
|
38
68
|
const resolvedDisabledOpacity = disabledOpacity ?? (search ? searchFieldRecipe.states.disabledOpacity : fieldRecipe.disabledOpacity);
|
|
39
69
|
const theme = useHjmNativeTheme();
|
|
40
70
|
const { colors, environment, textScaling } = theme;
|
|
@@ -61,21 +91,25 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
61
91
|
// Disabled and read-only fields are never focused on entry (busy plain fields are locked too).
|
|
62
92
|
useInitialFocusTarget(inputRef, initialFocusTarget, () => !disabled && !props.readOnly && props.editable !== false && (search || !busy));
|
|
63
93
|
const supportText = description;
|
|
64
|
-
const
|
|
94
|
+
const count = useCharacterCount(characterCount, currentValue, ownMaxLength);
|
|
95
|
+
const countHint = count.state ? [count.state.message, count.state.counter].filter(Boolean).join(", ") : undefined;
|
|
96
|
+
const hint = [error ?? accessibilityHint ?? supportText, countHint].filter(Boolean).join(", ") || undefined;
|
|
97
|
+
// Over the limit (overflow "allow") draws the invalid border like an external group error.
|
|
98
|
+
const invalidState = invalid || count.state?.status === "over";
|
|
65
99
|
const { accessibleName, visibleLabel } = resolveFieldAccessibleName(label, accessibilityLabel);
|
|
66
100
|
const resolvedShape = shape ??
|
|
67
101
|
(search ? searchFieldRecipe.defaults.shape : fieldRecipe.defaults.shape);
|
|
68
102
|
const searchSizing = searchFieldRecipe.sizes[searchSize];
|
|
69
103
|
const resolvedMaxVisibleLines = maxVisibleLines ?? fieldRecipe.multilineMaxVisibleLines;
|
|
70
|
-
const borderWidth = error ||
|
|
104
|
+
const borderWidth = error || invalidState ? fieldRecipe.states.invalid.borderWidth
|
|
71
105
|
: search ? searchFieldRecipe.borderWidth : fieldRecipe.borderWidth;
|
|
72
106
|
const borderColor = search
|
|
73
|
-
? resolveColorReference(error ||
|
|
107
|
+
? resolveColorReference(error || invalidState
|
|
74
108
|
? searchFieldRecipe.colors.invalid
|
|
75
109
|
: focused
|
|
76
110
|
? searchFieldRecipe.colors.focus
|
|
77
111
|
: searchFieldRecipe.colors.border, theme.palette)
|
|
78
|
-
: colors[error ||
|
|
112
|
+
: colors[error || invalidState
|
|
79
113
|
? fieldRecipe.states.invalid.border
|
|
80
114
|
: focused
|
|
81
115
|
? fieldRecipe.states.focused.border
|
|
@@ -158,7 +192,7 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
158
192
|
// fields keep the lock because their busy means a pending save of the current value.
|
|
159
193
|
// iOS scrolls the one-line viewport before reporting intrinsic growth. Keep bounded
|
|
160
194
|
// composers unscrollable until their content exceeds the recipe cap; ordinary editors retain caller behavior.
|
|
161
|
-
const editor = (_jsx(TextInput, { ...props, ...inputTextScaleProps, ref: attachInput, accessibilityHint: hint, accessibilityLabel: accessibleName, accessibilityRole: search ? "search" : undefined, accessibilityState: { busy, disabled }, editable: !disabled && !props.readOnly && props.editable !== false && (search || !busy), multiline: multiline, scrollEnabled: multiline && minVisibleLines !== undefined
|
|
195
|
+
const editor = (_jsx(TextInput, { ...props, ...(count.maxLength === undefined ? {} : { maxLength: count.maxLength }), ...inputTextScaleProps, ref: attachInput, accessibilityHint: hint, accessibilityLabel: accessibleName, accessibilityRole: search ? "search" : undefined, accessibilityState: { busy, disabled }, editable: !disabled && !props.readOnly && props.editable !== false && (search || !busy), multiline: multiline, scrollEnabled: multiline && minVisibleLines !== undefined
|
|
162
196
|
? !!currentValue && contentHeight > textStyle.lineHeight * (resolvedMaxVisibleLines ?? 1000) + fieldRecipe.paddingVertical * 2
|
|
163
197
|
: props.scrollEnabled, onBlur: (event) => {
|
|
164
198
|
setFocused(false);
|
|
@@ -176,26 +210,26 @@ const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultV
|
|
|
176
210
|
setFocused(true);
|
|
177
211
|
onFocus?.(event);
|
|
178
212
|
}, placeholderTextColor: placeholderColor, value: currentValue }, editorKey));
|
|
179
|
-
return (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
return (_jsxs(NativeFieldFrame, { frameRef: fieldFrameRef, descriptionAlign: hjmDescriptionAlign, ...(visibleLabel === undefined ? {} : { label: visibleLabel }), required: required, ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { description: supportText }), ...(disabled ? { disabledOpacity: resolvedDisabledOpacity } : {}), style: layoutStyle, children: [_jsxs(View, { style: {
|
|
214
|
+
opacity: disabled ? resolvedDisabledOpacity : 1,
|
|
215
|
+
alignItems: multiline ? "stretch" : "center",
|
|
216
|
+
backgroundColor,
|
|
217
|
+
borderColor,
|
|
218
|
+
borderRadius: controlRadius,
|
|
219
|
+
borderWidth,
|
|
220
|
+
direction: environment.direction,
|
|
221
|
+
flexDirection: "row",
|
|
222
|
+
gap: search ? searchSizing.gap : 0,
|
|
223
|
+
minHeight,
|
|
224
|
+
paddingHorizontal: search
|
|
225
|
+
? searchSizing.paddingHorizontal
|
|
226
|
+
: fieldRecipe.paddingHorizontal,
|
|
227
|
+
}, children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, leadingAction ? _jsx(View, { style: { alignSelf: "center" }, children: leadingAction }) : null, multiline && minVisibleLines !== undefined ? _jsxs(View, { style: { flex: 1, flexDirection: "row" }, children: [_jsx(NativeText, { ...resolveNativeTextScaleProps(textScaling, [{
|
|
228
|
+
...resolveNativeFontStyle(theme.tokens.fontFamily.ui), fontSize: textStyle.fontSize,
|
|
229
|
+
fontWeight: textStyle.fontWeight, lineHeight: textStyle.lineHeight,
|
|
230
|
+
position: "absolute", left: 0, right: 0, opacity: 0,
|
|
231
|
+
paddingVertical: fieldRecipe.paddingVertical,
|
|
232
|
+
}], allowFontScaling), accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", onTextLayout: event => setContentHeight(event.nativeEvent.lines.reduce((height, line) => height + Math.max(line.height, textStyle.lineHeight), 0) + fieldRecipe.paddingVertical * 2), children: currentValue + "\u200b" }), editor] }) : editor, trailing] }), count.state ? _jsx(CharacterCountRow, { state: count.state }) : null] }));
|
|
199
233
|
});
|
|
200
234
|
export const TextField = forwardRef(function TextField(props, ref) {
|
|
201
235
|
return _jsx(FieldRenderer, { ...props, ref: ref, multiline: false, search: false });
|
|
@@ -410,7 +444,7 @@ layoutStyle, allowFontScaling, onBlur, onFocus, ...props }, ref) {
|
|
|
410
444
|
}) })] }), _jsx(FieldMessage, { ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { supportText }) })] })] }));
|
|
411
445
|
});
|
|
412
446
|
// UI control labels keep the ui font even when their metric variant is body; content still uses reading.
|
|
413
|
-
function ChoiceRow({ kind, label, labelContent, action, 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, }) {
|
|
447
|
+
function ChoiceRow({ kind, label, labelContent, action, 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, onRailLayout, onRailFocus, }) {
|
|
414
448
|
const theme = useHjmNativeTheme();
|
|
415
449
|
const metrics = selectionControlRecipe.sizes[size];
|
|
416
450
|
const chips = presentation === "chips", chipMetrics = chipRecipe.sizes[size];
|
|
@@ -458,7 +492,7 @@ function ChoiceRow({ kind, label, labelContent, action, description, checked, di
|
|
|
458
492
|
readOnly,
|
|
459
493
|
onActivate,
|
|
460
494
|
...(webTabIndex === undefined ? {} : { tabIndex: webTabIndex }),
|
|
461
|
-
})), disabled: disabled || readOnly, hitSlop: chips ? chipMetrics.hitSlop : plate.useSizePadding ? 0 : metrics.hitSlop, onPress: () => {
|
|
495
|
+
})), disabled: disabled || readOnly, hitSlop: chips ? chipMetrics.hitSlop : plate.useSizePadding ? 0 : metrics.hitSlop, ...(onRailFocus ? { onFocus: onRailFocus } : {}), ...(onRailLayout && !action ? { onLayout: onRailLayout } : {}), onPress: () => {
|
|
462
496
|
if (!readOnly)
|
|
463
497
|
onActivate();
|
|
464
498
|
}, style: ({ pressed }) => [
|
|
@@ -497,7 +531,7 @@ function ChoiceRow({ kind, label, labelContent, action, description, checked, di
|
|
|
497
531
|
return target;
|
|
498
532
|
// The selection target owns its accessible label; the action is a separate sibling
|
|
499
533
|
// so rename/manage never changes the selected option or nests a button in a radio.
|
|
500
|
-
return _jsxs(View, { style: [{
|
|
534
|
+
return _jsxs(View, { ...(onRailLayout ? { onLayout: onRailLayout } : {}), style: [{
|
|
501
535
|
alignItems: "center", alignSelf: chips ? "flex-start" : "stretch", flexDirection: "row", direction: theme.environment.direction,
|
|
502
536
|
minHeight: rowMinHeight,
|
|
503
537
|
flexGrow: presentation === "card" ? 1 : undefined,
|
|
@@ -526,7 +560,34 @@ export function Radio({ label, checked, defaultChecked = false, onCheckedChange,
|
|
|
526
560
|
});
|
|
527
561
|
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 }));
|
|
528
562
|
}
|
|
529
|
-
|
|
563
|
+
/** Measures chips in the rail and scrolls the least distance that shows a focused chip (resolveScrollRevealOffset). */
|
|
564
|
+
function useChipRail(enabled) {
|
|
565
|
+
const ref = useRef(null);
|
|
566
|
+
const offset = useRef(0);
|
|
567
|
+
const viewport = useRef(0);
|
|
568
|
+
const rects = useRef({});
|
|
569
|
+
if (!enabled)
|
|
570
|
+
return undefined;
|
|
571
|
+
return {
|
|
572
|
+
ref,
|
|
573
|
+
onScroll: event => { offset.current = event.nativeEvent.contentOffset.x; },
|
|
574
|
+
onViewportLayout: event => { viewport.current = event.nativeEvent.layout.width; },
|
|
575
|
+
itemLayout: id => event => { const { x, width } = event.nativeEvent.layout; rects.current[id] = { x, width }; },
|
|
576
|
+
reveal: id => {
|
|
577
|
+
const rect = rects.current[id];
|
|
578
|
+
if (!rect || viewport.current <= 0)
|
|
579
|
+
return;
|
|
580
|
+
// Layout x is inside the padded content row; the scroll offset counts the content padding too.
|
|
581
|
+
const inset = selectionGroupRecipe.overflow.scrollInset;
|
|
582
|
+
const next = resolveScrollRevealOffset(offset.current, viewport.current, rect.x + inset, rect.width, inset);
|
|
583
|
+
if (next !== offset.current) {
|
|
584
|
+
offset.current = next;
|
|
585
|
+
ref.current?.scrollTo({ x: next, animated: false });
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel, readOnly, readOnlyLabel, disabled, description, error, role, orientation, presentation, grid, layoutStyle, rail, children, }) {
|
|
530
591
|
const theme = useHjmNativeTheme();
|
|
531
592
|
const id = useId().replaceAll(":", "");
|
|
532
593
|
const labelId = `${id}-label`;
|
|
@@ -539,7 +600,11 @@ function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel,
|
|
|
539
600
|
const gap = selectionGroupRecipe.orientations[orientation].gap[presentation];
|
|
540
601
|
useFieldErrorAnnouncement(error);
|
|
541
602
|
return (_jsxs(View, { accessibilityHint: [error ?? description, readOnly ? readOnlyLabel : undefined]
|
|
542
|
-
.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 }, 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,
|
|
603
|
+
.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 }, 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, rail ? (
|
|
604
|
+
// Same scroller as SegmentedControl overflow="scroll": an LTR scroll view with the reading direction on
|
|
605
|
+
// its row. selectionGroupRecipe.overflow.scrollInset pads every side (focus ring, chip hit slop, equal end
|
|
606
|
+
// gap) and the negative margin keeps the chips aligned with the column.
|
|
607
|
+
_jsx(ScrollView, { ref: rail.ref, horizontal: true, keyboardShouldPersistTaps: "handled", onScroll: rail.onScroll, scrollEventThrottle: 16, showsHorizontalScrollIndicator: false, onLayout: rail.onViewportLayout, style: { direction: "ltr", flexGrow: 0, margin: -selectionGroupRecipe.overflow.scrollInset }, contentContainerStyle: { padding: selectionGroupRecipe.overflow.scrollInset }, children: _jsx(View, { style: { direction: theme.environment.direction, flexDirection: "row", flexWrap: "nowrap", gap }, children: children }) })) : grid ? _jsx(Grid, { ...grid, children: children }) : (_jsx(View, { style: {
|
|
543
608
|
direction: theme.environment.direction,
|
|
544
609
|
flexDirection: presentation === "chips" || (orientation === "horizontal" && theme.environment.textScale < 1.6) ? "row" : "column",
|
|
545
610
|
flexWrap: presentation === "chips" ? "wrap" : undefined,
|
|
@@ -586,8 +651,12 @@ export function RadioGroup(props) {
|
|
|
586
651
|
}) }));
|
|
587
652
|
}
|
|
588
653
|
/** Validated controlled/uncontrolled checkbox collection using immutable Sets. */
|
|
589
|
-
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, grid, presentation = selectionGroupRecipe.defaults.presentation, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, layoutStyle, ...slotStyles }) {
|
|
654
|
+
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, grid, presentation = selectionGroupRecipe.defaults.presentation, overflow = selectionGroupRecipe.overflow.default, size = selectionControlRecipe.defaults.size, indicator = "default", renderLeading, renderIndicator, layoutStyle, ...slotStyles }) {
|
|
590
655
|
validateSelectionItems(items);
|
|
656
|
+
if (overflow === "scroll" && (presentation !== "chips" || grid)) {
|
|
657
|
+
throw new TypeError('CheckboxGroup overflow="scroll" requires presentation="chips" without grid');
|
|
658
|
+
}
|
|
659
|
+
const rail = useChipRail(overflow === "scroll");
|
|
591
660
|
if (value !== undefined)
|
|
592
661
|
validateCheckboxSelection(items, value);
|
|
593
662
|
const [storedValue, setSelected] = useControllableState({
|
|
@@ -601,10 +670,10 @@ export function CheckboxGroup({ label, accessibilityLabel, items, value, default
|
|
|
601
670
|
setSelected(selected);
|
|
602
671
|
}, [selected, setSelected, storedValue, value]);
|
|
603
672
|
const hasError = invalid || error !== undefined;
|
|
604
|
-
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, grid: grid, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, layoutStyle: layoutStyle, children: items.map((item) => {
|
|
673
|
+
return (_jsx(ChoiceGroupFrame, { accessibilityLabel: accessibilityLabel, description: description, disabled: disabled, error: error, label: label, orientation: orientation, grid: grid, presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, required: required, requiredLabel: requiredLabel, layoutStyle: layoutStyle, rail: rail, children: items.map((item) => {
|
|
605
674
|
const optionDisabled = disabled || item.disabled === true;
|
|
606
675
|
const optionSelected = selected.has(item.id);
|
|
607
|
-
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 }));
|
|
676
|
+
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, onRailLayout: rail?.itemLayout(item.id), onRailFocus: rail ? () => rail.reveal(item.id) : undefined }));
|
|
608
677
|
}) }));
|
|
609
678
|
}
|
|
610
679
|
export function Switch({ label, labelVisibility = "visible", presentation = switchRecipe.presentationDefaults.native, testID, description, size = switchRecipe.defaults.size, checked, defaultChecked, onCheckedChange, disabled = false, accessibilityLabel, accessibilityHint, layoutStyle, ...props }) {
|
|
@@ -695,7 +764,9 @@ function NativeSelectionHighlight({ rect, selection, reducedMotion, pills, disab
|
|
|
695
764
|
return _jsx(Animated.View, { pointerEvents: "none", accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { position: "absolute", left: positions.x, top: positions.y, width: positions.width, height: positions.height, borderRadius: pills ? segmentedControlRecipe.pills.radius : theme.tokens.radius[segmentedControlRecipe.item.radius], backgroundColor: resolveColorReference(brand ? segmentedControlRecipe.selectedBrand.background : pills ? segmentedControlRecipe.pills.selectedBackground : segmentedControlRecipe.item.selectedBackground, theme.palette), borderColor: resolveColorReference(segmentedControlRecipe.item.selectedBorder, theme.palette), borderWidth: pills ? 0 : segmentedControlRecipe.item.selectedBorderWidth, opacity: disabled ? segmentedControlRecipe.item.disabledOpacity : 1 } });
|
|
696
765
|
}
|
|
697
766
|
export function SegmentedControl(props) {
|
|
698
|
-
const { label, items, value, defaultValue, onValueChange, size = segmentedControlRecipe.defaults.size, presentation = "connected",
|
|
767
|
+
const { label, items, value, defaultValue, onValueChange, size = segmentedControlRecipe.defaults.size, presentation = "connected",
|
|
768
|
+
// Match Web's brand-filled default (2026-10-23 user review); neutral remains opt-in.
|
|
769
|
+
selectionTone = "brand", textEmphasis = "strong", selectionMotion: suppliedSelectionMotion, disabled = false, layoutStyle, overflow = "wrap", actions, testID, } = props;
|
|
699
770
|
// Removed aliases must not silently change the selected collection in JavaScript callers.
|
|
700
771
|
if ("options" in props || !Array.isArray(items))
|
|
701
772
|
throw new TypeError("SegmentedControl requires items; options was removed");
|
|
@@ -763,8 +834,7 @@ export function SegmentedControl(props) {
|
|
|
763
834
|
if (!scrollRow || !rect || trackWidth <= 0)
|
|
764
835
|
return;
|
|
765
836
|
const offset = railOffset.current;
|
|
766
|
-
const next =
|
|
767
|
-
? Math.max(0, rect.x + rect.width - trackWidth) : offset;
|
|
837
|
+
const next = resolveScrollRevealOffset(offset, trackWidth, rect.x, rect.width);
|
|
768
838
|
if (next !== offset) {
|
|
769
839
|
railOffset.current = next;
|
|
770
840
|
railRef.current?.scrollTo({ x: next, animated: false });
|
|
@@ -915,6 +985,9 @@ export const Chip = forwardRef(function Chip({ label, size = chipRecipe.defaults
|
|
|
915
985
|
_jsx(NativeText, { accessible: false, allowFontScaling: false, style: { ...typography.caption, color: indicatorColor }, children: "\u2713" })) })) : null, _jsx(Text, { fontRole: "ui", align: "center", style: [
|
|
916
986
|
{
|
|
917
987
|
color: contentColor,
|
|
988
|
+
// Shrink beside the leading glyph and selection mark so a long label wraps inside the chip (chip.md: labels
|
|
989
|
+
// wrap). At its natural width the label plus the glyphs overran the chip (2026-10-23 flex audit).
|
|
990
|
+
flexShrink: 1,
|
|
918
991
|
fontWeight: active
|
|
919
992
|
? chipRecipe.label.selectedFontWeight
|
|
920
993
|
: chipRecipe.label.fontWeight,
|