jfs-components 0.0.79 → 0.0.84
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/lib/commonjs/components/AppBar/AppBar.js +56 -6
- package/lib/commonjs/components/Attached/Attached.js +46 -7
- package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
- package/lib/commonjs/components/Drawer/Drawer.js +6 -1
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/commonjs/components/FormField/FormField.js +1 -14
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/commonjs/components/ListItem/ListItem.js +6 -11
- package/lib/commonjs/components/MessageField/MessageField.js +1 -13
- package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
- package/lib/commonjs/components/Spinner/Spinner.js +217 -0
- package/lib/commonjs/components/TextInput/TextInput.js +33 -18
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
- package/lib/commonjs/icons/components/IconArrowup.js +19 -0
- package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
- package/lib/commonjs/icons/components/IconSignin.js +19 -0
- package/lib/commonjs/icons/components/IconSignout.js +19 -0
- package/lib/commonjs/icons/components/index.js +132 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AppBar/AppBar.js +56 -6
- package/lib/module/components/Attached/Attached.js +46 -7
- package/lib/module/components/Checkbox/Checkbox.js +18 -2
- package/lib/module/components/Drawer/Drawer.js +6 -1
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/module/components/FormField/FormField.js +3 -16
- package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/module/components/ListItem/ListItem.js +6 -11
- package/lib/module/components/MessageField/MessageField.js +3 -15
- package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
- package/lib/module/components/Spinner/Spinner.js +212 -0
- package/lib/module/components/TextInput/TextInput.js +34 -19
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/components/IconArrowdown.js +12 -0
- package/lib/module/icons/components/IconArrowup.js +12 -0
- package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
- package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
- package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
- package/lib/module/icons/components/IconChevronupcircle.js +12 -0
- package/lib/module/icons/components/IconOsnavback.js +12 -0
- package/lib/module/icons/components/IconOsnavcenter.js +12 -0
- package/lib/module/icons/components/IconOsnavhome.js +12 -0
- package/lib/module/icons/components/IconOsnavtask.js +12 -0
- package/lib/module/icons/components/IconSignin.js +12 -0
- package/lib/module/icons/components/IconSignout.js +12 -0
- package/lib/module/icons/components/index.js +12 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
- package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
- package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
- package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +12 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -2
- package/src/components/AppBar/AppBar.tsx +79 -12
- package/src/components/Attached/Attached.tsx +63 -7
- package/src/components/Checkbox/Checkbox.tsx +14 -2
- package/src/components/Drawer/Drawer.tsx +4 -0
- package/src/components/DropdownInput/DropdownInput.tsx +54 -20
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
- package/src/components/FormField/FormField.tsx +3 -19
- package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
- package/src/components/ListItem/ListItem.tsx +14 -16
- package/src/components/MessageField/MessageField.tsx +3 -18
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
- package/src/components/Spinner/Spinner.tsx +273 -0
- package/src/components/TextInput/TextInput.tsx +37 -19
- package/src/components/index.ts +1 -0
- package/src/icons/components/IconArrowdown.tsx +11 -0
- package/src/icons/components/IconArrowup.tsx +11 -0
- package/src/icons/components/IconChevrondowncircle.tsx +11 -0
- package/src/icons/components/IconChevronleftcircle.tsx +11 -0
- package/src/icons/components/IconChevronrightcircle.tsx +11 -0
- package/src/icons/components/IconChevronupcircle.tsx +11 -0
- package/src/icons/components/IconOsnavback.tsx +11 -0
- package/src/icons/components/IconOsnavcenter.tsx +11 -0
- package/src/icons/components/IconOsnavhome.tsx +11 -0
- package/src/icons/components/IconOsnavtask.tsx +11 -0
- package/src/icons/components/IconSignin.tsx +11 -0
- package/src/icons/components/IconSignout.tsx +11 -0
- package/src/icons/components/index.ts +12 -0
- package/src/icons/registry.ts +49 -1
|
@@ -439,6 +439,10 @@ function Drawer({
|
|
|
439
439
|
style={[styles.content, contentStyle]}
|
|
440
440
|
contentContainerStyle={[{ paddingBottom: paddingBottom + bottomInset, gap: drawerGap, flexDirection: 'column', alignItems: 'stretch' }, contentContainerStyle]}
|
|
441
441
|
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
|
442
|
+
// Let a tap on an input inside the sheet focus it on the FIRST tap
|
|
443
|
+
// even while the keyboard is already open (default 'never' would
|
|
444
|
+
// eat that tap just to dismiss the keyboard).
|
|
445
|
+
keyboardShouldPersistTaps="handled"
|
|
442
446
|
animatedProps={animatedScrollProps}
|
|
443
447
|
alwaysBounceVertical={false}
|
|
444
448
|
overScrollMode="always"
|
|
@@ -117,8 +117,9 @@ export type DropdownInputProps = {
|
|
|
117
117
|
*/
|
|
118
118
|
menuMaxHeight?: number
|
|
119
119
|
/**
|
|
120
|
-
* Pixel
|
|
121
|
-
*
|
|
120
|
+
* Pixel gap between the trigger and the popup. When omitted, it defaults
|
|
121
|
+
* to the `formField/gap` design token so the menu sits the same distance
|
|
122
|
+
* below the input as the rest of the field's internal spacing.
|
|
122
123
|
*/
|
|
123
124
|
menuOffset?: number
|
|
124
125
|
/**
|
|
@@ -325,7 +326,7 @@ function DropdownInput({
|
|
|
325
326
|
supportText,
|
|
326
327
|
errorMessage,
|
|
327
328
|
menuMaxHeight = 240,
|
|
328
|
-
menuOffset
|
|
329
|
+
menuOffset,
|
|
329
330
|
matchTriggerWidth = true,
|
|
330
331
|
closeOnBackdropPress = true,
|
|
331
332
|
modes: propModes = EMPTY_MODES,
|
|
@@ -422,11 +423,30 @@ function DropdownInput({
|
|
|
422
423
|
const tokens = useFormFieldTokens(modes)
|
|
423
424
|
const chevron = useChevronTokens(modes)
|
|
424
425
|
|
|
426
|
+
// Gap between the input and the popup. Falls back to the `formField/gap`
|
|
427
|
+
// token so the menu's offset matches the field's own internal spacing.
|
|
428
|
+
const effectiveMenuOffset = menuOffset ?? tokens.gap
|
|
429
|
+
|
|
425
430
|
// ---------------- Layout / measurement ----------------
|
|
426
431
|
const triggerRef = useRef<View>(null)
|
|
427
432
|
const [triggerRect, setTriggerRect] = useState<Rect | null>(null)
|
|
428
433
|
const insets = useSafeAreaInsets()
|
|
429
434
|
|
|
435
|
+
// Android coordinate-space bridge.
|
|
436
|
+
//
|
|
437
|
+
// The popup lives inside a `statusBarTranslucent` Modal, whose window is
|
|
438
|
+
// laid out from the PHYSICAL top of the screen (behind the status bar).
|
|
439
|
+
// The trigger, however, is rendered inside the app's content area (Expo
|
|
440
|
+
// Router / react-native-screens under edge-to-edge), so its
|
|
441
|
+
// `measureInWindow` Y is relative to the content area — it does NOT include
|
|
442
|
+
// the status bar height. Feeding that Y straight into the Modal would place
|
|
443
|
+
// the popup one status-bar-height too high, landing it on top of the input.
|
|
444
|
+
//
|
|
445
|
+
// Adding `insets.top` converts the trigger's content-relative Y into the
|
|
446
|
+
// Modal's full-screen coordinate space. iOS/web share a single coordinate
|
|
447
|
+
// space for the Modal and the trigger, so no shift is needed there.
|
|
448
|
+
const windowTopOffset = Platform.OS === 'android' ? insets.top : 0
|
|
449
|
+
|
|
430
450
|
const measure = useCallback(() => {
|
|
431
451
|
if (!triggerRef.current) return
|
|
432
452
|
triggerRef.current.measureInWindow((x, y, width, height) => {
|
|
@@ -503,7 +523,7 @@ function DropdownInput({
|
|
|
503
523
|
menuSize?.height ?? menuMaxHeight,
|
|
504
524
|
menuMaxHeight
|
|
505
525
|
)
|
|
506
|
-
const needed = desiredHeight +
|
|
526
|
+
const needed = desiredHeight + effectiveMenuOffset + 8
|
|
507
527
|
if (placement === 'top') {
|
|
508
528
|
return spaceAbove >= needed || spaceAbove >= spaceBelow
|
|
509
529
|
? 'top'
|
|
@@ -523,7 +543,7 @@ function DropdownInput({
|
|
|
523
543
|
windowHeight,
|
|
524
544
|
menuSize?.height,
|
|
525
545
|
menuMaxHeight,
|
|
526
|
-
|
|
546
|
+
effectiveMenuOffset,
|
|
527
547
|
insets.top,
|
|
528
548
|
insets.bottom,
|
|
529
549
|
])
|
|
@@ -544,15 +564,18 @@ function DropdownInput({
|
|
|
544
564
|
if (leftPos > maxLeft) leftPos = maxLeft
|
|
545
565
|
if (leftPos < minLeft) leftPos = minLeft
|
|
546
566
|
|
|
567
|
+
// Trigger top expressed in the Modal's (full-screen) coordinate space.
|
|
568
|
+
const triggerTop = triggerRect.y + windowTopOffset
|
|
569
|
+
|
|
547
570
|
let topPos: number
|
|
548
571
|
if (computedPlacement === 'top') {
|
|
549
572
|
const desiredHeight = menuSize?.height ?? menuMaxHeight
|
|
550
|
-
topPos =
|
|
573
|
+
topPos = triggerTop - desiredHeight - effectiveMenuOffset
|
|
551
574
|
if (topPos < insets.top + screenPadding) {
|
|
552
575
|
topPos = insets.top + screenPadding
|
|
553
576
|
}
|
|
554
577
|
} else {
|
|
555
|
-
topPos =
|
|
578
|
+
topPos = triggerTop + triggerRect.height + effectiveMenuOffset
|
|
556
579
|
}
|
|
557
580
|
|
|
558
581
|
const style: ViewStyle = {
|
|
@@ -569,7 +592,8 @@ function DropdownInput({
|
|
|
569
592
|
triggerRect,
|
|
570
593
|
computedPlacement,
|
|
571
594
|
menuSize,
|
|
572
|
-
|
|
595
|
+
effectiveMenuOffset,
|
|
596
|
+
windowTopOffset,
|
|
573
597
|
menuMaxHeight,
|
|
574
598
|
matchTriggerWidth,
|
|
575
599
|
windowWidth,
|
|
@@ -779,22 +803,32 @@ function DropdownInput({
|
|
|
779
803
|
)}
|
|
780
804
|
|
|
781
805
|
{/*
|
|
782
|
-
IMPORTANT:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
806
|
+
IMPORTANT: this Modal MUST be `statusBarTranslucent` (and
|
|
807
|
+
`navigationBarTranslucent`) on Android.
|
|
808
|
+
|
|
809
|
+
The app runs edge-to-edge (Expo SDK 54+ / Android 15 enforce it
|
|
810
|
+
and it cannot be disabled). That means the activity window spans
|
|
811
|
+
the entire physical screen, so `measureInWindow` on the trigger
|
|
812
|
+
returns a `y` measured from the very TOP of the screen — the
|
|
813
|
+
status bar height is INCLUDED.
|
|
814
|
+
|
|
815
|
+
A non-translucent Modal, however, opens a window whose content
|
|
816
|
+
area starts BELOW the status bar, so `top: 0` inside it maps to
|
|
817
|
+
screen-Y = statusBarHeight. Every `top` we compute is then
|
|
818
|
+
shifted UP by one status-bar-height relative to the trigger,
|
|
819
|
+
which (because the input row height is roughly a status bar tall)
|
|
820
|
+
drops the popup right on top of the input.
|
|
821
|
+
|
|
822
|
+
Making the Modal translucent gives it a full-screen window whose
|
|
823
|
+
origin matches the edge-to-edge activity window, so the
|
|
824
|
+
`measureInWindow` coordinates and the popup's absolute `top`/
|
|
825
|
+
`left` finally live in the same coordinate space.
|
|
794
826
|
*/}
|
|
795
827
|
<Modal
|
|
796
828
|
visible={isOpen}
|
|
797
829
|
transparent
|
|
830
|
+
statusBarTranslucent
|
|
831
|
+
navigationBarTranslucent
|
|
798
832
|
animationType="fade"
|
|
799
833
|
onRequestClose={closeMenu}
|
|
800
834
|
>
|
|
@@ -129,10 +129,6 @@ function ExpandableCheckbox({
|
|
|
129
129
|
|
|
130
130
|
const rowGap =
|
|
131
131
|
(getVariableByName('checkboxItem/gap', modes) as number | null) ?? 8
|
|
132
|
-
const rowPaddingHorizontal =
|
|
133
|
-
(getVariableByName('checkboxItem/padding/horizontal', modes) as number | null) ?? 0
|
|
134
|
-
const rowPaddingVertical =
|
|
135
|
-
(getVariableByName('checkboxItem/padding/vertical', modes) as number | null) ?? 0
|
|
136
132
|
|
|
137
133
|
const labelColor =
|
|
138
134
|
(getVariableByName('checkboxItem/foreground', modes) as string | null) ?? '#1a1c1f'
|
|
@@ -163,12 +159,10 @@ function ExpandableCheckbox({
|
|
|
163
159
|
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
164
160
|
minWidth: 0,
|
|
165
161
|
flexDirection: 'row',
|
|
166
|
-
alignItems: 'flex-start',
|
|
162
|
+
alignItems: isExpanded ? 'flex-start' : 'center',
|
|
167
163
|
gap: rowGap,
|
|
168
|
-
paddingHorizontal: rowPaddingHorizontal,
|
|
169
|
-
paddingVertical: rowPaddingVertical,
|
|
170
164
|
}),
|
|
171
|
-
[isExpanded, rowGap
|
|
165
|
+
[isExpanded, rowGap]
|
|
172
166
|
)
|
|
173
167
|
|
|
174
168
|
const resolvedLabelStyle: TextStyle = useMemo(
|
|
@@ -180,6 +174,13 @@ function ExpandableCheckbox({
|
|
|
180
174
|
fontSize: labelFontSize,
|
|
181
175
|
lineHeight: labelLineHeight,
|
|
182
176
|
fontWeight: labelFontWeight,
|
|
177
|
+
// Android adds asymmetric font padding and top-aligns the glyph inside
|
|
178
|
+
// an inflated line box when `lineHeight` is set. That makes the centered
|
|
179
|
+
// checkbox look like it drops below the text. Disabling the extra
|
|
180
|
+
// padding + centering the glyph keeps the single-line label optically
|
|
181
|
+
// aligned with the checkbox. No-op on iOS / web.
|
|
182
|
+
includeFontPadding: false,
|
|
183
|
+
textAlignVertical: isExpanded ? 'top' : 'center',
|
|
183
184
|
}),
|
|
184
185
|
[
|
|
185
186
|
labelColor,
|
|
@@ -187,11 +188,14 @@ function ExpandableCheckbox({
|
|
|
187
188
|
labelFontSize,
|
|
188
189
|
labelLineHeight,
|
|
189
190
|
labelFontWeight,
|
|
191
|
+
isExpanded,
|
|
190
192
|
]
|
|
191
193
|
)
|
|
192
194
|
|
|
195
|
+
// Layer component modes first (e.g. Color Mode), then button defaults so
|
|
196
|
+
// Secondary / XS / Low always win unless a dedicated override prop is added.
|
|
193
197
|
const buttonModes = useMemo(
|
|
194
|
-
() => ({ ...
|
|
198
|
+
() => ({ ...modes, ...BUTTON_DEFAULT_MODES }),
|
|
195
199
|
[modes]
|
|
196
200
|
)
|
|
197
201
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React, { useCallback, useMemo,
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
-
Pressable,
|
|
6
5
|
TextInput as RNTextInput,
|
|
7
6
|
type StyleProp,
|
|
8
7
|
type TextInputProps as RNTextInputProps,
|
|
@@ -347,16 +346,6 @@ function FormField({
|
|
|
347
346
|
const [isFocused, setIsFocused] = useState(false)
|
|
348
347
|
const interactive = !isDisabled && !isReadOnly
|
|
349
348
|
|
|
350
|
-
// Ref to the native input so tapping anywhere in the input row (padding,
|
|
351
|
-
// leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
|
|
352
|
-
// "two taps to open the keyboard" issue caused by the row intercepting the
|
|
353
|
-
// initial touch.
|
|
354
|
-
const inputRef = useRef<RNTextInput>(null)
|
|
355
|
-
const focusInput = useCallback(() => {
|
|
356
|
-
if (!interactive) return
|
|
357
|
-
inputRef.current?.focus()
|
|
358
|
-
}, [interactive])
|
|
359
|
-
|
|
360
349
|
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
361
350
|
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
362
351
|
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
@@ -552,11 +541,7 @@ function FormField({
|
|
|
552
541
|
</View>
|
|
553
542
|
)}
|
|
554
543
|
|
|
555
|
-
<
|
|
556
|
-
style={[inputRowStyle, inputStyle]}
|
|
557
|
-
onPress={focusInput}
|
|
558
|
-
accessible={false}
|
|
559
|
-
>
|
|
544
|
+
<View style={[inputRowStyle, inputStyle]}>
|
|
560
545
|
{processedLeading != null && (
|
|
561
546
|
<View
|
|
562
547
|
accessibilityElementsHidden
|
|
@@ -566,7 +551,6 @@ function FormField({
|
|
|
566
551
|
</View>
|
|
567
552
|
)}
|
|
568
553
|
<RNTextInput
|
|
569
|
-
ref={inputRef}
|
|
570
554
|
style={[inputTextStyles, inputTextStyle]}
|
|
571
555
|
value={value ?? ''}
|
|
572
556
|
onChangeText={handleChangeText}
|
|
@@ -594,7 +578,7 @@ function FormField({
|
|
|
594
578
|
{processedTrailing}
|
|
595
579
|
</View>
|
|
596
580
|
)}
|
|
597
|
-
</
|
|
581
|
+
</View>
|
|
598
582
|
|
|
599
583
|
{supportLabel != null && supportLabel !== '' && (
|
|
600
584
|
<SupportText
|
|
@@ -374,6 +374,9 @@ function FullscreenModal({
|
|
|
374
374
|
showsVerticalScrollIndicator={false}
|
|
375
375
|
onScroll={onScroll}
|
|
376
376
|
scrollEventThrottle={16}
|
|
377
|
+
// Tap an input in the body and it focuses on the FIRST tap, even when
|
|
378
|
+
// the keyboard is already open (default 'never' eats that tap).
|
|
379
|
+
keyboardShouldPersistTaps="handled"
|
|
377
380
|
>
|
|
378
381
|
<View style={heroTextRegionStyle}>
|
|
379
382
|
<HeroText
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
type PressableStateCallbackType,
|
|
12
12
|
} from 'react-native'
|
|
13
13
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
14
|
-
import IconCapsule from '../IconCapsule/IconCapsule'
|
|
15
14
|
import NavArrow from '../NavArrow/NavArrow'
|
|
16
15
|
import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
17
16
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
@@ -21,8 +20,8 @@ type ListItemProps = {
|
|
|
21
20
|
title?: string;
|
|
22
21
|
supportText?: string;
|
|
23
22
|
showSupportText?: boolean;
|
|
24
|
-
/** Leading slot (Figma "leading").
|
|
25
|
-
leading?: React.ReactNode;
|
|
23
|
+
/** Leading slot (Figma "leading"). Omitted or `null` renders nothing. */
|
|
24
|
+
leading?: React.ReactNode | null;
|
|
26
25
|
supportSlot?: React.ReactNode;
|
|
27
26
|
/** Trailing slot (Figma "trailing"), e.g. `MoneyValue` or `Button`. Horizontal layout only. */
|
|
28
27
|
trailing?: React.ReactNode;
|
|
@@ -188,7 +187,7 @@ const verticalSupportTextOverride: TextStyle = { textAlign: 'center' }
|
|
|
188
187
|
* @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
|
|
189
188
|
* @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
|
|
190
189
|
* @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
|
|
191
|
-
* @param {React.ReactNode} [props.leading] - Optional leading slot.
|
|
190
|
+
* @param {React.ReactNode|null} [props.leading] - Optional leading slot. Omitted or `null` renders nothing.
|
|
192
191
|
* @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
|
|
193
192
|
* @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
|
|
194
193
|
* @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
|
|
@@ -263,16 +262,15 @@ function ListItemImpl({
|
|
|
263
262
|
// Process leading slot to pass modes to children. Memoized on
|
|
264
263
|
// (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
|
|
265
264
|
const leadingElement = useMemo(() => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (processed.length === 0)
|
|
274
|
-
|
|
275
|
-
}
|
|
265
|
+
if (leading == null) return null
|
|
266
|
+
|
|
267
|
+
const processed = cloneChildrenWithModes(
|
|
268
|
+
React.Children.toArray(leading),
|
|
269
|
+
tokens.resolvedModes,
|
|
270
|
+
SLOT_FORCED_MODES
|
|
271
|
+
)
|
|
272
|
+
if (processed.length === 0) return null
|
|
273
|
+
|
|
276
274
|
return processed.length === 1 ? processed[0] : processed
|
|
277
275
|
}, [leading, tokens.resolvedModes])
|
|
278
276
|
|
|
@@ -373,7 +371,7 @@ function ListItemImpl({
|
|
|
373
371
|
if (layout === 'Horizontal') {
|
|
374
372
|
const innerContent = (
|
|
375
373
|
<View style={innerContentStyleArray}>
|
|
376
|
-
{leadingElement}
|
|
374
|
+
{leadingElement ?? null}
|
|
377
375
|
<View
|
|
378
376
|
style={{
|
|
379
377
|
flex: 1,
|
|
@@ -431,7 +429,7 @@ function ListItemImpl({
|
|
|
431
429
|
// Vertical layout — icon on top, support text/slot below
|
|
432
430
|
const verticalContent = (
|
|
433
431
|
<View style={verticalContentStyleArray}>
|
|
434
|
-
{leadingElement}
|
|
432
|
+
{leadingElement ?? null}
|
|
435
433
|
{renderSupportContent()}
|
|
436
434
|
</View>
|
|
437
435
|
)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React, { useCallback, useMemo,
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
-
Pressable,
|
|
6
5
|
TextInput as RNTextInput,
|
|
7
6
|
type StyleProp,
|
|
8
7
|
type TextInputProps as RNTextInputProps,
|
|
@@ -297,15 +296,6 @@ function MessageField({
|
|
|
297
296
|
const [isFocused, setIsFocused] = useState(false)
|
|
298
297
|
const interactive = !isDisabled && !isReadOnly
|
|
299
298
|
|
|
300
|
-
// Ref to the native textarea so tapping anywhere in the (padded) textarea
|
|
301
|
-
// container focuses it on the FIRST tap, fixing the Android "two taps to
|
|
302
|
-
// open the keyboard" issue.
|
|
303
|
-
const inputRef = useRef<RNTextInput>(null)
|
|
304
|
-
const focusInput = useCallback(() => {
|
|
305
|
-
if (!interactive) return
|
|
306
|
-
inputRef.current?.focus()
|
|
307
|
-
}, [interactive])
|
|
308
|
-
|
|
309
299
|
const { modes: globalModes } = useTokens()
|
|
310
300
|
const baseModes = useMemo(
|
|
311
301
|
() => ({ ...globalModes, ...propModes }),
|
|
@@ -508,13 +498,8 @@ function MessageField({
|
|
|
508
498
|
</View>
|
|
509
499
|
)}
|
|
510
500
|
|
|
511
|
-
<
|
|
512
|
-
style={[textareaContainerStyle, textareaStyle]}
|
|
513
|
-
onPress={focusInput}
|
|
514
|
-
accessible={false}
|
|
515
|
-
>
|
|
501
|
+
<View style={[textareaContainerStyle, textareaStyle]}>
|
|
516
502
|
<RNTextInput
|
|
517
|
-
ref={inputRef}
|
|
518
503
|
multiline
|
|
519
504
|
value={currentValue}
|
|
520
505
|
onChangeText={handleChangeText}
|
|
@@ -529,7 +514,7 @@ function MessageField({
|
|
|
529
514
|
accessibilityHint={accessibilityHint}
|
|
530
515
|
style={[inputTextStyle, inputStyle]}
|
|
531
516
|
/>
|
|
532
|
-
</
|
|
517
|
+
</View>
|
|
533
518
|
|
|
534
519
|
{shouldShowCounter && (
|
|
535
520
|
<Text style={counterTextStyle} accessibilityElementsHidden>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React, { type ReactNode
|
|
1
|
+
import React, { type ReactNode } from 'react'
|
|
2
2
|
import { View, Text, type ViewStyle, type TextStyle } from 'react-native'
|
|
3
3
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
4
|
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
5
|
import IconCapsule from '../IconCapsule/IconCapsule'
|
|
6
|
-
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
6
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
7
7
|
|
|
8
8
|
export type PaymentFeedbackProps = {
|
|
9
9
|
/** Large heading text, typically a monetary value (e.g. "₹50,000") */
|
|
@@ -20,7 +20,11 @@ export type PaymentFeedbackProps = {
|
|
|
20
20
|
iconName?: string
|
|
21
21
|
/** Optional custom media slot that replaces the default IconCapsule */
|
|
22
22
|
renderMedia?: ReactNode
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Mode configuration for design tokens. Also drives the default
|
|
25
|
+
* IconCapsule's color — pass `AppearanceSystem: 'positive' | 'warning' |
|
|
26
|
+
* 'negative'` to render a green/orange/red capsule (defaults to `positive`).
|
|
27
|
+
*/
|
|
24
28
|
modes?: Record<string, any>
|
|
25
29
|
style?: ViewStyle
|
|
26
30
|
}
|
|
@@ -28,7 +32,7 @@ export type PaymentFeedbackProps = {
|
|
|
28
32
|
export default function PaymentFeedback({
|
|
29
33
|
title = '₹50,000',
|
|
30
34
|
subtitle = 'Payment successful',
|
|
31
|
-
body
|
|
35
|
+
body,
|
|
32
36
|
details = '18 March 2025, 4:15 pm\nTransaction ID: TXN121466784',
|
|
33
37
|
showDetails = true,
|
|
34
38
|
iconName = 'ic_confirm',
|
|
@@ -123,14 +127,17 @@ export default function PaymentFeedback({
|
|
|
123
127
|
textAlign: 'center',
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
// Cascade modes into a custom media slot (per the modes-cascade convention);
|
|
131
|
+
// any modes the consumer set on the slot child still take precedence.
|
|
132
|
+
const mediaContent =
|
|
133
|
+
renderMedia != null ? cloneChildrenWithModes(renderMedia, modes) : null
|
|
129
134
|
|
|
130
135
|
const defaultMedia = (
|
|
131
136
|
<IconCapsule
|
|
132
137
|
iconName={iconName}
|
|
133
|
-
|
|
138
|
+
// `positive` is the default; consumers override the capsule color by
|
|
139
|
+
// passing `AppearanceSystem` (or any other mode) via the `modes` prop.
|
|
140
|
+
modes={{ AppearanceSystem: 'positive', ...modes, 'Icon Capsule Size': 'L', Emphasis: 'High', 'Semantic Intent': 'System' }}
|
|
134
141
|
/>
|
|
135
142
|
)
|
|
136
143
|
|