jfs-components 0.0.77 → 0.0.79
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/CHANGELOG.md +28 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Attached/Attached.js +144 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
- package/lib/commonjs/components/ListItem/ListItem.js +46 -24
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +49 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Attached/Attached.js +139 -0
- package/lib/module/components/Card/Card.js +25 -2
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
- package/lib/module/components/ListItem/ListItem.js +46 -24
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +7 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/index.d.ts +10 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Attached/Attached.tsx +181 -0
- package/src/components/Card/Card.tsx +28 -1
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/ListItem/ListItem.tsx +55 -25
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
- package/src/components/Slot/Slot.tsx +91 -0
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +10 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -15,9 +15,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
const IS_IOS = Platform.OS === 'ios';
|
|
16
16
|
const PRESS_DELAY = IS_IOS ? 130 : 0;
|
|
17
17
|
|
|
18
|
-
// Forced modes for the
|
|
19
|
-
// overridden by external modes. Frozen so identity is stable across
|
|
20
|
-
|
|
18
|
+
// Forced modes for the leading/trailing slots — `Context: 'ListItem'` can
|
|
19
|
+
// never be overridden by external modes. Frozen so identity is stable across
|
|
20
|
+
// renders. Applied to both slots so they cascade modes identically.
|
|
21
|
+
const SLOT_FORCED_MODES = Object.freeze({
|
|
21
22
|
Context: 'ListItem'
|
|
22
23
|
});
|
|
23
24
|
|
|
@@ -32,27 +33,42 @@ const pressedOverlayStyle = {
|
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
|
|
34
35
|
function resolveListItemTokens(modes) {
|
|
36
|
+
// Modes used to cascade into slot children (leading / supportSlot / trailing).
|
|
37
|
+
// We do NOT inject an `AppearanceBrand` default here: slot content such as
|
|
38
|
+
// Buttons or Badges carry their own intended appearance, so forcing one onto
|
|
39
|
+
// them would be surprising.
|
|
35
40
|
const resolvedModes = {
|
|
36
41
|
...modes,
|
|
37
42
|
Context: 'ListItem'
|
|
38
43
|
};
|
|
44
|
+
|
|
45
|
+
// Modes used to resolve the ListItem's OWN title + support text. Within this
|
|
46
|
+
// component, `AppearanceBrand` only affects `listItem/title/color` and
|
|
47
|
+
// `listItem/supportText/color`, so the text defaults to the "Neutral"
|
|
48
|
+
// appearance (in both Vertical and Horizontal layouts). A caller-supplied
|
|
49
|
+
// `AppearanceBrand` still wins; `Context` is always forced to 'ListItem'.
|
|
50
|
+
const textModes = {
|
|
51
|
+
AppearanceBrand: 'Neutral',
|
|
52
|
+
...modes,
|
|
53
|
+
Context: 'ListItem'
|
|
54
|
+
};
|
|
39
55
|
const gap = getVariableByName('listItem/gap', resolvedModes) ?? 8;
|
|
40
56
|
const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0;
|
|
41
57
|
const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0;
|
|
42
58
|
const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0;
|
|
43
59
|
const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0;
|
|
44
60
|
const textWrapGap = getVariableByName('listItem/text wrap', resolvedModes) ?? 0;
|
|
45
|
-
const titleColor = getVariableByName('listItem/title/color',
|
|
46
|
-
const titleFontSize = getVariableByName('listItem/title/fontSize',
|
|
47
|
-
const titleLineHeight = getVariableByName('listItem/title/lineHeight',
|
|
48
|
-
const titleFontFamily = getVariableByName('listItem/title/fontFamily',
|
|
49
|
-
const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight',
|
|
61
|
+
const titleColor = getVariableByName('listItem/title/color', textModes) || '#0f0d0a';
|
|
62
|
+
const titleFontSize = getVariableByName('listItem/title/fontSize', textModes) || 14;
|
|
63
|
+
const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes) || 16;
|
|
64
|
+
const titleFontFamily = getVariableByName('listItem/title/fontFamily', textModes) || 'System';
|
|
65
|
+
const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', textModes) || 700;
|
|
50
66
|
const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
|
|
51
|
-
const supportColor = getVariableByName('listItem/supportText/color',
|
|
52
|
-
const supportFontSize = getVariableByName('listItem/supportText/fontSize',
|
|
53
|
-
const supportLineHeight = getVariableByName('listItem/supportText/lineHeight',
|
|
54
|
-
const supportFontFamily = getVariableByName('listItem/supportText/fontFamily',
|
|
55
|
-
const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight',
|
|
67
|
+
const supportColor = getVariableByName('listItem/supportText/color', textModes) || '#1f1a14';
|
|
68
|
+
const supportFontSize = getVariableByName('listItem/supportText/fontSize', textModes) || 12;
|
|
69
|
+
const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', textModes) || 14;
|
|
70
|
+
const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', textModes) || 'System';
|
|
71
|
+
const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', textModes) || 500;
|
|
56
72
|
const supportFontWeight = typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw;
|
|
57
73
|
return {
|
|
58
74
|
baseContainerStyle: {
|
|
@@ -116,9 +132,11 @@ const verticalSupportTextOverride = {
|
|
|
116
132
|
* - **design-token driven styling** via `getVariableByName` and `modes`
|
|
117
133
|
*
|
|
118
134
|
* Wherever the Figma layer name contains "Slot", this component exposes a
|
|
119
|
-
* dedicated React "slot" prop
|
|
135
|
+
* dedicated React "slot" prop. The leading and trailing edges share a
|
|
136
|
+
* symmetric `leading` / `trailing` slot API:
|
|
137
|
+
* - Slot "leading" → `leading`
|
|
120
138
|
* - Slot "support text" → `supportSlot`
|
|
121
|
-
* - Slot "
|
|
139
|
+
* - Slot "trailing" → `trailing`
|
|
122
140
|
*
|
|
123
141
|
* @component
|
|
124
142
|
* @param {Object} props
|
|
@@ -126,9 +144,9 @@ const verticalSupportTextOverride = {
|
|
|
126
144
|
* @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
|
|
127
145
|
* @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
|
|
128
146
|
* @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
|
|
129
|
-
* @param {React.ReactNode} [props.leading] - Optional leading
|
|
147
|
+
* @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
|
|
130
148
|
* @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
|
|
131
|
-
* @param {React.ReactNode} [props.
|
|
149
|
+
* @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
|
|
132
150
|
* @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
|
|
133
151
|
* @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens.
|
|
134
152
|
* @param {Function} [props.onPress] - When provided, the entire item becomes pressable (navigation variant).
|
|
@@ -157,6 +175,7 @@ function ListItemImpl({
|
|
|
157
175
|
showSupportText = true,
|
|
158
176
|
leading,
|
|
159
177
|
supportSlot,
|
|
178
|
+
trailing,
|
|
160
179
|
endSlot,
|
|
161
180
|
navArrow = true,
|
|
162
181
|
modes = EMPTY_MODES,
|
|
@@ -194,7 +213,7 @@ function ListItemImpl({
|
|
|
194
213
|
// Process leading slot to pass modes to children. Memoized on
|
|
195
214
|
// (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
|
|
196
215
|
const leadingElement = useMemo(() => {
|
|
197
|
-
const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes) : [];
|
|
216
|
+
const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES) : [];
|
|
198
217
|
if (processed.length === 0) {
|
|
199
218
|
return /*#__PURE__*/_jsx(IconCapsule, {
|
|
200
219
|
modes: tokens.resolvedModes,
|
|
@@ -208,11 +227,14 @@ function ListItemImpl({
|
|
|
208
227
|
const processed = cloneChildrenWithModes(React.Children.toArray(supportSlot), tokens.resolvedModes);
|
|
209
228
|
return processed.length === 1 ? processed[0] : processed;
|
|
210
229
|
}, [supportSlot, tokens.resolvedModes]);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
|
|
231
|
+
// `trailing` wins; `endSlot` is the deprecated alias kept for back-compat.
|
|
232
|
+
const trailingContent = trailing ?? endSlot;
|
|
233
|
+
const processedTrailing = useMemo(() => {
|
|
234
|
+
if (!trailingContent) return null;
|
|
235
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(trailingContent), tokens.resolvedModes, SLOT_FORCED_MODES);
|
|
214
236
|
return processed.length === 1 ? processed[0] : processed;
|
|
215
|
-
}, [
|
|
237
|
+
}, [trailingContent, tokens.resolvedModes]);
|
|
216
238
|
const renderSupportContent = () => {
|
|
217
239
|
if (processedSupportSlot) return processedSupportSlot;
|
|
218
240
|
|
|
@@ -258,9 +280,9 @@ function ListItemImpl({
|
|
|
258
280
|
numberOfLines: 1,
|
|
259
281
|
children: title
|
|
260
282
|
}), showSupportText && renderSupportContent()]
|
|
261
|
-
}),
|
|
283
|
+
}), processedTrailing ? /*#__PURE__*/_jsx(View, {
|
|
262
284
|
style: tokens.trailingWrapperStyle,
|
|
263
|
-
children:
|
|
285
|
+
children: processedTrailing
|
|
264
286
|
}) : null, navArrow && /*#__PURE__*/_jsx(NavArrow, {
|
|
265
287
|
direction: "Forward",
|
|
266
288
|
modes: tokens.resolvedModes
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
8
|
+
import { useFormContext } from '../Form/Form';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Visual state of the textarea. Mirrors the `FormField States` collection so
|
|
16
|
+
* MessageField slots into the same theming pipeline as FormField. The state
|
|
17
|
+
* is always derived from props (`isInvalid`, `isDisabled`, `isReadOnly` and
|
|
18
|
+
* focus) and is locked in `modes['FormField States']` — passing that key in
|
|
19
|
+
* `modes` is intentionally ignored to keep interactive behaviour and visual
|
|
20
|
+
* state in sync.
|
|
21
|
+
*/
|
|
22
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Token helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function toNumber(value, fallback) {
|
|
28
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
const parsed = parseFloat(value);
|
|
31
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
32
|
+
}
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
function toFontWeight(value, fallback) {
|
|
36
|
+
if (typeof value === 'number') return value.toString();
|
|
37
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
function firstError(error) {
|
|
41
|
+
if (!error) return undefined;
|
|
42
|
+
if (Array.isArray(error)) return error[0];
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
function useMessageFieldTokens(modes) {
|
|
46
|
+
return useMemo(() => {
|
|
47
|
+
const wrapperGap = toNumber(getVariableByName('messageField/gap', modes), 8);
|
|
48
|
+
const labelColor = getVariableByName('messageField/label/foreground', modes) || '#000000';
|
|
49
|
+
const labelFontFamily = getVariableByName('messageField/label/fontFamily', modes) || 'JioType Var';
|
|
50
|
+
const labelFontSize = toNumber(getVariableByName('messageField/label/fontSize', modes), 14);
|
|
51
|
+
const labelLineHeight = toNumber(getVariableByName('messageField/label/lineHeight', modes), 17);
|
|
52
|
+
const labelFontWeight = toFontWeight(getVariableByName('messageField/label/fontWeight', modes), '500');
|
|
53
|
+
const textareaBackground = getVariableByName('messageField/textarea/background', modes) || '#ffffff';
|
|
54
|
+
const textareaBorderColor = getVariableByName('messageField/textarea/border/color', modes) || '#b5b6b7';
|
|
55
|
+
const textareaBorderSize = toNumber(getVariableByName('messageField/textarea/border/size', modes), 1.5);
|
|
56
|
+
const textareaRadius = toNumber(getVariableByName('messageField/textarea/radius', modes), 8);
|
|
57
|
+
const textareaPadding = toNumber(getVariableByName('messageField/textarea/padding', modes), 12);
|
|
58
|
+
const textareaHeight = toNumber(getVariableByName('messageField/textarea/height', modes), 108);
|
|
59
|
+
const textareaGap = toNumber(getVariableByName('messageField/textarea/gap', modes), 0);
|
|
60
|
+
|
|
61
|
+
// `messageField/text/foreground` is the input text color. It also
|
|
62
|
+
// serves as the placeholder color — in mode-aware token sets it
|
|
63
|
+
// resolves to a muted/idle color when empty and shifts darker via
|
|
64
|
+
// the `FormField States` cascade once typed-state tokens land. We
|
|
65
|
+
// never re-route this through another token (e.g. the counter
|
|
66
|
+
// color) because that conflates two semantically distinct tokens.
|
|
67
|
+
const inputTextColor = getVariableByName('messageField/text/foreground', modes) || '#707275';
|
|
68
|
+
const inputFontFamily = getVariableByName('messageField/text/fontFamily', modes) || 'JioType Var';
|
|
69
|
+
const inputFontSize = toNumber(getVariableByName('messageField/text/fontSize', modes), 16);
|
|
70
|
+
const inputLineHeight = toNumber(getVariableByName('messageField/text/lineHeight', modes), 21);
|
|
71
|
+
const inputFontWeight = toFontWeight(getVariableByName('messageField/text/fontWeight', modes), '400');
|
|
72
|
+
const counterColor = getVariableByName('messageField/maxLength/foreground', modes) || '#24262b';
|
|
73
|
+
const counterFontFamily = getVariableByName('messageField/maxLength/fontFamily', modes) || 'JioType Var';
|
|
74
|
+
const counterFontSize = toNumber(getVariableByName('messageField/maxLength/fontSize', modes), 14);
|
|
75
|
+
const counterLineHeight = toNumber(getVariableByName('messageField/maxLength/lineHeight', modes), 18);
|
|
76
|
+
const counterFontWeight = toFontWeight(getVariableByName('messageField/maxLength/fontWeight', modes), '400');
|
|
77
|
+
return {
|
|
78
|
+
wrapperGap,
|
|
79
|
+
labelColor,
|
|
80
|
+
labelFontFamily,
|
|
81
|
+
labelFontSize,
|
|
82
|
+
labelLineHeight,
|
|
83
|
+
labelFontWeight,
|
|
84
|
+
textareaBackground,
|
|
85
|
+
textareaBorderColor,
|
|
86
|
+
textareaBorderSize,
|
|
87
|
+
textareaRadius,
|
|
88
|
+
textareaPadding,
|
|
89
|
+
textareaHeight,
|
|
90
|
+
textareaGap,
|
|
91
|
+
inputTextColor,
|
|
92
|
+
inputFontFamily,
|
|
93
|
+
inputFontSize,
|
|
94
|
+
inputLineHeight,
|
|
95
|
+
inputFontWeight,
|
|
96
|
+
counterColor,
|
|
97
|
+
counterFontFamily,
|
|
98
|
+
counterFontSize,
|
|
99
|
+
counterLineHeight,
|
|
100
|
+
counterFontWeight
|
|
101
|
+
};
|
|
102
|
+
}, [modes]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Component
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
const REQUIRED_INDICATOR_COLOR = '#d93d3d';
|
|
110
|
+
function MessageField({
|
|
111
|
+
label,
|
|
112
|
+
placeholder,
|
|
113
|
+
value,
|
|
114
|
+
defaultValue,
|
|
115
|
+
onChangeText,
|
|
116
|
+
name,
|
|
117
|
+
maxLength,
|
|
118
|
+
showCounter,
|
|
119
|
+
rows,
|
|
120
|
+
isRequired = false,
|
|
121
|
+
isDisabled = false,
|
|
122
|
+
isInvalid = false,
|
|
123
|
+
isReadOnly = false,
|
|
124
|
+
autoFocus = false,
|
|
125
|
+
modes: propModes = EMPTY_MODES,
|
|
126
|
+
style,
|
|
127
|
+
textareaStyle,
|
|
128
|
+
inputStyle,
|
|
129
|
+
accessibilityLabel,
|
|
130
|
+
accessibilityHint,
|
|
131
|
+
testID,
|
|
132
|
+
onFocus,
|
|
133
|
+
onBlur
|
|
134
|
+
}) {
|
|
135
|
+
const formCtx = useFormContext();
|
|
136
|
+
const formError = name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined;
|
|
137
|
+
const resolvedIsInvalid = isInvalid || Boolean(formError);
|
|
138
|
+
const isControlled = value !== undefined;
|
|
139
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
|
|
140
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
141
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
142
|
+
const interactive = !isDisabled && !isReadOnly;
|
|
143
|
+
|
|
144
|
+
// Ref to the native textarea so tapping anywhere in the (padded) textarea
|
|
145
|
+
// container focuses it on the FIRST tap, fixing the Android "two taps to
|
|
146
|
+
// open the keyboard" issue.
|
|
147
|
+
const inputRef = useRef(null);
|
|
148
|
+
const focusInput = useCallback(() => {
|
|
149
|
+
if (!interactive) return;
|
|
150
|
+
inputRef.current?.focus();
|
|
151
|
+
}, [interactive]);
|
|
152
|
+
const {
|
|
153
|
+
modes: globalModes
|
|
154
|
+
} = useTokens();
|
|
155
|
+
const baseModes = useMemo(() => ({
|
|
156
|
+
...globalModes,
|
|
157
|
+
...propModes
|
|
158
|
+
}), [globalModes, propModes]);
|
|
159
|
+
|
|
160
|
+
// FormField States cascade — error > disabled > read only > active (focus)
|
|
161
|
+
// > idle. Always derived from props and locked into the modes object so
|
|
162
|
+
// consumers cannot pass `modes={{ 'FormField States': ... }}` and get out
|
|
163
|
+
// of sync with the component's actual interactive behaviour.
|
|
164
|
+
const stateMode = useMemo(() => {
|
|
165
|
+
if (resolvedIsInvalid) return 'Error';
|
|
166
|
+
if (isDisabled) return 'Disabled';
|
|
167
|
+
if (isReadOnly) return 'Read Only';
|
|
168
|
+
if (isFocused) return 'Active';
|
|
169
|
+
return 'Idle';
|
|
170
|
+
}, [resolvedIsInvalid, isDisabled, isReadOnly, isFocused]);
|
|
171
|
+
const modes = useMemo(() => ({
|
|
172
|
+
...baseModes,
|
|
173
|
+
'FormField States': stateMode
|
|
174
|
+
}), [baseModes, stateMode]);
|
|
175
|
+
const tokens = useMessageFieldTokens(modes);
|
|
176
|
+
|
|
177
|
+
// ---------- Event handlers ---------------------------------------------
|
|
178
|
+
const handleFocus = useCallback(e => {
|
|
179
|
+
setIsFocused(true);
|
|
180
|
+
onFocus?.(e);
|
|
181
|
+
}, [onFocus]);
|
|
182
|
+
const handleBlur = useCallback(e => {
|
|
183
|
+
setIsFocused(false);
|
|
184
|
+
onBlur?.(e);
|
|
185
|
+
}, [onBlur]);
|
|
186
|
+
const handleChangeText = useCallback(next => {
|
|
187
|
+
if (!isControlled) {
|
|
188
|
+
setUncontrolledValue(next);
|
|
189
|
+
}
|
|
190
|
+
onChangeText?.(next);
|
|
191
|
+
if (name && formCtx) formCtx.onFieldChange(name);
|
|
192
|
+
}, [isControlled, onChangeText, name, formCtx]);
|
|
193
|
+
|
|
194
|
+
// ---------- Derived layout values --------------------------------------
|
|
195
|
+
const computedHeight = useMemo(() => {
|
|
196
|
+
if (rows && rows > 0) {
|
|
197
|
+
return Math.round(rows * tokens.inputLineHeight + 2 * tokens.textareaPadding);
|
|
198
|
+
}
|
|
199
|
+
return tokens.textareaHeight;
|
|
200
|
+
}, [rows, tokens.inputLineHeight, tokens.textareaPadding, tokens.textareaHeight]);
|
|
201
|
+
const shouldShowCounter = useMemo(() => {
|
|
202
|
+
if (showCounter === false) return false;
|
|
203
|
+
if (showCounter === true) return true;
|
|
204
|
+
return typeof maxLength === 'number';
|
|
205
|
+
}, [showCounter, maxLength]);
|
|
206
|
+
const counterText = useMemo(() => {
|
|
207
|
+
const count = currentValue.length;
|
|
208
|
+
if (typeof maxLength === 'number') return `${count}/${maxLength}`;
|
|
209
|
+
return `${count}`;
|
|
210
|
+
}, [currentValue.length, maxLength]);
|
|
211
|
+
|
|
212
|
+
// ---------- Styles -----------------------------------------------------
|
|
213
|
+
const wrapperStyle = useMemo(() => ({
|
|
214
|
+
gap: tokens.wrapperGap,
|
|
215
|
+
width: '100%'
|
|
216
|
+
}), [tokens.wrapperGap]);
|
|
217
|
+
const labelRowStyle = useMemo(() => ({
|
|
218
|
+
flexDirection: 'row',
|
|
219
|
+
alignItems: 'baseline'
|
|
220
|
+
}), []);
|
|
221
|
+
const labelTextStyle = useMemo(() => ({
|
|
222
|
+
color: tokens.labelColor,
|
|
223
|
+
fontFamily: tokens.labelFontFamily,
|
|
224
|
+
fontSize: tokens.labelFontSize,
|
|
225
|
+
lineHeight: tokens.labelLineHeight,
|
|
226
|
+
fontWeight: tokens.labelFontWeight
|
|
227
|
+
}), [tokens.labelColor, tokens.labelFontFamily, tokens.labelFontSize, tokens.labelLineHeight, tokens.labelFontWeight]);
|
|
228
|
+
const requiredIndicatorStyle = useMemo(() => ({
|
|
229
|
+
...labelTextStyle,
|
|
230
|
+
color: REQUIRED_INDICATOR_COLOR
|
|
231
|
+
}), [labelTextStyle]);
|
|
232
|
+
const textareaContainerStyle = useMemo(() => ({
|
|
233
|
+
backgroundColor: tokens.textareaBackground,
|
|
234
|
+
borderColor: tokens.textareaBorderColor,
|
|
235
|
+
borderWidth: tokens.textareaBorderSize,
|
|
236
|
+
borderStyle: 'solid',
|
|
237
|
+
borderRadius: tokens.textareaRadius,
|
|
238
|
+
padding: tokens.textareaPadding,
|
|
239
|
+
height: computedHeight,
|
|
240
|
+
width: '100%',
|
|
241
|
+
overflow: 'hidden',
|
|
242
|
+
// The gap token is for content within the textarea (icons, etc.);
|
|
243
|
+
// we keep it so downstream layouts that pass children align.
|
|
244
|
+
gap: tokens.textareaGap
|
|
245
|
+
}), [tokens.textareaBackground, tokens.textareaBorderColor, tokens.textareaBorderSize, tokens.textareaRadius, tokens.textareaPadding, computedHeight, tokens.textareaGap]);
|
|
246
|
+
const inputTextStyle = useMemo(() => ({
|
|
247
|
+
flex: 1,
|
|
248
|
+
color: tokens.inputTextColor,
|
|
249
|
+
fontFamily: tokens.inputFontFamily,
|
|
250
|
+
fontSize: tokens.inputFontSize,
|
|
251
|
+
lineHeight: tokens.inputLineHeight,
|
|
252
|
+
fontWeight: tokens.inputFontWeight,
|
|
253
|
+
padding: 0,
|
|
254
|
+
margin: 0,
|
|
255
|
+
textAlignVertical: 'top',
|
|
256
|
+
// Disable the default web focus ring; the textarea border
|
|
257
|
+
// already encodes focus state.
|
|
258
|
+
outlineStyle: 'none',
|
|
259
|
+
outlineWidth: 0,
|
|
260
|
+
outlineColor: 'transparent'
|
|
261
|
+
}), [tokens.inputTextColor, tokens.inputFontFamily, tokens.inputFontSize, tokens.inputLineHeight, tokens.inputFontWeight]);
|
|
262
|
+
const counterTextStyle = useMemo(() => ({
|
|
263
|
+
color: tokens.counterColor,
|
|
264
|
+
fontFamily: tokens.counterFontFamily,
|
|
265
|
+
fontSize: tokens.counterFontSize,
|
|
266
|
+
lineHeight: tokens.counterLineHeight,
|
|
267
|
+
fontWeight: tokens.counterFontWeight,
|
|
268
|
+
textAlign: 'right',
|
|
269
|
+
width: '100%'
|
|
270
|
+
}), [tokens.counterColor, tokens.counterFontFamily, tokens.counterFontSize, tokens.counterLineHeight, tokens.counterFontWeight]);
|
|
271
|
+
const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Message field';
|
|
272
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
273
|
+
style: [wrapperStyle, style],
|
|
274
|
+
pointerEvents: isDisabled ? 'none' : 'auto',
|
|
275
|
+
testID: testID,
|
|
276
|
+
accessible: false,
|
|
277
|
+
children: [label != null && label !== '' && /*#__PURE__*/_jsxs(View, {
|
|
278
|
+
style: labelRowStyle,
|
|
279
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
280
|
+
style: labelTextStyle,
|
|
281
|
+
children: label
|
|
282
|
+
}), isRequired && /*#__PURE__*/_jsx(Text, {
|
|
283
|
+
style: requiredIndicatorStyle,
|
|
284
|
+
children: " *"
|
|
285
|
+
})]
|
|
286
|
+
}), /*#__PURE__*/_jsx(Pressable, {
|
|
287
|
+
style: [textareaContainerStyle, textareaStyle],
|
|
288
|
+
onPress: focusInput,
|
|
289
|
+
accessible: false,
|
|
290
|
+
children: /*#__PURE__*/_jsx(RNTextInput, {
|
|
291
|
+
ref: inputRef,
|
|
292
|
+
multiline: true,
|
|
293
|
+
value: currentValue,
|
|
294
|
+
onChangeText: handleChangeText,
|
|
295
|
+
onFocus: handleFocus,
|
|
296
|
+
onBlur: handleBlur,
|
|
297
|
+
placeholder: placeholder ?? '',
|
|
298
|
+
placeholderTextColor: tokens.inputTextColor,
|
|
299
|
+
editable: interactive,
|
|
300
|
+
maxLength: maxLength,
|
|
301
|
+
autoFocus: autoFocus,
|
|
302
|
+
accessibilityLabel: resolvedA11yLabel,
|
|
303
|
+
accessibilityHint: accessibilityHint,
|
|
304
|
+
style: [inputTextStyle, inputStyle]
|
|
305
|
+
})
|
|
306
|
+
}), shouldShowCounter && /*#__PURE__*/_jsx(Text, {
|
|
307
|
+
style: counterTextStyle,
|
|
308
|
+
accessibilityElementsHidden: true,
|
|
309
|
+
children: counterText
|
|
310
|
+
})]
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
export default MessageField;
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useMemo } from 'react';
|
|
4
|
-
import { View } from 'react-native';
|
|
4
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
5
5
|
import Svg, { Polyline } from 'react-native-svg';
|
|
6
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
7
|
+
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
7
8
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
11
|
+
const MIN_TOUCH_TARGET = 44;
|
|
12
|
+
const IS_IOS = Platform.OS === 'ios';
|
|
13
|
+
const PRESS_DELAY = IS_IOS ? 130 : 0;
|
|
14
|
+
const touchTargetStyle = {
|
|
15
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
16
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
justifyContent: 'center'
|
|
19
|
+
};
|
|
9
20
|
function resolveNavArrowTokens(modes) {
|
|
10
21
|
const iconColor = getVariableByName('navArrow/icon/color', modes) || '#24262b';
|
|
11
22
|
const widthToken = Number(getVariableByName('navArrow/width', modes)) || 6;
|
|
@@ -46,6 +57,8 @@ function NavArrow({
|
|
|
46
57
|
modes = EMPTY_MODES,
|
|
47
58
|
style,
|
|
48
59
|
accessibilityLabel,
|
|
60
|
+
onPress,
|
|
61
|
+
disabled = false,
|
|
49
62
|
...rest
|
|
50
63
|
}) {
|
|
51
64
|
const tokens = useMemo(() => resolveNavArrowTokens(modes), [modes]);
|
|
@@ -59,8 +72,7 @@ function NavArrow({
|
|
|
59
72
|
borderRadius: tokens.borderRadius,
|
|
60
73
|
backgroundColor: tokens.backgroundColor,
|
|
61
74
|
alignItems: 'center',
|
|
62
|
-
justifyContent: 'center'
|
|
63
|
-
...(style || {})
|
|
75
|
+
justifyContent: 'center'
|
|
64
76
|
};
|
|
65
77
|
const chevronW = isDown ? tokens.iconHeight : tokens.iconWidth;
|
|
66
78
|
const chevronH = isDown ? tokens.iconWidth : tokens.iconHeight;
|
|
@@ -86,26 +98,55 @@ function NavArrow({
|
|
|
86
98
|
svgHeight,
|
|
87
99
|
points
|
|
88
100
|
};
|
|
89
|
-
}, [tokens, direction
|
|
101
|
+
}, [tokens, direction]);
|
|
90
102
|
const defaultAccessibilityLabel = accessibilityLabel || (direction === 'Back' ? 'Go back' : direction === 'Forward' ? 'Go forward' : 'Go down');
|
|
103
|
+
const webProps = usePressableWebSupport({
|
|
104
|
+
restProps: rest,
|
|
105
|
+
onPress,
|
|
106
|
+
disabled,
|
|
107
|
+
accessibilityLabel: defaultAccessibilityLabel
|
|
108
|
+
});
|
|
109
|
+
const chevron = /*#__PURE__*/_jsx(Svg, {
|
|
110
|
+
width: computed.svgWidth,
|
|
111
|
+
height: computed.svgHeight,
|
|
112
|
+
viewBox: `0 0 ${computed.svgWidth} ${computed.svgHeight}`,
|
|
113
|
+
children: /*#__PURE__*/_jsx(Polyline, {
|
|
114
|
+
points: computed.points,
|
|
115
|
+
stroke: tokens.iconColor,
|
|
116
|
+
strokeWidth: tokens.strokeWeight,
|
|
117
|
+
strokeLinecap: "round",
|
|
118
|
+
strokeLinejoin: "round",
|
|
119
|
+
fill: "none"
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
if (onPress) {
|
|
123
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
124
|
+
onPress: onPress,
|
|
125
|
+
disabled: disabled,
|
|
126
|
+
accessibilityRole: "button",
|
|
127
|
+
accessibilityLabel: defaultAccessibilityLabel,
|
|
128
|
+
accessibilityState: {
|
|
129
|
+
disabled
|
|
130
|
+
},
|
|
131
|
+
unstable_pressDelay: PRESS_DELAY,
|
|
132
|
+
style: ({
|
|
133
|
+
pressed
|
|
134
|
+
}) => [touchTargetStyle, style, pressed && !disabled ? {
|
|
135
|
+
opacity: 0.7
|
|
136
|
+
} : null],
|
|
137
|
+
...webProps,
|
|
138
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
139
|
+
style: computed.containerStyle,
|
|
140
|
+
children: chevron
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
}
|
|
91
144
|
return /*#__PURE__*/_jsx(View, {
|
|
92
|
-
style: computed.containerStyle,
|
|
145
|
+
style: [computed.containerStyle, style],
|
|
93
146
|
accessibilityRole: "image",
|
|
94
147
|
accessibilityLabel: defaultAccessibilityLabel,
|
|
95
148
|
...rest,
|
|
96
|
-
children:
|
|
97
|
-
width: computed.svgWidth,
|
|
98
|
-
height: computed.svgHeight,
|
|
99
|
-
viewBox: `0 0 ${computed.svgWidth} ${computed.svgHeight}`,
|
|
100
|
-
children: /*#__PURE__*/_jsx(Polyline, {
|
|
101
|
-
points: computed.points,
|
|
102
|
-
stroke: tokens.iconColor,
|
|
103
|
-
strokeWidth: tokens.strokeWeight,
|
|
104
|
-
strokeLinecap: "round",
|
|
105
|
-
strokeLinejoin: "round",
|
|
106
|
-
fill: "none"
|
|
107
|
-
})
|
|
108
|
-
})
|
|
149
|
+
children: chevron
|
|
109
150
|
});
|
|
110
151
|
}
|
|
111
152
|
export default /*#__PURE__*/React.memo(NavArrow);
|