jfs-components 0.0.95 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/lib/commonjs/components/Drawer/Drawer.js +146 -82
- package/lib/commonjs/components/Dropdown/Dropdown.js +37 -18
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +22 -1
- package/lib/commonjs/components/Spinner/Spinner.js +5 -1
- package/lib/commonjs/components/TestimonialsCard/TestimonialsCard.js +121 -0
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/skeleton/Skeleton.js +10 -2
- package/lib/module/components/Drawer/Drawer.js +148 -84
- package/lib/module/components/Dropdown/Dropdown.js +37 -18
- package/lib/module/components/FullscreenModal/FullscreenModal.js +22 -1
- package/lib/module/components/Spinner/Spinner.js +6 -2
- package/lib/module/components/TestimonialsCard/TestimonialsCard.js +116 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/skeleton/Skeleton.js +11 -3
- package/lib/typescript/src/components/Drawer/Drawer.d.ts +23 -4
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +7 -1
- package/lib/typescript/src/components/TestimonialsCard/TestimonialsCard.d.ts +51 -0
- package/lib/typescript/src/components/index.d.ts +2 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Drawer/Drawer.tsx +94 -15
- package/src/components/Dropdown/Dropdown.tsx +38 -18
- package/src/components/FullscreenModal/FullscreenModal.tsx +29 -2
- package/src/components/Spinner/Spinner.tsx +2 -2
- package/src/components/TestimonialsCard/TestimonialsCard.tsx +162 -0
- package/src/components/index.ts +2 -1
- package/src/icons/registry.ts +1 -1
- package/src/skeleton/Skeleton.tsx +10 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useId, useMemo, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { View } from 'react-native';
|
|
5
5
|
import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated';
|
|
6
6
|
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from 'react-native-svg';
|
|
7
7
|
import { getVariableByName } from '../design-tokens/figma-variables-resolver';
|
|
@@ -16,12 +16,20 @@ const SOLID_OVERLAY_COLOR = 'rgba(255, 255, 255, 1)';
|
|
|
16
16
|
// `pointerEvents: 'none'` lives on the style (not the deprecated prop) so it
|
|
17
17
|
// works on both native and React Native Web without warnings.
|
|
18
18
|
const absoluteFillStyle = {
|
|
19
|
-
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
top: 0,
|
|
21
|
+
left: 0,
|
|
22
|
+
right: 0,
|
|
23
|
+
bottom: 0,
|
|
20
24
|
overflow: 'hidden',
|
|
21
25
|
pointerEvents: 'none'
|
|
22
26
|
};
|
|
23
27
|
const solidOverlayStyle = {
|
|
24
|
-
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
top: 0,
|
|
30
|
+
left: 0,
|
|
31
|
+
right: 0,
|
|
32
|
+
bottom: 0,
|
|
25
33
|
backgroundColor: SOLID_OVERLAY_COLOR
|
|
26
34
|
};
|
|
27
35
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
type DrawerProps = {
|
|
2
|
+
export type DrawerProps = {
|
|
3
3
|
modes?: Record<string, any>;
|
|
4
4
|
style?: import('react-native').StyleProp<import('react-native').ViewStyle>;
|
|
5
5
|
title?: string;
|
|
@@ -11,7 +11,19 @@ type DrawerProps = {
|
|
|
11
11
|
children?: React.ReactNode;
|
|
12
12
|
collapsedHeight?: number;
|
|
13
13
|
expandedRatio?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Sets the drawer state when it first mounts (uncontrolled mode).
|
|
16
|
+
* Ignored once `state` is provided (controlled mode).
|
|
17
|
+
*/
|
|
14
18
|
initialState?: 'collapsed' | 'expanded';
|
|
19
|
+
/**
|
|
20
|
+
* Programmatically controls the drawer's collapsed/expanded state.
|
|
21
|
+
* When provided, the drawer becomes a controlled component: it will
|
|
22
|
+
* animate to match this value (reusing the same spring used by gestures)
|
|
23
|
+
* and gesture-driven changes are reported via `onStateChange` for the
|
|
24
|
+
* parent to apply back. Leave undefined for uncontrolled behaviour.
|
|
25
|
+
*/
|
|
26
|
+
state?: 'collapsed' | 'expanded';
|
|
15
27
|
contentStyle?: any;
|
|
16
28
|
sheetStyle?: any;
|
|
17
29
|
accessibilityLabel?: string;
|
|
@@ -31,9 +43,16 @@ type DrawerProps = {
|
|
|
31
43
|
onStateChange?: (state: 'collapsed' | 'expanded') => void;
|
|
32
44
|
};
|
|
33
45
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
46
|
+
* Imperative handle exposed via `ref` for programmatic control of the drawer.
|
|
47
|
+
* Each method animates using the same spring as gesture-driven transitions.
|
|
36
48
|
*/
|
|
37
|
-
|
|
49
|
+
export type DrawerHandle = {
|
|
50
|
+
expand: () => void;
|
|
51
|
+
collapse: () => void;
|
|
52
|
+
toggle: () => void;
|
|
53
|
+
/** Current settled state of the drawer. */
|
|
54
|
+
getState: () => 'collapsed' | 'expanded';
|
|
55
|
+
};
|
|
56
|
+
declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<DrawerHandle>>;
|
|
38
57
|
export default Drawer;
|
|
39
58
|
//# sourceMappingURL=Drawer.d.ts.map
|
|
@@ -29,6 +29,12 @@ export type FullscreenModalProps = {
|
|
|
29
29
|
heroHeight?: number;
|
|
30
30
|
/** Whether to render the floating close button (top-right). Defaults to true. */
|
|
31
31
|
showClose?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Additional vertical offset (in px) applied to the floating close button's
|
|
34
|
+
* top position, on top of the base inset (`12 + safe-area top`). Positive
|
|
35
|
+
* values push it further down. Defaults to 0.
|
|
36
|
+
*/
|
|
37
|
+
closeOffsetY?: number;
|
|
32
38
|
/** Press handler for the close button. */
|
|
33
39
|
onClose?: () => void;
|
|
34
40
|
/** Accessibility label for the close button. */
|
|
@@ -104,6 +110,6 @@ export type FullscreenModalProps = {
|
|
|
104
110
|
* </FullscreenModal>
|
|
105
111
|
* ```
|
|
106
112
|
*/
|
|
107
|
-
declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
|
|
113
|
+
declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, closeOffsetY, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
|
|
108
114
|
export default FullscreenModal;
|
|
109
115
|
//# sourceMappingURL=FullscreenModal.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ViewStyle, type StyleProp } from 'react-native';
|
|
3
|
+
import { type AvatarProps } from '../Avatar/Avatar';
|
|
4
|
+
export type TestimonialsCardProps = {
|
|
5
|
+
/**
|
|
6
|
+
* The testimonial heading, typically the author's name.
|
|
7
|
+
*/
|
|
8
|
+
title?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The testimonial body copy.
|
|
11
|
+
*/
|
|
12
|
+
body?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Mode configuration passed to the token resolver. Also forwarded to the
|
|
15
|
+
* Avatar child for consistent theming.
|
|
16
|
+
*/
|
|
17
|
+
modes?: Record<string, any>;
|
|
18
|
+
/**
|
|
19
|
+
* Optional style overrides for the card container.
|
|
20
|
+
*/
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
/**
|
|
23
|
+
* Props forwarded to the Avatar component (e.g. a custom `imageSource`).
|
|
24
|
+
*/
|
|
25
|
+
avatarProps?: Partial<AvatarProps>;
|
|
26
|
+
/**
|
|
27
|
+
* Accessibility label for the card region. Falls back to a label generated
|
|
28
|
+
* from the title and body.
|
|
29
|
+
*/
|
|
30
|
+
accessibilityLabel?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* TestimonialsCard renders a compact, fixed-width card with a circular avatar,
|
|
34
|
+
* a bold title, and a body paragraph. It is typically used inside a horizontal
|
|
35
|
+
* carousel of customer testimonials.
|
|
36
|
+
*
|
|
37
|
+
* All styling values are resolved from Figma design tokens using the provided
|
|
38
|
+
* `modes`.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <TestimonialsCard
|
|
43
|
+
* title="Aarav S."
|
|
44
|
+
* body="I was dreading renewing my car insurance, but JioFinance made it a breeze."
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare function TestimonialsCard({ title, body, modes, style, avatarProps, accessibilityLabel, }: TestimonialsCardProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
declare const _default: React.MemoExoticComponent<typeof TestimonialsCard>;
|
|
50
|
+
export default _default;
|
|
51
|
+
//# sourceMappingURL=TestimonialsCard.d.ts.map
|
|
@@ -22,7 +22,7 @@ export { default as CardFinancialCondition, type CardFinancialConditionProps } f
|
|
|
22
22
|
export { default as CardInsight, type CardInsightProps } from './CardInsight/CardInsight';
|
|
23
23
|
export { default as Disclaimer } from './Disclaimer/Disclaimer';
|
|
24
24
|
export { default as Divider, type DividerProps, type DividerDirection } from './Divider/Divider';
|
|
25
|
-
export { default as Drawer } from './Drawer/Drawer';
|
|
25
|
+
export { default as Drawer, type DrawerProps, type DrawerHandle } from './Drawer/Drawer';
|
|
26
26
|
export { default as Dropdown, DropdownItem, type DropdownProps, type DropdownItemProps } from './Dropdown/Dropdown';
|
|
27
27
|
export { default as DropdownInput, type DropdownInputProps, type DropdownInputOption, type DropdownInputOptionValue } from './DropdownInput/DropdownInput';
|
|
28
28
|
export { default as SuggestiveSearch, type SuggestiveSearchProps, type SuggestiveSearchOption, type SuggestiveSearchOptionValue, type SuggestiveSearchItem } from './SuggestiveSearch/SuggestiveSearch';
|
|
@@ -130,6 +130,7 @@ export { default as StatItem, type StatItemProps, type StatItemLabelPosition } f
|
|
|
130
130
|
export { default as StatGroup, type StatGroupProps, type StatGroupItem } from './StatGroup/StatGroup';
|
|
131
131
|
export { default as StrengthIndicator, type StrengthIndicatorProps, type StrengthIndicatorConfidence, type StrengthIndicatorConfidenceValue } from './StrengthIndicator/StrengthIndicator';
|
|
132
132
|
export { default as SummaryTile, type SummaryTileProps } from './SummaryTile/SummaryTile';
|
|
133
|
+
export { default as TestimonialsCard, type TestimonialsCardProps } from './TestimonialsCard/TestimonialsCard';
|
|
133
134
|
export { default as Text, type TextProps } from './Text/Text';
|
|
134
135
|
export { default as SegmentedControl, type SegmentedControlProps, type SegmentedControlItem } from './SegmentedControl/SegmentedControl';
|
|
135
136
|
export { default as Toggle, type ToggleProps } from './Toggle/Toggle';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Auto-generated from SVG files in src/icons/
|
|
5
5
|
* DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
|
|
6
6
|
*
|
|
7
|
-
* Generated: 2026-06-
|
|
7
|
+
* Generated: 2026-06-09T13:55:40.250Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React, { useCallback, useEffect,
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
|
2
2
|
import { Platform, StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'
|
|
3
3
|
import {
|
|
4
4
|
Gesture,
|
|
5
5
|
GestureDetector,
|
|
6
|
-
GestureHandlerRootView,
|
|
7
6
|
ScrollView,
|
|
8
7
|
} from 'react-native-gesture-handler'
|
|
9
8
|
import Animated, {
|
|
@@ -53,7 +52,7 @@ function rubberBand(value: number, min: number, max: number, friction: number =
|
|
|
53
52
|
return value
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
type DrawerProps = {
|
|
55
|
+
export type DrawerProps = {
|
|
57
56
|
|
|
58
57
|
modes?: Record<string, any>
|
|
59
58
|
style?: import('react-native').StyleProp<import('react-native').ViewStyle>
|
|
@@ -66,7 +65,19 @@ type DrawerProps = {
|
|
|
66
65
|
children?: React.ReactNode
|
|
67
66
|
collapsedHeight?: number
|
|
68
67
|
expandedRatio?: number
|
|
68
|
+
/**
|
|
69
|
+
* Sets the drawer state when it first mounts (uncontrolled mode).
|
|
70
|
+
* Ignored once `state` is provided (controlled mode).
|
|
71
|
+
*/
|
|
69
72
|
initialState?: 'collapsed' | 'expanded'
|
|
73
|
+
/**
|
|
74
|
+
* Programmatically controls the drawer's collapsed/expanded state.
|
|
75
|
+
* When provided, the drawer becomes a controlled component: it will
|
|
76
|
+
* animate to match this value (reusing the same spring used by gestures)
|
|
77
|
+
* and gesture-driven changes are reported via `onStateChange` for the
|
|
78
|
+
* parent to apply back. Leave undefined for uncontrolled behaviour.
|
|
79
|
+
*/
|
|
80
|
+
state?: 'collapsed' | 'expanded'
|
|
70
81
|
contentStyle?: any
|
|
71
82
|
sheetStyle?: any
|
|
72
83
|
accessibilityLabel?: string
|
|
@@ -86,11 +97,23 @@ type DrawerProps = {
|
|
|
86
97
|
onStateChange?: (state: 'collapsed' | 'expanded') => void
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Imperative handle exposed via `ref` for programmatic control of the drawer.
|
|
102
|
+
* Each method animates using the same spring as gesture-driven transitions.
|
|
103
|
+
*/
|
|
104
|
+
export type DrawerHandle = {
|
|
105
|
+
expand: () => void
|
|
106
|
+
collapse: () => void
|
|
107
|
+
toggle: () => void
|
|
108
|
+
/** Current settled state of the drawer. */
|
|
109
|
+
getState: () => 'collapsed' | 'expanded'
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
/**
|
|
90
113
|
* Drawer component with nested scrolling support.
|
|
91
114
|
* Uses react-native-gesture-handler and react-native-reanimated.
|
|
92
115
|
*/
|
|
93
|
-
function
|
|
116
|
+
function DrawerInner({
|
|
94
117
|
|
|
95
118
|
modes = EMPTY_MODES,
|
|
96
119
|
style,
|
|
@@ -100,6 +123,7 @@ function Drawer({
|
|
|
100
123
|
collapsedHeight = 200,
|
|
101
124
|
expandedRatio = 0.90,
|
|
102
125
|
initialState = 'collapsed',
|
|
126
|
+
state,
|
|
103
127
|
contentStyle,
|
|
104
128
|
sheetStyle,
|
|
105
129
|
accessibilityLabel,
|
|
@@ -108,7 +132,7 @@ function Drawer({
|
|
|
108
132
|
showsVerticalScrollIndicator = false,
|
|
109
133
|
bottomInset = 80,
|
|
110
134
|
onStateChange,
|
|
111
|
-
}: DrawerProps) {
|
|
135
|
+
}: DrawerProps, ref: React.Ref<DrawerHandle>) {
|
|
112
136
|
const { height: screenHeight } = useWindowDimensions()
|
|
113
137
|
|
|
114
138
|
// Calculate snap points
|
|
@@ -168,15 +192,58 @@ function Drawer({
|
|
|
168
192
|
translateY.value = withSpring(destination, SPRING_CONFIG)
|
|
169
193
|
}, [translateY])
|
|
170
194
|
|
|
171
|
-
// Update JS
|
|
195
|
+
// Update the JS-side mode. Pure: only schedules the state update. Side
|
|
196
|
+
// effects (notifying the parent via onStateChange) MUST NOT live inside the
|
|
197
|
+
// setState updater — doing so calls the parent's setState during React's
|
|
198
|
+
// render phase, which throws "Cannot update a component while rendering a
|
|
199
|
+
// different component" and causes an infinite update loop. We report changes
|
|
200
|
+
// from a dedicated effect below instead.
|
|
172
201
|
const updateMode = useCallback((newMode: 'collapsed' | 'expanded') => {
|
|
173
|
-
setMode(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
setMode(newMode)
|
|
203
|
+
}, [])
|
|
204
|
+
|
|
205
|
+
// Notify the parent exactly once per settled mode change, after commit.
|
|
206
|
+
const reportedModeRef = useRef(mode)
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (reportedModeRef.current !== mode) {
|
|
209
|
+
reportedModeRef.current = mode
|
|
210
|
+
onStateChange?.(mode)
|
|
211
|
+
}
|
|
212
|
+
}, [mode, onStateChange])
|
|
213
|
+
|
|
214
|
+
// Programmatic transition (JS thread). Reuses the same spring as gestures:
|
|
215
|
+
// animates `translateY`, keeps the scroll-gate (`isFullyExpanded`) in sync,
|
|
216
|
+
// and updates `mode` (which notifies via the onStateChange effect above).
|
|
217
|
+
const applyMode = useCallback((newMode: 'collapsed' | 'expanded') => {
|
|
218
|
+
const target = newMode === 'expanded' ? minTranslateY : maxTranslateY
|
|
219
|
+
translateY.value = withSpring(target, SPRING_CONFIG)
|
|
220
|
+
isFullyExpanded.value = newMode === 'expanded'
|
|
221
|
+
setMode(newMode)
|
|
222
|
+
}, [minTranslateY, maxTranslateY, translateY, isFullyExpanded])
|
|
223
|
+
|
|
224
|
+
// Controlled mode: react ONLY to genuine changes of the `state` prop, tracked
|
|
225
|
+
// against its previous value. We must NOT reconcile `state` against the
|
|
226
|
+
// internal `mode` on every render: a gesture updates `mode` optimistically
|
|
227
|
+
// one render before the parent echoes it back through `onStateChange` into
|
|
228
|
+
// `state`, so a `state !== mode` check would "correct" the gesture and fight
|
|
229
|
+
// the echo, ping-ponging forever (Maximum update depth exceeded).
|
|
230
|
+
// `applyMode` is idempotent, so re-applying the already-current mode is a
|
|
231
|
+
// no-op when the parent simply mirrors our own change back.
|
|
232
|
+
const prevStateProp = useRef(state)
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (state === undefined) return
|
|
235
|
+
if (state === prevStateProp.current) return
|
|
236
|
+
prevStateProp.current = state
|
|
237
|
+
applyMode(state)
|
|
238
|
+
}, [state, applyMode])
|
|
239
|
+
|
|
240
|
+
// Imperative API for parents holding a ref.
|
|
241
|
+
useImperativeHandle(ref, () => ({
|
|
242
|
+
expand: () => applyMode('expanded'),
|
|
243
|
+
collapse: () => applyMode('collapsed'),
|
|
244
|
+
toggle: () => applyMode(mode === 'expanded' ? 'collapsed' : 'expanded'),
|
|
245
|
+
getState: () => mode,
|
|
246
|
+
}), [applyMode, mode])
|
|
180
247
|
|
|
181
248
|
// Gesture policy:
|
|
182
249
|
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
@@ -362,7 +429,16 @@ function Drawer({
|
|
|
362
429
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer'
|
|
363
430
|
|
|
364
431
|
return (
|
|
365
|
-
|
|
432
|
+
// IMPORTANT: the host is a plain box-none View, NOT a GestureHandlerRootView.
|
|
433
|
+
// On Android, GestureHandlerRootView renders a *native* root view that
|
|
434
|
+
// intercepts every touch within its bounds (to feed the gesture system) and
|
|
435
|
+
// does NOT honor pointerEvents="box-none". Because this host fills the whole
|
|
436
|
+
// screen as an overlay, using GestureHandlerRootView here swallowed all
|
|
437
|
+
// touches to content rendered behind the drawer (buttons, page content).
|
|
438
|
+
// Per the standard react-native-gesture-handler architecture, a single
|
|
439
|
+
// GestureHandlerRootView must wrap the app root; this overlay only needs to
|
|
440
|
+
// let touches fall through where the sheet isn't.
|
|
441
|
+
<View style={[styles.host, style]} pointerEvents="box-none">
|
|
366
442
|
<GestureDetector gesture={gesture}>
|
|
367
443
|
<Animated.View
|
|
368
444
|
style={[
|
|
@@ -457,7 +533,7 @@ function Drawer({
|
|
|
457
533
|
</View>
|
|
458
534
|
</Animated.View>
|
|
459
535
|
</GestureDetector>
|
|
460
|
-
</
|
|
536
|
+
</View>
|
|
461
537
|
)
|
|
462
538
|
}
|
|
463
539
|
|
|
@@ -492,4 +568,7 @@ const styles = StyleSheet.create({
|
|
|
492
568
|
},
|
|
493
569
|
})
|
|
494
570
|
|
|
571
|
+
const Drawer = forwardRef<DrawerHandle, DrawerProps>(DrawerInner)
|
|
572
|
+
Drawer.displayName = 'Drawer'
|
|
573
|
+
|
|
495
574
|
export default Drawer
|
|
@@ -290,15 +290,33 @@ export function Dropdown({
|
|
|
290
290
|
const shadowBlur =
|
|
291
291
|
parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
// Shadow lives on the OUTER view, which must NOT set `overflow: 'hidden'`.
|
|
294
|
+
// On native, clipping a view also clips its shadow (iOS clips the layer
|
|
295
|
+
// shadow; Android clips the elevation shadow), so the soft popup shadow
|
|
296
|
+
// that renders fine on web (CSS box-shadow paints outside the box) would
|
|
297
|
+
// get cut off. The rounded-corner clipping is moved to a separate inner
|
|
298
|
+
// view below.
|
|
299
|
+
//
|
|
300
|
+
// The `boxShadow` style prop (RN 0.76+ / react-native-web) is used as the
|
|
301
|
+
// single source of truth so the designed color/offset/blur are honored on
|
|
302
|
+
// iOS, Android AND web. We intentionally do NOT also set the legacy
|
|
303
|
+
// `shadow*` / `elevation` props: on the new architecture (and web) those
|
|
304
|
+
// are translated into a box-shadow internally, so combining them with an
|
|
305
|
+
// explicit `boxShadow` would stack two shadows. Android's legacy
|
|
306
|
+
// `elevation` is also undesirable here because it ignores the shadow color
|
|
307
|
+
// and only paints a generic gray shadow.
|
|
308
|
+
const shadowStyle: ViewStyle & { boxShadow?: string } = {
|
|
294
309
|
backgroundColor: background,
|
|
310
|
+
borderRadius: radius,
|
|
311
|
+
boxShadow: `${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px 0px ${shadowColor}`,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Inner view carries the rounded corners + clipping so list/scroll content
|
|
315
|
+
// stays inside the radius without affecting the outer view's shadow.
|
|
316
|
+
const clipStyle: ViewStyle = {
|
|
295
317
|
borderRadius: radius,
|
|
296
318
|
overflow: 'hidden',
|
|
297
|
-
|
|
298
|
-
shadowOffset: { width: shadowOffsetX, height: shadowOffsetY },
|
|
299
|
-
shadowOpacity: 1,
|
|
300
|
-
shadowRadius: shadowBlur / 2,
|
|
301
|
-
elevation: 4,
|
|
319
|
+
backgroundColor: background,
|
|
302
320
|
}
|
|
303
321
|
|
|
304
322
|
const content = (
|
|
@@ -309,21 +327,23 @@ export function Dropdown({
|
|
|
309
327
|
|
|
310
328
|
return (
|
|
311
329
|
<View
|
|
312
|
-
style={[
|
|
330
|
+
style={[shadowStyle, style]}
|
|
313
331
|
accessibilityRole="menu"
|
|
314
332
|
accessibilityLabel={accessibilityLabel || 'Dropdown menu'}
|
|
315
333
|
>
|
|
316
|
-
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
<View style={clipStyle}>
|
|
335
|
+
{maxHeight != null ? (
|
|
336
|
+
<ScrollView
|
|
337
|
+
style={{ maxHeight }}
|
|
338
|
+
showsVerticalScrollIndicator={true}
|
|
339
|
+
keyboardShouldPersistTaps="handled"
|
|
340
|
+
>
|
|
341
|
+
{content}
|
|
342
|
+
</ScrollView>
|
|
343
|
+
) : (
|
|
344
|
+
content
|
|
345
|
+
)}
|
|
346
|
+
</View>
|
|
327
347
|
</View>
|
|
328
348
|
)
|
|
329
349
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ViewStyle,
|
|
8
8
|
type TextStyle,
|
|
9
9
|
} from 'react-native'
|
|
10
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
10
11
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
11
12
|
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
12
13
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
@@ -70,6 +71,12 @@ export type FullscreenModalProps = {
|
|
|
70
71
|
heroHeight?: number
|
|
71
72
|
/** Whether to render the floating close button (top-right). Defaults to true. */
|
|
72
73
|
showClose?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* Additional vertical offset (in px) applied to the floating close button's
|
|
76
|
+
* top position, on top of the base inset (`12 + safe-area top`). Positive
|
|
77
|
+
* values push it further down. Defaults to 0.
|
|
78
|
+
*/
|
|
79
|
+
closeOffsetY?: number
|
|
73
80
|
/** Press handler for the close button. */
|
|
74
81
|
onClose?: () => void
|
|
75
82
|
/** Accessibility label for the close button. */
|
|
@@ -229,6 +236,7 @@ function FullscreenModal({
|
|
|
229
236
|
heroMedia,
|
|
230
237
|
heroHeight = 420,
|
|
231
238
|
showClose = true,
|
|
239
|
+
closeOffsetY = 0,
|
|
232
240
|
onClose,
|
|
233
241
|
closeAccessibilityLabel = 'Close',
|
|
234
242
|
footer,
|
|
@@ -260,6 +268,23 @@ function FullscreenModal({
|
|
|
260
268
|
|
|
261
269
|
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16
|
|
262
270
|
|
|
271
|
+
// Safe-area insets so the floating chrome clears the system bars: the close
|
|
272
|
+
// button drops below the status bar / notch, and the sticky footer keeps its
|
|
273
|
+
// designed bottom padding ON TOP of the bottom inset (home indicator /
|
|
274
|
+
// Android gesture or nav bar). On web — and anywhere without a
|
|
275
|
+
// SafeAreaProvider — every inset is 0, so the layout is unchanged.
|
|
276
|
+
const insets = useSafeAreaInsets()
|
|
277
|
+
const closeButtonInsetStyle = useMemo<ViewStyle>(
|
|
278
|
+
() => ({ top: 12 + insets.top + closeOffsetY }),
|
|
279
|
+
[insets.top, closeOffsetY]
|
|
280
|
+
)
|
|
281
|
+
// Extend (not replace) the footer's token bottom padding by the bottom inset
|
|
282
|
+
// so the action button never sits under the system navigation area.
|
|
283
|
+
const footerInsetStyle = useMemo<ViewStyle>(() => {
|
|
284
|
+
const base = Number(getVariableByName('actionFooter/padding/bottom', modes)) || 41
|
|
285
|
+
return { paddingBottom: base + insets.bottom }
|
|
286
|
+
}, [modes, insets.bottom])
|
|
287
|
+
|
|
263
288
|
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
264
289
|
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
265
290
|
// behind the transparent footer), but we translate it up by the exact scroll
|
|
@@ -388,7 +413,9 @@ function FullscreenModal({
|
|
|
388
413
|
</Animated.ScrollView>
|
|
389
414
|
|
|
390
415
|
{footerContent ? (
|
|
391
|
-
<ActionFooter modes={modes}
|
|
416
|
+
<ActionFooter modes={modes} style={footerInsetStyle}>
|
|
417
|
+
{footerContent}
|
|
418
|
+
</ActionFooter>
|
|
392
419
|
) : null}
|
|
393
420
|
|
|
394
421
|
{showClose ? (
|
|
@@ -396,7 +423,7 @@ function FullscreenModal({
|
|
|
396
423
|
iconName="ic_close"
|
|
397
424
|
modes={modes}
|
|
398
425
|
accessibilityLabel={closeAccessibilityLabel}
|
|
399
|
-
style={closeButtonStyle}
|
|
426
|
+
style={[closeButtonStyle, closeButtonInsetStyle]}
|
|
400
427
|
{...(onClose ? { onPress: onClose } : {})}
|
|
401
428
|
/>
|
|
402
429
|
) : null}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native'
|
|
3
3
|
import Animated, {
|
|
4
4
|
Easing,
|
|
5
5
|
cancelAnimation,
|
|
@@ -171,7 +171,7 @@ const useSegmentRotation = (
|
|
|
171
171
|
}
|
|
172
172
|
}, [gravity, index, spreadMinRad, spreadMaxRad, spreadOutFrac])
|
|
173
173
|
|
|
174
|
-
const fullSize: ViewStyle = {
|
|
174
|
+
const fullSize: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }
|
|
175
175
|
|
|
176
176
|
function Spinner({
|
|
177
177
|
size = DEFAULT_SIZE,
|