@hjmds/react-native 0.8.2
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 +310 -0
- package/dist/actions.d.ts +128 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +157 -0
- package/dist/actions.js.map +1 -0
- package/dist/data-display.d.ts +285 -0
- package/dist/data-display.d.ts.map +1 -0
- package/dist/data-display.js +775 -0
- package/dist/data-display.js.map +1 -0
- package/dist/date-picker.d.ts +25 -0
- package/dist/date-picker.d.ts.map +1 -0
- package/dist/date-picker.js +39 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/evidence.d.ts +954 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +98 -0
- package/dist/evidence.js.map +1 -0
- package/dist/feedback.d.ts +148 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +559 -0
- package/dist/feedback.js.map +1 -0
- package/dist/file-picker.d.ts +20 -0
- package/dist/file-picker.d.ts.map +1 -0
- package/dist/file-picker.js +37 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/forms.d.ts +170 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +595 -0
- package/dist/forms.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs-public.d.ts +6 -0
- package/dist/inputs-public.d.ts.map +1 -0
- package/dist/inputs-public.js +6 -0
- package/dist/inputs-public.js.map +1 -0
- package/dist/inputs.d.ts +258 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +681 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal/modal-lifecycle.d.ts +15 -0
- package/dist/internal/modal-lifecycle.d.ts.map +1 -0
- package/dist/internal/modal-lifecycle.js +17 -0
- package/dist/internal/modal-lifecycle.js.map +1 -0
- package/dist/internal/state.d.ts +8 -0
- package/dist/internal/state.d.ts.map +1 -0
- package/dist/internal/state.js +18 -0
- package/dist/internal/state.js.map +1 -0
- package/dist/internal/styles.d.ts +29 -0
- package/dist/internal/styles.d.ts.map +1 -0
- package/dist/internal/styles.js +51 -0
- package/dist/internal/styles.js.map +1 -0
- package/dist/navigation.d.ts +271 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +978 -0
- package/dist/navigation.js.map +1 -0
- package/dist/number-field.d.ts +58 -0
- package/dist/number-field.d.ts.map +1 -0
- package/dist/number-field.js +172 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +2 -0
- package/dist/otp-field.d.ts.map +1 -0
- package/dist/otp-field.js +2 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlays.d.ts +68 -0
- package/dist/overlays.d.ts.map +1 -0
- package/dist/overlays.js +757 -0
- package/dist/overlays.js.map +1 -0
- package/dist/password-field.d.ts +2 -0
- package/dist/password-field.d.ts.map +1 -0
- package/dist/password-field.js +2 -0
- package/dist/password-field.js.map +1 -0
- package/dist/primitives.d.ts +132 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +238 -0
- package/dist/primitives.js.map +1 -0
- package/dist/provider.d.ts +41 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +78 -0
- package/dist/provider.js.map +1 -0
- package/dist/slider.d.ts +45 -0
- package/dist/slider.d.ts.map +1 -0
- package/dist/slider.js +200 -0
- package/dist/slider.js.map +1 -0
- package/dist/steps.d.ts +13 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +13 -0
- package/dist/steps.js.map +1 -0
- package/dist/upload-item.d.ts +13 -0
- package/dist/upload-item.d.ts.map +1 -0
- package/dist/upload-item.js +25 -0
- package/dist/upload-item.js.map +1 -0
- package/package.json +166 -0
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { resolveColorReference } from "@hjmds/design-contracts/color-references";
|
|
4
|
+
import { glyph, radius, spacing, typography } from "@hjmds/design-contracts/foundations";
|
|
5
|
+
import { fieldRecipe, } from "@hjmds/design-contracts/recipes/base";
|
|
6
|
+
import { chipRecipe, searchFieldRecipe, segmentedControlRecipe, selectionControlRecipe, selectionGroupRecipe, switchRecipe, } from "@hjmds/design-contracts/recipes";
|
|
7
|
+
import { passwordFieldRecipe, resolvePasswordFieldDescriptor, } from "@hjmds/design-contracts/components/password-field";
|
|
8
|
+
import { getOtpFieldSlotValues, otpFieldRecipe, resolveOtpFieldValue, } from "@hjmds/design-contracts/components/otp-field";
|
|
9
|
+
import { getCheckboxNextState, reconcileCheckboxSelection, resolveControlAccessibleName, resolveInitialRadioValue, resolveInitialTabValue, reconcileRadioSelection, selectionGroupBehaviorDefaults, toggleCheckboxSelection, validateCheckboxSelection, validateRadioSelection, validateSelectionItems, } from "@hjmds/design-contracts/behaviors";
|
|
10
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState, } from "react";
|
|
11
|
+
import { ActivityIndicator, Pressable, Switch as NativeSwitch, TextInput, View, } from "react-native";
|
|
12
|
+
import { useControllableState } from "./internal/state.js";
|
|
13
|
+
import { logicalTextAlign, minimumTargetStyle, resolveNativeTextScaleProps, } from "./internal/styles.js";
|
|
14
|
+
import { Text } from "./primitives.js";
|
|
15
|
+
import { useHjmNativeTheme } from "./provider.js";
|
|
16
|
+
function resolveFieldAccessibleName(label, accessibilityLabel) {
|
|
17
|
+
const visibleLabel = label?.trim();
|
|
18
|
+
const explicitAccessibleName = accessibilityLabel?.trim();
|
|
19
|
+
const accessibleName = explicitAccessibleName || visibleLabel;
|
|
20
|
+
if (!accessibleName) {
|
|
21
|
+
throw new TypeError("Field requires a non-empty label or accessibilityLabel");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
accessibleName,
|
|
25
|
+
...(visibleLabel ? { visibleLabel } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function FieldMessage({ error, supportText }) {
|
|
29
|
+
if (!error && !supportText)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsx(Text, { accessibilityLiveRegion: error ? "assertive" : "none", tone: error ? "danger" : "muted", variant: fieldRecipe.support.textVariant, children: error ?? supportText }));
|
|
32
|
+
}
|
|
33
|
+
const FieldRenderer = forwardRef(function FieldRenderer({ label, value, defaultValue = "", onValueChange, supportText, error, required = false, disabled = false, busy = false, variant = fieldRecipe.defaults.variant, shape, accessibilityLabel, inputStyle, containerStyle, allowFontScaling, multiline, search, searchSize = searchFieldRecipe.defaults.size, leading, trailing, onBlur, onFocus, ...props }, ref) {
|
|
34
|
+
const theme = useHjmNativeTheme();
|
|
35
|
+
const { colors, environment, textScaling } = theme;
|
|
36
|
+
const [focused, setFocused] = useState(false);
|
|
37
|
+
const [currentValue, setCurrentValue] = useControllableState({
|
|
38
|
+
...(value === undefined ? {} : { value }),
|
|
39
|
+
defaultValue,
|
|
40
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
41
|
+
});
|
|
42
|
+
const hint = error ?? supportText;
|
|
43
|
+
const { accessibleName, visibleLabel } = resolveFieldAccessibleName(label, accessibilityLabel);
|
|
44
|
+
const resolvedShape = shape ?? (search ? searchFieldRecipe.defaults.shape : fieldRecipe.defaults.shape);
|
|
45
|
+
const searchSizing = searchFieldRecipe.sizes[searchSize];
|
|
46
|
+
const minHeight = multiline
|
|
47
|
+
? fieldRecipe.multilineMinHeight
|
|
48
|
+
: search
|
|
49
|
+
? searchSizing.minHeight
|
|
50
|
+
: fieldRecipe.minHeight;
|
|
51
|
+
const borderWidth = search ? searchFieldRecipe.borderWidth : fieldRecipe.borderWidth;
|
|
52
|
+
const borderColor = search
|
|
53
|
+
? resolveColorReference(error
|
|
54
|
+
? searchFieldRecipe.colors.invalid
|
|
55
|
+
: focused
|
|
56
|
+
? searchFieldRecipe.colors.focus
|
|
57
|
+
: searchFieldRecipe.colors.border, theme.palette)
|
|
58
|
+
: colors[error
|
|
59
|
+
? fieldRecipe.states.invalid.border
|
|
60
|
+
: focused
|
|
61
|
+
? fieldRecipe.states.focused.border
|
|
62
|
+
: fieldRecipe.states.idle.border];
|
|
63
|
+
const backgroundColor = search && variant === fieldRecipe.defaults.variant
|
|
64
|
+
? resolveColorReference(searchFieldRecipe.colors.background, theme.palette)
|
|
65
|
+
: colors[fieldRecipe.variants[variant].background];
|
|
66
|
+
const placeholderColor = search
|
|
67
|
+
? resolveColorReference(searchFieldRecipe.colors.placeholder, theme.palette)
|
|
68
|
+
: colors[fieldRecipe.placeholder.color];
|
|
69
|
+
const textStyle = typography[search ? searchSizing.textVariant : fieldRecipe.textVariant];
|
|
70
|
+
const controlRadius = radius[search ? searchFieldRecipe.shapes[resolvedShape] : fieldRecipe.shapes[resolvedShape]];
|
|
71
|
+
const inputTextScaleProps = resolveNativeTextScaleProps(textScaling, [
|
|
72
|
+
{
|
|
73
|
+
color: search
|
|
74
|
+
? resolveColorReference(searchFieldRecipe.colors.content, theme.palette)
|
|
75
|
+
: colors.text,
|
|
76
|
+
flex: 1,
|
|
77
|
+
fontSize: textStyle.fontSize,
|
|
78
|
+
fontWeight: textStyle.fontWeight,
|
|
79
|
+
lineHeight: textStyle.lineHeight,
|
|
80
|
+
minHeight: minHeight - (borderWidth * 2),
|
|
81
|
+
paddingHorizontal: 0,
|
|
82
|
+
paddingVertical: fieldRecipe.paddingVertical,
|
|
83
|
+
textAlign: logicalTextAlign(environment.direction),
|
|
84
|
+
textAlignVertical: multiline ? "top" : "center",
|
|
85
|
+
},
|
|
86
|
+
inputStyle,
|
|
87
|
+
], allowFontScaling);
|
|
88
|
+
return (_jsxs(View, { style: [
|
|
89
|
+
{
|
|
90
|
+
gap: fieldRecipe.label.gap,
|
|
91
|
+
opacity: disabled
|
|
92
|
+
? search
|
|
93
|
+
? searchFieldRecipe.states.disabledOpacity
|
|
94
|
+
: fieldRecipe.disabledOpacity
|
|
95
|
+
: 1,
|
|
96
|
+
},
|
|
97
|
+
containerStyle,
|
|
98
|
+
], children: [visibleLabel ? (_jsxs(Text, { style: {
|
|
99
|
+
color: colors[fieldRecipe.label.color],
|
|
100
|
+
fontWeight: fieldRecipe.label.fontWeight,
|
|
101
|
+
}, tone: "body", variant: fieldRecipe.label.textVariant, children: [visibleLabel, required ? " *" : ""] })) : null, _jsxs(View, { style: { gap: fieldRecipe.support.gap }, children: [_jsxs(View, { style: {
|
|
102
|
+
alignItems: multiline ? "stretch" : "center",
|
|
103
|
+
backgroundColor,
|
|
104
|
+
borderColor,
|
|
105
|
+
borderRadius: controlRadius,
|
|
106
|
+
borderWidth,
|
|
107
|
+
direction: environment.direction,
|
|
108
|
+
flexDirection: "row",
|
|
109
|
+
gap: search ? searchSizing.gap : 0,
|
|
110
|
+
minHeight,
|
|
111
|
+
paddingHorizontal: search
|
|
112
|
+
? searchSizing.paddingHorizontal
|
|
113
|
+
: fieldRecipe.paddingHorizontal,
|
|
114
|
+
}, children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(TextInput, { ...props, ...inputTextScaleProps, ref: ref, accessibilityHint: hint, accessibilityLabel: accessibleName, accessibilityRole: search ? "search" : undefined, accessibilityState: { busy, disabled }, editable: !disabled && !busy, multiline: multiline, onBlur: (event) => {
|
|
115
|
+
setFocused(false);
|
|
116
|
+
onBlur?.(event);
|
|
117
|
+
}, onChangeText: setCurrentValue, onFocus: (event) => {
|
|
118
|
+
setFocused(true);
|
|
119
|
+
onFocus?.(event);
|
|
120
|
+
}, placeholderTextColor: placeholderColor, value: currentValue }), trailing] }), _jsx(FieldMessage, { ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { supportText }) })] })] }));
|
|
121
|
+
});
|
|
122
|
+
export const TextField = forwardRef(function TextField(props, ref) {
|
|
123
|
+
return _jsx(FieldRenderer, { ...props, ref: ref, multiline: false, search: false });
|
|
124
|
+
});
|
|
125
|
+
export const TextArea = forwardRef(function TextArea(props, ref) {
|
|
126
|
+
return _jsx(FieldRenderer, { ...props, ref: ref, multiline: true, search: false });
|
|
127
|
+
});
|
|
128
|
+
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) {
|
|
129
|
+
const theme = useHjmNativeTheme();
|
|
130
|
+
const searchSizing = searchFieldRecipe.sizes[size];
|
|
131
|
+
const iconProps = {
|
|
132
|
+
color: resolveColorReference(searchFieldRecipe.colors.leading, theme.palette),
|
|
133
|
+
size: glyph[searchSizing.glyph],
|
|
134
|
+
disabled,
|
|
135
|
+
};
|
|
136
|
+
const [searchValue, setSearchValue] = useControllableState({
|
|
137
|
+
...(value === undefined ? {} : { value }),
|
|
138
|
+
defaultValue: defaultValue ?? "",
|
|
139
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
140
|
+
});
|
|
141
|
+
const leading = leadingNode ?? renderLeading?.(iconProps);
|
|
142
|
+
const trailing = busy ? (_jsx(View, { accessibilityLabel: busyLabel, accessibilityRole: "progressbar", accessibilityState: { busy: true }, style: {
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
height: searchSizing.clearDiameter,
|
|
145
|
+
justifyContent: "center",
|
|
146
|
+
width: searchSizing.clearDiameter,
|
|
147
|
+
}, 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: () => {
|
|
148
|
+
setSearchValue("");
|
|
149
|
+
onClear?.();
|
|
150
|
+
}, style: {
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
height: searchSizing.clearDiameter,
|
|
153
|
+
justifyContent: "center",
|
|
154
|
+
width: searchSizing.clearDiameter,
|
|
155
|
+
}, children: renderClearIcon?.(iconProps) ?? (_jsx(Text, { align: "center", style: { fontSize: iconProps.size, lineHeight: iconProps.size }, tone: "muted", children: "\u00D7" })) })) : trailingNode;
|
|
156
|
+
return (_jsx(FieldRenderer, { ...props, ref: ref, busy: busy, disabled: disabled, leading: leading, multiline: false, onValueChange: (next) => {
|
|
157
|
+
if (!busy && !disabled)
|
|
158
|
+
setSearchValue(next);
|
|
159
|
+
}, search: true, searchSize: size, trailing: trailing, value: searchValue }));
|
|
160
|
+
});
|
|
161
|
+
function DefaultPasswordToggleIcon({ color, revealed, size, }) {
|
|
162
|
+
const eyeWidth = size * 1.08;
|
|
163
|
+
const eyeHeight = size * 0.68;
|
|
164
|
+
const strokeWidth = Math.max(1.5, size * 0.1);
|
|
165
|
+
return (_jsxs(View, { accessible: false, style: {
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
height: size,
|
|
168
|
+
justifyContent: "center",
|
|
169
|
+
width: eyeWidth,
|
|
170
|
+
}, children: [_jsx(View, { style: {
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
borderColor: color,
|
|
173
|
+
borderRadius: eyeHeight / 2,
|
|
174
|
+
borderWidth: strokeWidth,
|
|
175
|
+
height: eyeHeight,
|
|
176
|
+
justifyContent: "center",
|
|
177
|
+
width: eyeWidth,
|
|
178
|
+
}, children: _jsx(View, { style: {
|
|
179
|
+
backgroundColor: color,
|
|
180
|
+
borderRadius: size * 0.15,
|
|
181
|
+
height: size * 0.3,
|
|
182
|
+
width: size * 0.3,
|
|
183
|
+
} }) }), revealed ? (_jsx(View, { style: {
|
|
184
|
+
backgroundColor: color,
|
|
185
|
+
borderRadius: strokeWidth,
|
|
186
|
+
height: strokeWidth,
|
|
187
|
+
position: "absolute",
|
|
188
|
+
transform: [{ rotate: "-42deg" }],
|
|
189
|
+
width: eyeWidth * 1.18,
|
|
190
|
+
} })) : null] }));
|
|
191
|
+
}
|
|
192
|
+
/** Password input with independent reveal state and native autofill translation. */
|
|
193
|
+
export const PasswordField = forwardRef(function PasswordField({ revealed: revealedProp, defaultRevealed = false, onRevealedChange, autofillHint, revealLabel, concealLabel, size = passwordFieldRecipe.defaults.size, renderToggleIcon, disabled = false, onSelectionChange, ...props }, forwardedRef) {
|
|
194
|
+
const theme = useHjmNativeTheme();
|
|
195
|
+
const inputRef = useRef(null);
|
|
196
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
197
|
+
const selectionRef = useRef({ start: 0, end: 0 });
|
|
198
|
+
const [revealed, setRevealed] = useControllableState({
|
|
199
|
+
...(revealedProp === undefined ? {} : { value: revealedProp }),
|
|
200
|
+
defaultValue: defaultRevealed,
|
|
201
|
+
...(onRevealedChange === undefined ? {} : { onChange: onRevealedChange }),
|
|
202
|
+
});
|
|
203
|
+
const resolved = resolvePasswordFieldDescriptor({ revealed, autofillHint }, {
|
|
204
|
+
composeToggleAccessibleName: ({ willReveal }) => willReveal ? revealLabel : concealLabel,
|
|
205
|
+
});
|
|
206
|
+
const metrics = passwordFieldRecipe.sizes[size];
|
|
207
|
+
const toggleColor = resolveColorReference(passwordFieldRecipe.toggle.color, theme.palette);
|
|
208
|
+
const appearance = {
|
|
209
|
+
name: revealed
|
|
210
|
+
? passwordFieldRecipe.toggle.icons.revealed
|
|
211
|
+
: passwordFieldRecipe.toggle.icons.concealed,
|
|
212
|
+
color: toggleColor,
|
|
213
|
+
size: glyph.sm,
|
|
214
|
+
revealed,
|
|
215
|
+
disabled,
|
|
216
|
+
};
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const timeout = setTimeout(() => {
|
|
219
|
+
inputRef.current?.setNativeProps({ selection: selectionRef.current });
|
|
220
|
+
}, 0);
|
|
221
|
+
return () => clearTimeout(timeout);
|
|
222
|
+
}, [revealed]);
|
|
223
|
+
return (_jsx(FieldRenderer, { ...props, ref: inputRef, autoComplete: autofillHint === "current" ? "current-password" : "new-password", disabled: disabled, inputStyle: [
|
|
224
|
+
size === "large"
|
|
225
|
+
? {
|
|
226
|
+
fontSize: typography.bodyLarge.fontSize,
|
|
227
|
+
lineHeight: typography.bodyLarge.lineHeight,
|
|
228
|
+
minHeight: metrics.minHeight - (passwordFieldRecipe.frame.borderWidth * 2),
|
|
229
|
+
}
|
|
230
|
+
: undefined,
|
|
231
|
+
props.inputStyle,
|
|
232
|
+
], multiline: false, onSelectionChange: (event) => {
|
|
233
|
+
selectionRef.current = event.nativeEvent.selection;
|
|
234
|
+
onSelectionChange?.(event);
|
|
235
|
+
}, search: false, secureTextEntry: resolved.nativeSecureTextEntry, textContentType: autofillHint === "current" ? "password" : "newPassword", trailing: (_jsx(Pressable, { accessibilityLabel: resolved.toggleAccessibleName, accessibilityRole: "button", accessibilityState: { disabled, selected: revealed }, disabled: disabled, onPress: () => setRevealed(!revealed), style: ({ pressed }) => ({
|
|
236
|
+
alignItems: "center",
|
|
237
|
+
height: metrics.toggleDiameter,
|
|
238
|
+
justifyContent: "center",
|
|
239
|
+
opacity: pressed ? 0.72 : 1,
|
|
240
|
+
width: metrics.toggleDiameter,
|
|
241
|
+
}), children: renderToggleIcon?.(appearance) ?? _jsx(DefaultPasswordToggleIcon, { ...appearance }) })) }));
|
|
242
|
+
});
|
|
243
|
+
/** One accessible numeric TextInput rendered through decorative OTP slots. */
|
|
244
|
+
export const OtpField = forwardRef(function OtpField({ label, accessibilityLabel, supportText, error, required = false, disabled = false, busy = false, readOnly = false, length, value: valueProp, defaultValue = "", onValueChange, onComplete, size = otpFieldRecipe.defaults.size, slotStyle, slotTextStyle, containerStyle, allowFontScaling, onBlur, onFocus, ...props }, ref) {
|
|
245
|
+
const theme = useHjmNativeTheme();
|
|
246
|
+
const { accessibleName, visibleLabel } = resolveFieldAccessibleName(label, accessibilityLabel);
|
|
247
|
+
const [focused, setFocused] = useState(false);
|
|
248
|
+
const [value, setValue] = useControllableState({
|
|
249
|
+
...(valueProp === undefined ? {} : { value: valueProp }),
|
|
250
|
+
defaultValue: resolveOtpFieldValue(length, defaultValue),
|
|
251
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
252
|
+
});
|
|
253
|
+
const slots = getOtpFieldSlotValues({ length, value });
|
|
254
|
+
const complete = value.length === length;
|
|
255
|
+
const wasCompleteRef = useRef(complete);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (complete && !wasCompleteRef.current)
|
|
258
|
+
onComplete?.(value);
|
|
259
|
+
wasCompleteRef.current = complete;
|
|
260
|
+
}, [complete, onComplete, value]);
|
|
261
|
+
const metrics = otpFieldRecipe.sizes[size];
|
|
262
|
+
const activeIndex = Math.min(value.length, length - 1);
|
|
263
|
+
const slotHeight = Math.max(metrics.slotSize, typography[metrics.textVariant].lineHeight * theme.environment.textScale + spacing.xs * 2);
|
|
264
|
+
const baseBorder = resolveColorReference(otpFieldRecipe.slot.border, theme.palette);
|
|
265
|
+
const focusBorder = resolveColorReference(otpFieldRecipe.slot.focusBorder, theme.palette);
|
|
266
|
+
const invalidBorder = resolveColorReference(otpFieldRecipe.slot.invalidBorder, theme.palette);
|
|
267
|
+
const filledBorder = resolveColorReference(otpFieldRecipe.slot.filledBorder, theme.palette);
|
|
268
|
+
const contentColor = resolveColorReference(otpFieldRecipe.slot.content, theme.palette);
|
|
269
|
+
return (_jsxs(View, { style: [
|
|
270
|
+
{
|
|
271
|
+
gap: fieldRecipe.label.gap,
|
|
272
|
+
opacity: disabled || busy ? otpFieldRecipe.states.disabledOpacity : 1,
|
|
273
|
+
},
|
|
274
|
+
containerStyle,
|
|
275
|
+
], children: [visibleLabel ? (_jsxs(Text, { style: {
|
|
276
|
+
color: theme.colors[fieldRecipe.label.color],
|
|
277
|
+
fontWeight: fieldRecipe.label.fontWeight,
|
|
278
|
+
}, tone: "body", variant: fieldRecipe.label.textVariant, children: [visibleLabel, required ? " *" : ""] })) : null, _jsxs(View, { style: { gap: otpFieldRecipe.support.gap }, children: [_jsxs(View, { style: {
|
|
279
|
+
direction: "ltr",
|
|
280
|
+
flexDirection: "row",
|
|
281
|
+
gap: metrics.gap,
|
|
282
|
+
maxWidth: metrics.slotSize * length + metrics.gap * (length - 1),
|
|
283
|
+
position: "relative",
|
|
284
|
+
width: "100%",
|
|
285
|
+
}, 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) => {
|
|
286
|
+
setFocused(false);
|
|
287
|
+
onBlur?.(event);
|
|
288
|
+
}, onChangeText: (rawText) => setValue(resolveOtpFieldValue(length, rawText)), onFocus: (event) => {
|
|
289
|
+
setFocused(true);
|
|
290
|
+
onFocus?.(event);
|
|
291
|
+
}, selectionColor: "transparent", style: {
|
|
292
|
+
bottom: 0,
|
|
293
|
+
color: "transparent",
|
|
294
|
+
left: 0,
|
|
295
|
+
opacity: 0.01,
|
|
296
|
+
padding: 0,
|
|
297
|
+
position: "absolute",
|
|
298
|
+
right: 0,
|
|
299
|
+
top: 0,
|
|
300
|
+
zIndex: 1,
|
|
301
|
+
}, textContentType: "oneTimeCode", value: value }), slots.map((digit, index) => {
|
|
302
|
+
const borderColor = error
|
|
303
|
+
? invalidBorder
|
|
304
|
+
: focused && index === activeIndex
|
|
305
|
+
? focusBorder
|
|
306
|
+
: digit
|
|
307
|
+
? filledBorder
|
|
308
|
+
: baseBorder;
|
|
309
|
+
return (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: [
|
|
310
|
+
{
|
|
311
|
+
alignItems: "center",
|
|
312
|
+
backgroundColor: theme.colors.surface,
|
|
313
|
+
borderColor,
|
|
314
|
+
borderRadius: radius[otpFieldRecipe.slot.radius],
|
|
315
|
+
borderWidth: otpFieldRecipe.slot.borderWidth,
|
|
316
|
+
flex: 1,
|
|
317
|
+
height: slotHeight,
|
|
318
|
+
justifyContent: "center",
|
|
319
|
+
maxWidth: metrics.slotSize,
|
|
320
|
+
minWidth: 0,
|
|
321
|
+
},
|
|
322
|
+
slotStyle,
|
|
323
|
+
], children: _jsx(Text, { accessible: false, align: "center", allowFontScaling: allowFontScaling, style: [{ color: contentColor }, slotTextStyle], variant: metrics.textVariant, children: digit }) }, index));
|
|
324
|
+
})] }), _jsx(FieldMessage, { ...(error === undefined ? {} : { error }), ...(supportText === undefined ? {} : { supportText }) })] })] }));
|
|
325
|
+
});
|
|
326
|
+
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, style, controlStyle, indicatorStyle, leadingStyle, contentStyle, labelStyle, descriptionStyle, }) {
|
|
327
|
+
const theme = useHjmNativeTheme();
|
|
328
|
+
const metrics = selectionControlRecipe.sizes[size];
|
|
329
|
+
const plate = selectionControlRecipe.presentations[presentation];
|
|
330
|
+
const selected = checked === true || checked === "mixed";
|
|
331
|
+
const indicatorColor = resolveColorReference(selectionControlRecipe.states.indicator, theme.palette);
|
|
332
|
+
const appearance = {
|
|
333
|
+
checked,
|
|
334
|
+
selected,
|
|
335
|
+
disabled,
|
|
336
|
+
readOnly,
|
|
337
|
+
color: indicatorColor,
|
|
338
|
+
size: metrics.control,
|
|
339
|
+
};
|
|
340
|
+
const resolvedLeading = leading ?? renderLeading?.(appearance);
|
|
341
|
+
const plateBackground = selected
|
|
342
|
+
? selectionControlRecipe.states.selectedBackground
|
|
343
|
+
: plate.background ?? selectionControlRecipe.states.idleBackground;
|
|
344
|
+
const plateBorder = invalid
|
|
345
|
+
? selectionControlRecipe.states.invalidBorder
|
|
346
|
+
: selected
|
|
347
|
+
? selectionControlRecipe.states.selectedBorder
|
|
348
|
+
: plate.border;
|
|
349
|
+
const controlBorder = invalid
|
|
350
|
+
? selectionControlRecipe.states.invalidBorder
|
|
351
|
+
: selected
|
|
352
|
+
? selectionControlRecipe.states.checkedBorder
|
|
353
|
+
: selectionControlRecipe.states.idleBorder;
|
|
354
|
+
const resolvedHint = [
|
|
355
|
+
accessibilityHint ?? description,
|
|
356
|
+
required ? requiredLabel : undefined,
|
|
357
|
+
readOnly ? readOnlyLabel : undefined,
|
|
358
|
+
invalid ? invalidLabel : undefined,
|
|
359
|
+
].filter(Boolean).join(". ") || undefined;
|
|
360
|
+
const defaultIndicator = kind === "radio" ? (checked === true ? (_jsx(View, { style: {
|
|
361
|
+
backgroundColor: indicatorColor,
|
|
362
|
+
borderRadius: radius.full,
|
|
363
|
+
height: metrics.control * selectionControlRecipe.radioDotRatio,
|
|
364
|
+
width: metrics.control * selectionControlRecipe.radioDotRatio,
|
|
365
|
+
} })) : null) : checked === "mixed" ? (_jsx(Text, { accessible: false, align: "center", style: { color: indicatorColor }, variant: "label", children: "\u2212" })) : checked ? (_jsx(Text, { accessible: false, align: "center", style: { color: indicatorColor }, variant: "label", children: "\u2713" })) : null;
|
|
366
|
+
return (_jsxs(Pressable, { accessibilityHint: resolvedHint, accessibilityLabel: label, accessibilityRole: kind, accessibilityState: { checked, disabled: disabled || readOnly }, disabled: disabled || readOnly, hitSlop: plate.useSizePadding ? 0 : metrics.hitSlop, onPress: () => {
|
|
367
|
+
if (!readOnly)
|
|
368
|
+
onActivate();
|
|
369
|
+
}, style: ({ pressed }) => [
|
|
370
|
+
{
|
|
371
|
+
alignItems: "center",
|
|
372
|
+
alignSelf: "stretch",
|
|
373
|
+
backgroundColor: resolveColorReference(plateBackground, theme.palette),
|
|
374
|
+
borderColor: plateBorder
|
|
375
|
+
? resolveColorReference(plateBorder, theme.palette)
|
|
376
|
+
: "transparent",
|
|
377
|
+
borderRadius: radius[plate.radius],
|
|
378
|
+
borderWidth: plate.borderWidth,
|
|
379
|
+
direction: theme.environment.direction,
|
|
380
|
+
flexDirection: "row",
|
|
381
|
+
gap: metrics.gap,
|
|
382
|
+
minHeight: metrics.rowMinHeight,
|
|
383
|
+
opacity: disabled
|
|
384
|
+
? selectionControlRecipe.states.disabledOpacity
|
|
385
|
+
: pressed && !readOnly
|
|
386
|
+
? 0.86
|
|
387
|
+
: 1,
|
|
388
|
+
paddingHorizontal: plate.useSizePadding ? metrics.paddingHorizontal : 0,
|
|
389
|
+
paddingVertical: plate.useSizePadding ? metrics.paddingVertical : 0,
|
|
390
|
+
},
|
|
391
|
+
style,
|
|
392
|
+
], children: [indicator === "default" ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: [
|
|
393
|
+
{
|
|
394
|
+
alignItems: "center",
|
|
395
|
+
backgroundColor: resolveColorReference(selected
|
|
396
|
+
? selectionControlRecipe.states.checkedBackground
|
|
397
|
+
: selectionControlRecipe.states.idleBackground, theme.palette),
|
|
398
|
+
borderColor: resolveColorReference(controlBorder, theme.palette),
|
|
399
|
+
borderRadius: radius[selectionControlRecipe.shapes[kind]],
|
|
400
|
+
borderWidth: 1,
|
|
401
|
+
height: metrics.control,
|
|
402
|
+
justifyContent: "center",
|
|
403
|
+
width: metrics.control,
|
|
404
|
+
},
|
|
405
|
+
controlStyle,
|
|
406
|
+
], children: _jsx(View, { style: indicatorStyle, children: renderIndicator?.(appearance) ?? defaultIndicator }) })) : null, resolvedLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: leadingStyle, children: resolvedLeading })) : null, _jsxs(View, { style: [{ flex: 1, gap: spacing.xxs, minWidth: 0 }, contentStyle], children: [_jsx(Text, { style: [
|
|
407
|
+
{
|
|
408
|
+
color: resolveColorReference(selectionControlRecipe.label.color, theme.palette),
|
|
409
|
+
fontWeight: selected
|
|
410
|
+
? selectionControlRecipe.label.checkedFontWeight
|
|
411
|
+
: selectionControlRecipe.label.fontWeight,
|
|
412
|
+
},
|
|
413
|
+
labelStyle,
|
|
414
|
+
], variant: metrics.labelVariant, children: label }), description ? (_jsx(Text, { style: [
|
|
415
|
+
{
|
|
416
|
+
color: resolveColorReference(selectionControlRecipe.description.color, theme.palette),
|
|
417
|
+
},
|
|
418
|
+
descriptionStyle,
|
|
419
|
+
], variant: metrics.descriptionVariant, children: description })) : null] })] }));
|
|
420
|
+
}
|
|
421
|
+
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 }) {
|
|
422
|
+
const [selected, setSelected] = useControllableState({
|
|
423
|
+
...(checked === undefined ? {} : { value: checked }),
|
|
424
|
+
defaultValue: defaultChecked,
|
|
425
|
+
...(onCheckedChange === undefined
|
|
426
|
+
? {}
|
|
427
|
+
: { onChange: (next) => onCheckedChange(next === true) }),
|
|
428
|
+
});
|
|
429
|
+
return (_jsx(ChoiceRow, { ...visual, accessibilityHint: accessibilityHint, checked: selected, description: description, disabled: disabled, indicator: visual.indicator ?? "default", invalid: invalid, invalidLabel: invalidLabel, kind: "checkbox", label: label, leading: leading, onActivate: () => setSelected(getCheckboxNextState(selected)), readOnly: readOnly, readOnlyLabel: readOnlyLabel, renderIndicator: renderIndicator, renderLeading: renderLeading, required: required, requiredLabel: requiredLabel }));
|
|
430
|
+
}
|
|
431
|
+
/** Standalone native radio item. Prefer RadioGroup when group state is owned here. */
|
|
432
|
+
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 }) {
|
|
433
|
+
const [selected, setSelected] = useControllableState({
|
|
434
|
+
...(checked === undefined ? {} : { value: checked }),
|
|
435
|
+
defaultValue: defaultChecked,
|
|
436
|
+
...(onCheckedChange === undefined
|
|
437
|
+
? {}
|
|
438
|
+
: { onChange: (next) => next && onCheckedChange(true) }),
|
|
439
|
+
});
|
|
440
|
+
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 }));
|
|
441
|
+
}
|
|
442
|
+
function ChoiceGroupFrame({ label, accessibilityLabel, required, requiredLabel, readOnly, readOnlyLabel, disabled, description, error, role, orientation, presentation, style, children, }) {
|
|
443
|
+
const theme = useHjmNativeTheme();
|
|
444
|
+
const id = useId().replaceAll(":", "");
|
|
445
|
+
const labelId = `${id}-label`;
|
|
446
|
+
const accessibleName = resolveControlAccessibleName(label, accessibilityLabel, "Choice group");
|
|
447
|
+
const announcedName = [
|
|
448
|
+
accessibleName,
|
|
449
|
+
required ? requiredLabel ?? "*" : undefined,
|
|
450
|
+
readOnly ? readOnlyLabel : undefined,
|
|
451
|
+
].filter(Boolean).join(", ");
|
|
452
|
+
const gap = selectionGroupRecipe.orientations[orientation].gap[presentation];
|
|
453
|
+
return (_jsxs(View, { accessibilityHint: [error ?? description, readOnly ? readOnlyLabel : undefined]
|
|
454
|
+
.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: {
|
|
455
|
+
direction: theme.environment.direction,
|
|
456
|
+
flexDirection: orientation === "horizontal" && theme.environment.textScale < 1.6
|
|
457
|
+
? "row"
|
|
458
|
+
: "column",
|
|
459
|
+
gap,
|
|
460
|
+
}, children: children }), error ? (_jsx(Text, { accessibilityLiveRegion: "assertive", accessibilityRole: "alert", tone: "danger", variant: selectionGroupRecipe.error.textVariant, children: error })) : description ? (_jsx(Text, { tone: "muted", variant: selectionGroupRecipe.description.textVariant, children: description })) : null] }));
|
|
461
|
+
}
|
|
462
|
+
export function RadioGroup({ label, accessibilityLabel, options, 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 }) {
|
|
463
|
+
const selectionItems = options.map((option) => ({
|
|
464
|
+
id: option.value,
|
|
465
|
+
label: option.label,
|
|
466
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
467
|
+
...(option.disabled === undefined ? {} : { disabled: option.disabled }),
|
|
468
|
+
}));
|
|
469
|
+
validateSelectionItems(selectionItems);
|
|
470
|
+
if (value !== undefined)
|
|
471
|
+
validateRadioSelection(selectionItems, value);
|
|
472
|
+
const initialRef = useRef(null);
|
|
473
|
+
initialRef.current ??= {
|
|
474
|
+
value: resolveInitialRadioValue(selectionItems, value ?? defaultValue, required),
|
|
475
|
+
};
|
|
476
|
+
const [storedValue, setSelected] = useControllableState({
|
|
477
|
+
...(value === undefined ? {} : { value }),
|
|
478
|
+
defaultValue: initialRef.current.value,
|
|
479
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
480
|
+
});
|
|
481
|
+
const selected = reconcileRadioSelection(selectionItems, storedValue, required);
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (value === undefined && selected !== storedValue)
|
|
484
|
+
setSelected(selected);
|
|
485
|
+
}, [selected, setSelected, storedValue, value]);
|
|
486
|
+
const hasError = invalid || error !== undefined;
|
|
487
|
+
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: options.map((option) => {
|
|
488
|
+
const optionDisabled = disabled || option.disabled === true;
|
|
489
|
+
const optionSelected = selected === option.value;
|
|
490
|
+
return (_createElement(ChoiceRow, { ...slotStyles, key: option.value, accessibilityHint: option.accessibilityHint, checked: optionSelected, description: option.description, disabled: optionDisabled, indicator: indicator, invalid: hasError, invalidLabel: invalidLabel ?? error, kind: "radio", label: option.label, leading: option.leading, onActivate: () => setSelected(option.value), presentation: presentation, readOnly: readOnly, readOnlyLabel: readOnlyLabel, renderIndicator: renderIndicator ? (props) => renderIndicator(option, props) : undefined, renderLeading: renderLeading ? (props) => renderLeading(option, props) : undefined, required: required, requiredLabel: requiredLabel, size: size }));
|
|
491
|
+
}) }));
|
|
492
|
+
}
|
|
493
|
+
/** Validated controlled/uncontrolled checkbox collection using immutable Sets. */
|
|
494
|
+
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 }) {
|
|
495
|
+
validateSelectionItems(items);
|
|
496
|
+
if (value !== undefined)
|
|
497
|
+
validateCheckboxSelection(items, value);
|
|
498
|
+
const [storedValue, setSelected] = useControllableState({
|
|
499
|
+
...(value === undefined ? {} : { value }),
|
|
500
|
+
defaultValue,
|
|
501
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
502
|
+
});
|
|
503
|
+
const selected = reconcileCheckboxSelection(items, storedValue);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
if (value === undefined && selected !== storedValue)
|
|
506
|
+
setSelected(selected);
|
|
507
|
+
}, [selected, setSelected, storedValue, value]);
|
|
508
|
+
const hasError = invalid || error !== undefined;
|
|
509
|
+
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) => {
|
|
510
|
+
const optionDisabled = disabled || item.disabled === true;
|
|
511
|
+
const optionSelected = selected.has(item.id);
|
|
512
|
+
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 }));
|
|
513
|
+
}) }));
|
|
514
|
+
}
|
|
515
|
+
export function Switch({ label, description, size = switchRecipe.defaults.size, value, defaultValue = false, onValueChange, disabled = false, accessibilityLabel, accessibilityHint, style, ...props }) {
|
|
516
|
+
const { colors, environment } = useHjmNativeTheme();
|
|
517
|
+
const dimensions = switchRecipe.sizes[size];
|
|
518
|
+
const [enabled, setEnabled] = useControllableState({
|
|
519
|
+
...(value === undefined ? {} : { value }),
|
|
520
|
+
defaultValue,
|
|
521
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
522
|
+
});
|
|
523
|
+
return (_jsxs(Pressable, { accessibilityHint: accessibilityHint ?? description, accessibilityLabel: accessibilityLabel ?? label, accessibilityRole: "switch", accessibilityState: { checked: enabled, disabled }, disabled: disabled, onPress: () => setEnabled(!enabled), style: ({ pressed }) => [
|
|
524
|
+
minimumTargetStyle,
|
|
525
|
+
{
|
|
526
|
+
alignItems: "center",
|
|
527
|
+
direction: environment.direction,
|
|
528
|
+
flexDirection: "row",
|
|
529
|
+
gap: spacing.sm,
|
|
530
|
+
minHeight: description
|
|
531
|
+
? switchRecipe.rowTwoLineMinHeight
|
|
532
|
+
: switchRecipe.rowMinHeight,
|
|
533
|
+
opacity: disabled ? 0.5 : pressed ? 0.86 : 1,
|
|
534
|
+
},
|
|
535
|
+
style,
|
|
536
|
+
], children: [_jsxs(View, { style: { flex: 1, gap: spacing.xxs }, children: [_jsx(Text, { tone: "body", variant: "bodyLarge", children: label }), description ? (_jsx(Text, { tone: "muted", variant: "caption", children: description })) : null] }), _jsx(NativeSwitch, { ...props, accessible: false, disabled: disabled, ios_backgroundColor: colors.surfaceAlt, pointerEvents: "none", style: { height: dimensions.height, width: dimensions.width }, thumbColor: colors.bg, trackColor: { false: colors.surfaceAlt, true: colors.primary }, value: enabled })] }));
|
|
537
|
+
}
|
|
538
|
+
export function SegmentedControl({ label, options, value, defaultValue, onValueChange, size = segmentedControlRecipe.defaults.size, disabled = false, style, }) {
|
|
539
|
+
const theme = useHjmNativeTheme();
|
|
540
|
+
const { environment } = theme;
|
|
541
|
+
const sizeContract = segmentedControlRecipe.sizes[size];
|
|
542
|
+
const stacked = segmentedControlRecipe.adaptive.largeTextLayout === "stacked"
|
|
543
|
+
&& environment.textScale >= segmentedControlRecipe.adaptive.stackAtFontScale;
|
|
544
|
+
const descriptors = options.map((option) => ({
|
|
545
|
+
id: option.value,
|
|
546
|
+
label: option.label,
|
|
547
|
+
...(option.disabled === undefined ? {} : { disabled: option.disabled }),
|
|
548
|
+
}));
|
|
549
|
+
const collectionFallback = resolveInitialTabValue(descriptors);
|
|
550
|
+
if (collectionFallback === undefined)
|
|
551
|
+
throw new Error("SegmentedControl requires an enabled option");
|
|
552
|
+
if (value !== undefined)
|
|
553
|
+
resolveInitialTabValue(descriptors, value);
|
|
554
|
+
const initialRef = useRef(null);
|
|
555
|
+
if (initialRef.current === null) {
|
|
556
|
+
const initial = resolveInitialTabValue(descriptors, value ?? defaultValue);
|
|
557
|
+
if (initial === undefined)
|
|
558
|
+
throw new Error("SegmentedControl requires an enabled option");
|
|
559
|
+
initialRef.current = { value: initial };
|
|
560
|
+
}
|
|
561
|
+
const [storedValue, setSelected] = useControllableState({
|
|
562
|
+
...(value === undefined ? {} : { value }),
|
|
563
|
+
defaultValue: initialRef.current.value,
|
|
564
|
+
...(onValueChange === undefined ? {} : { onChange: onValueChange }),
|
|
565
|
+
});
|
|
566
|
+
const storedValueValid = descriptors.some((item) => item.id === storedValue && !item.disabled);
|
|
567
|
+
const selected = storedValueValid ? storedValue : collectionFallback;
|
|
568
|
+
const controlled = value !== undefined;
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
if (!controlled && !storedValueValid)
|
|
571
|
+
setSelected(collectionFallback);
|
|
572
|
+
}, [collectionFallback, controlled, setSelected, storedValueValid]);
|
|
573
|
+
return (_jsx(View, { accessibilityLabel: label, accessibilityRole: "radiogroup", style: [
|
|
574
|
+
{
|
|
575
|
+
backgroundColor: resolveColorReference(segmentedControlRecipe.container.background, theme.palette),
|
|
576
|
+
borderColor: resolveColorReference(segmentedControlRecipe.container.border, theme.palette),
|
|
577
|
+
borderRadius: radius[segmentedControlRecipe.container.radius],
|
|
578
|
+
borderWidth: segmentedControlRecipe.container.borderWidth,
|
|
579
|
+
direction: environment.direction,
|
|
580
|
+
flexDirection: stacked ? "column" : "row",
|
|
581
|
+
gap: segmentedControlRecipe.container.gap,
|
|
582
|
+
padding: segmentedControlRecipe.container.padding,
|
|
583
|
+
},
|
|
584
|
+
style,
|
|
585
|
+
], children: options.map((option) => {
|
|
586
|
+
const isSelected = option.value === selected;
|
|
587
|
+
const optionDisabled = disabled || option.disabled === true;
|
|
588
|
+
const contentColor = resolveColorReference(isSelected
|
|
589
|
+
? segmentedControlRecipe.item.selectedContent
|
|
590
|
+
: segmentedControlRecipe.item.idleContent, theme.palette);
|
|
591
|
+
const leading = option.leading ?? option.renderLeading?.({
|
|
592
|
+
selected: isSelected,
|
|
593
|
+
disabled: optionDisabled,
|
|
594
|
+
color: contentColor,
|
|
595
|
+
size: glyph.sm,
|
|
596
|
+
});
|
|
597
|
+
return (_jsxs(Pressable, { accessibilityLabel: option.label, accessibilityRole: "radio", accessibilityState: { checked: isSelected, disabled: optionDisabled }, disabled: optionDisabled, hitSlop: sizeContract.hitSlop, onPress: () => setSelected(option.value), style: ({ pressed }) => [
|
|
598
|
+
{
|
|
599
|
+
alignItems: "center",
|
|
600
|
+
backgroundColor: isSelected
|
|
601
|
+
? resolveColorReference(segmentedControlRecipe.item.selectedBackground, theme.palette)
|
|
602
|
+
: "transparent",
|
|
603
|
+
borderColor: isSelected
|
|
604
|
+
? resolveColorReference(segmentedControlRecipe.item.selectedBorder, theme.palette)
|
|
605
|
+
: "transparent",
|
|
606
|
+
borderRadius: radius[segmentedControlRecipe.item.radius],
|
|
607
|
+
borderWidth: isSelected
|
|
608
|
+
? segmentedControlRecipe.item.selectedBorderWidth
|
|
609
|
+
: 0,
|
|
610
|
+
flex: stacked ? undefined : 1,
|
|
611
|
+
gap: segmentedControlRecipe.item.gap,
|
|
612
|
+
justifyContent: "center",
|
|
613
|
+
minHeight: sizeContract.minHeight,
|
|
614
|
+
opacity: optionDisabled
|
|
615
|
+
? segmentedControlRecipe.item.disabledOpacity
|
|
616
|
+
: pressed
|
|
617
|
+
? segmentedControlRecipe.item.pressedOpacity
|
|
618
|
+
: 1,
|
|
619
|
+
paddingHorizontal: sizeContract.paddingHorizontal,
|
|
620
|
+
width: stacked ? "100%" : undefined,
|
|
621
|
+
},
|
|
622
|
+
], children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsx(Text, { align: "center", style: {
|
|
623
|
+
color: contentColor,
|
|
624
|
+
fontWeight: isSelected
|
|
625
|
+
? segmentedControlRecipe.item.selectedFontWeight
|
|
626
|
+
: segmentedControlRecipe.item.fontWeight,
|
|
627
|
+
}, tone: isSelected ? "brand" : "muted", variant: sizeContract.textVariant, children: option.label })] }, option.value));
|
|
628
|
+
}) }));
|
|
629
|
+
}
|
|
630
|
+
/** Action/filter chip with role-specific, controlled selection semantics. */
|
|
631
|
+
export function Chip({ label, size = chipRecipe.defaults.size, disabled = false, leading, trailing, accessibilityLabel, accessibilityHint, style, leadingStyle, indicatorStyle, labelStyle, trailingStyle, renderSelectionIndicator, selectionMode = "action", selected, onPress, }) {
|
|
632
|
+
const theme = useHjmNativeTheme();
|
|
633
|
+
const selectable = selectionMode !== "action";
|
|
634
|
+
const active = selectable && selected === true;
|
|
635
|
+
const metrics = chipRecipe.sizes[size];
|
|
636
|
+
const presentation = chipRecipe.states[active ? "selected" : "idle"];
|
|
637
|
+
const contentColor = resolveColorReference(presentation.content, theme.palette);
|
|
638
|
+
const indicatorColor = resolveColorReference(chipRecipe.selectionIndicator.color, theme.palette);
|
|
639
|
+
const role = selectionMode === "single" ? "radio" : selectionMode === "multiple" ? "checkbox" : "button";
|
|
640
|
+
return (_jsxs(Pressable, { accessibilityHint: accessibilityHint, accessibilityLabel: accessibilityLabel ?? label, accessibilityRole: role, accessibilityState: selectable ? { checked: active, disabled } : { disabled }, disabled: disabled, hitSlop: metrics.hitSlop, onPress: (event) => {
|
|
641
|
+
if (selectionMode === "action") {
|
|
642
|
+
onPress(event);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
onPress(!active, event);
|
|
646
|
+
}
|
|
647
|
+
}, style: ({ pressed }) => [
|
|
648
|
+
{
|
|
649
|
+
alignItems: "center",
|
|
650
|
+
alignSelf: "flex-start",
|
|
651
|
+
backgroundColor: resolveColorReference(presentation.background, theme.palette),
|
|
652
|
+
borderColor: resolveColorReference(presentation.border, theme.palette),
|
|
653
|
+
borderRadius: radius[chipRecipe.radius],
|
|
654
|
+
borderWidth: chipRecipe.borderWidth,
|
|
655
|
+
direction: theme.environment.direction,
|
|
656
|
+
flexDirection: "row",
|
|
657
|
+
gap: metrics.gap,
|
|
658
|
+
height: metrics.height,
|
|
659
|
+
opacity: disabled
|
|
660
|
+
? chipRecipe.states.disabledOpacity
|
|
661
|
+
: pressed
|
|
662
|
+
? chipRecipe.states.pressedOpacity
|
|
663
|
+
: 1,
|
|
664
|
+
paddingHorizontal: metrics.paddingHorizontal,
|
|
665
|
+
},
|
|
666
|
+
style,
|
|
667
|
+
], children: [leading ? _jsx(View, { accessible: false, style: leadingStyle, children: leading }) : null, active ? (_jsx(View, { accessible: false, style: indicatorStyle, children: renderSelectionIndicator ? (renderSelectionIndicator({
|
|
668
|
+
selected: active,
|
|
669
|
+
color: indicatorColor,
|
|
670
|
+
size: glyph[chipRecipe.selectionIndicator.glyph],
|
|
671
|
+
})) : (_jsx(Text, { style: { color: indicatorColor }, variant: "caption", children: "\u2713" })) })) : null, _jsx(Text, { align: "center", style: [
|
|
672
|
+
{
|
|
673
|
+
color: contentColor,
|
|
674
|
+
fontWeight: active
|
|
675
|
+
? chipRecipe.label.selectedFontWeight
|
|
676
|
+
: chipRecipe.label.fontWeight,
|
|
677
|
+
},
|
|
678
|
+
labelStyle,
|
|
679
|
+
], variant: metrics.textVariant, children: label }), trailing ? _jsx(View, { accessible: false, style: trailingStyle, children: trailing }) : null] }));
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=inputs.js.map
|