@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.
Files changed (83) hide show
  1. package/dist/actions.d.ts +43 -1
  2. package/dist/actions.d.ts.map +1 -1
  3. package/dist/actions.js +87 -43
  4. package/dist/actions.js.map +1 -1
  5. package/dist/alert-dialog-queue.d.ts +22 -0
  6. package/dist/alert-dialog-queue.d.ts.map +1 -0
  7. package/dist/alert-dialog-queue.js +30 -0
  8. package/dist/alert-dialog-queue.js.map +1 -0
  9. package/dist/chart.d.ts +6 -3
  10. package/dist/chart.d.ts.map +1 -1
  11. package/dist/chart.js +12 -5
  12. package/dist/chart.js.map +1 -1
  13. package/dist/choice-row.d.ts +11 -3
  14. package/dist/choice-row.d.ts.map +1 -1
  15. package/dist/choice-row.js +16 -3
  16. package/dist/choice-row.js.map +1 -1
  17. package/dist/collection-rail.d.ts.map +1 -1
  18. package/dist/collection-rail.js +4 -3
  19. package/dist/collection-rail.js.map +1 -1
  20. package/dist/data-display.d.ts +53 -16
  21. package/dist/data-display.d.ts.map +1 -1
  22. package/dist/data-display.js +121 -47
  23. package/dist/data-display.js.map +1 -1
  24. package/dist/evidence.d.ts +1 -1
  25. package/dist/evidence.js +1 -1
  26. package/dist/feedback.d.ts.map +1 -1
  27. package/dist/feedback.js +1 -1
  28. package/dist/feedback.js.map +1 -1
  29. package/dist/image-viewer.d.ts +1 -0
  30. package/dist/image-viewer.d.ts.map +1 -1
  31. package/dist/image-viewer.js +1 -0
  32. package/dist/image-viewer.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/inputs.d.ts +19 -4
  38. package/dist/inputs.d.ts.map +1 -1
  39. package/dist/inputs.js +112 -39
  40. package/dist/inputs.js.map +1 -1
  41. package/dist/internal/image-viewer-trigger.d.ts +18 -0
  42. package/dist/internal/image-viewer-trigger.d.ts.map +1 -0
  43. package/dist/internal/image-viewer-trigger.js +21 -0
  44. package/dist/internal/image-viewer-trigger.js.map +1 -0
  45. package/dist/internal/list-row-private.d.ts +5 -0
  46. package/dist/internal/list-row-private.d.ts.map +1 -1
  47. package/dist/internal/styles.d.ts +10 -1
  48. package/dist/internal/styles.d.ts.map +1 -1
  49. package/dist/internal/styles.js +14 -1
  50. package/dist/internal/styles.js.map +1 -1
  51. package/dist/notification-bell.d.ts.map +1 -1
  52. package/dist/notification-bell.js +6 -2
  53. package/dist/notification-bell.js.map +1 -1
  54. package/dist/primitives.d.ts +7 -2
  55. package/dist/primitives.d.ts.map +1 -1
  56. package/dist/primitives.js +9 -14
  57. package/dist/primitives.js.map +1 -1
  58. package/dist/provider.d.ts +10 -2
  59. package/dist/provider.d.ts.map +1 -1
  60. package/dist/provider.js +2 -2
  61. package/dist/provider.js.map +1 -1
  62. package/dist/public-exports.generated.d.ts +5 -0
  63. package/dist/public-exports.generated.d.ts.map +1 -1
  64. package/dist/public-exports.generated.js +16 -0
  65. package/dist/public-exports.generated.js.map +1 -1
  66. package/dist/screen-flows.d.ts +5 -1
  67. package/dist/screen-flows.d.ts.map +1 -1
  68. package/dist/screen-flows.js +12 -4
  69. package/dist/screen-flows.js.map +1 -1
  70. package/dist/screens.d.ts +60 -6
  71. package/dist/screens.d.ts.map +1 -1
  72. package/dist/screens.js +74 -15
  73. package/dist/screens.js.map +1 -1
  74. package/dist/social-compositions.js +1 -1
  75. package/dist/social-compositions.js.map +1 -1
  76. package/dist/steps.d.ts +9 -10
  77. package/dist/steps.d.ts.map +1 -1
  78. package/dist/steps.js +53 -17
  79. package/dist/steps.js.map +1 -1
  80. package/dist/top.d.ts.map +1 -1
  81. package/dist/top.js +4 -4
  82. package/dist/top.js.map +1 -1
  83. 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 { getCheckboxNextState, reconcileCheckboxSelection, resolveControlAccessibleName, resolveInitialRadioValue, resolveRadioTabStop, resolveInitialTabValue, reconcileRadioSelection, selectionGroupBehaviorDefaults, toggleCheckboxSelection, validateCheckboxSelection, validateRadioSelection, validateSelectionItems, } from "@hjmds/design-contracts/behaviors";
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
- 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, ...props }, ref) {
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 hint = error ?? accessibilityHint ?? supportText;
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 || invalid ? fieldRecipe.states.invalid.borderWidth
104
+ const borderWidth = error || invalidState ? fieldRecipe.states.invalid.borderWidth
71
105
  : search ? searchFieldRecipe.borderWidth : fieldRecipe.borderWidth;
72
106
  const borderColor = search
73
- ? resolveColorReference(error || invalid
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 || invalid
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 (_jsx(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: {
180
- opacity: disabled ? resolvedDisabledOpacity : 1,
181
- alignItems: multiline ? "stretch" : "center",
182
- backgroundColor,
183
- borderColor,
184
- borderRadius: controlRadius,
185
- borderWidth,
186
- direction: environment.direction,
187
- flexDirection: "row",
188
- gap: search ? searchSizing.gap : 0,
189
- minHeight,
190
- paddingHorizontal: search
191
- ? searchSizing.paddingHorizontal
192
- : fieldRecipe.paddingHorizontal,
193
- }, 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, [{
194
- ...resolveNativeFontStyle(theme.tokens.fontFamily.ui), fontSize: textStyle.fontSize,
195
- fontWeight: textStyle.fontWeight, lineHeight: textStyle.lineHeight,
196
- position: "absolute", left: 0, right: 0, opacity: 0,
197
- paddingVertical: fieldRecipe.paddingVertical,
198
- }], 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] }) }));
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
- function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel, readOnly, readOnlyLabel, disabled, description, error, role, orientation, presentation, grid, layoutStyle, children, }) {
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, grid ? _jsx(Grid, { ...grid, children: children }) : (_jsx(View, { style: {
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", selectionTone = "neutral", textEmphasis = "strong", selectionMotion: suppliedSelectionMotion, disabled = false, layoutStyle, overflow = "wrap", actions, testID, } = props;
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 = rect.x < offset ? Math.max(0, rect.x) : rect.x + rect.width > offset + trackWidth
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,