jfs-components 0.0.86 → 0.0.99
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 +8 -0
- package/lib/commonjs/assets.d.js +1 -0
- package/lib/commonjs/components/Drawer/Drawer.js +146 -82
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +118 -51
- package/lib/commonjs/components/Icon/Icon.js +112 -0
- package/lib/commonjs/components/Spinner/Spinner.js +5 -1
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/skeleton/Skeleton.js +10 -2
- package/lib/module/assets.d.js +1 -0
- package/lib/module/components/Drawer/Drawer.js +148 -84
- package/lib/module/components/FullscreenModal/FullscreenModal.js +120 -53
- package/lib/module/components/Icon/Icon.js +106 -0
- package/lib/module/components/Spinner/Spinner.js +6 -2
- package/lib/module/components/index.js +1 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- 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 +35 -21
- package/lib/typescript/src/components/Icon/Icon.d.ts +75 -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/assets.d.ts +24 -0
- package/src/components/Drawer/Drawer.tsx +94 -15
- package/src/components/FullscreenModal/FullscreenModal.tsx +146 -63
- package/src/components/Icon/Icon.tsx +167 -0
- package/src/components/Spinner/Spinner.tsx +2 -2
- package/src/components/index.ts +2 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -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
|
|
@@ -10,18 +10,21 @@ export type FullscreenModalProps = {
|
|
|
10
10
|
/** Secondary line below the supporting paragraph (e.g. a price / timeline). */
|
|
11
11
|
priceText?: string;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* Full-bleed background media for the whole modal. It is pinned to the top
|
|
14
|
+
* and laid out at the full modal width; size it with an aspect ratio
|
|
15
|
+
* (e.g. `<Image ratio={1080 / 4140} />`) so its height follows the width
|
|
16
|
+
* naturally. It renders as a single continuous background BEHIND both the
|
|
17
|
+
* hero text and the body content — there is no separate body box stacked on
|
|
18
|
+
* top of it. Bring any renderer — most commonly an `Image`, but a
|
|
19
|
+
* `LottiePlayer`, `Video`, or `SvgXml` works too. It never intercepts
|
|
20
|
+
* touches and the foreground content scrolls over it (no parallax).
|
|
21
|
+
* `modes` are cascaded into it.
|
|
19
22
|
*/
|
|
20
23
|
heroMedia?: React.ReactNode;
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* Height reserved for the hero text region (eyebrow / headline / supporting
|
|
26
|
+
* / price), whose content is anchored to the bottom. Applies whether or not
|
|
27
|
+
* `heroMedia` is present. Defaults to 420.
|
|
25
28
|
*/
|
|
26
29
|
heroHeight?: number;
|
|
27
30
|
/** Whether to render the floating close button (top-right). Defaults to true. */
|
|
@@ -41,15 +44,17 @@ export type FullscreenModalProps = {
|
|
|
41
44
|
onPrimaryAction?: () => void;
|
|
42
45
|
/** Disclaimer text shown below the default primary action button. */
|
|
43
46
|
disclaimer?: string;
|
|
44
|
-
/** Solid backdrop color for the scrollable body. Defaults to a near-black. */
|
|
45
|
-
backgroundColor?: string;
|
|
46
47
|
/** Body content (typically `Section`s). `modes` are cascaded automatically. */
|
|
47
48
|
children?: React.ReactNode;
|
|
48
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Mode configuration. `Page type` defaults to `'JioPlus'` (overridable here),
|
|
51
|
+
* and `context5` is always forced to `'Fullscreen Modal'` (non-overridable).
|
|
52
|
+
* The resolved modes cascade to the body, hero media, and the `ActionFooter`.
|
|
53
|
+
*/
|
|
49
54
|
modes?: Record<string, any>;
|
|
50
55
|
/** Style overrides for the outer container. */
|
|
51
56
|
style?: StyleProp<ViewStyle>;
|
|
52
|
-
/** Style overrides for the
|
|
57
|
+
/** Style overrides for the transparent body wrapper. */
|
|
53
58
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
54
59
|
testID?: string;
|
|
55
60
|
};
|
|
@@ -64,12 +69,21 @@ export type FullscreenModalProps = {
|
|
|
64
69
|
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
65
70
|
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
66
71
|
*
|
|
67
|
-
* ###
|
|
68
|
-
* The `heroMedia` is
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* ### Background media
|
|
73
|
+
* The `heroMedia` is a single full-bleed background pinned to the top of the
|
|
74
|
+
* modal at the full width and its own natural aspect ratio. It lives at the
|
|
75
|
+
* ROOT — behind both the scrolling content and the (transparent) footer — so
|
|
76
|
+
* it fills the whole surface and is NEVER clipped to the content height. It
|
|
77
|
+
* also contributes ZERO scroll height: the scroll extent is driven purely by
|
|
78
|
+
* the in-flow foreground (hero text + `children`), so the number of body
|
|
79
|
+
* elements dictates how far the surface scrolls. It still scrolls in lockstep
|
|
80
|
+
* WITH the content (the background is translated by the scroll offset), so the
|
|
81
|
+
* content reads as sitting ON one continuous image that moves with it — there
|
|
82
|
+
* is no parallax and no separate solid body box.
|
|
83
|
+
*
|
|
84
|
+
* Pass a background sized to the full width at its natural ratio
|
|
85
|
+
* (e.g. `<Image imageSource={bg} ratio={1080 / 4140} />`). Use an asset at
|
|
86
|
+
* least as tall as the surface so it covers the full modal.
|
|
73
87
|
*
|
|
74
88
|
* @component
|
|
75
89
|
* @example
|
|
@@ -79,7 +93,7 @@ export type FullscreenModalProps = {
|
|
|
79
93
|
* headline="Get more from your money."
|
|
80
94
|
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
81
95
|
* priceText="₹999/year · ₹0 until 2027"
|
|
82
|
-
* heroMedia={<Image imageSource={hero} ratio={
|
|
96
|
+
* heroMedia={<Image imageSource={hero} ratio={1080 / 4140} />}
|
|
83
97
|
* primaryActionLabel="Upgrade for free"
|
|
84
98
|
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
85
99
|
* onPrimaryAction={() => upgrade()}
|
|
@@ -90,6 +104,6 @@ export type FullscreenModalProps = {
|
|
|
90
104
|
* </FullscreenModal>
|
|
91
105
|
* ```
|
|
92
106
|
*/
|
|
93
|
-
declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer,
|
|
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;
|
|
94
108
|
export default FullscreenModal;
|
|
95
109
|
//# sourceMappingURL=FullscreenModal.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type AccessibilityProps, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { UnifiedSource } from '../../utils/MediaSource';
|
|
4
|
+
type IconProps = AccessibilityProps & {
|
|
5
|
+
/**
|
|
6
|
+
* Built-in icon name from the registry, in the `ic_something` format
|
|
7
|
+
* (e.g. `'ic_card'`, `'ic_scan_qr_code'`). When omitted and no `source` or
|
|
8
|
+
* `children` slot is supplied, defaults to `'ic_card'` to match the Figma
|
|
9
|
+
* design's default glyph.
|
|
10
|
+
*/
|
|
11
|
+
iconName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Unified fallback source rendered when `iconName` is missing or not in the
|
|
14
|
+
* registry. Accepts a remote URI, an inline SVG XML string, a `require()`
|
|
15
|
+
* asset, an SVG React component, or an already-rendered element. The media
|
|
16
|
+
* is tinted with the mode-resolved icon color so it follows design tokens
|
|
17
|
+
* just like a built-in icon. See {@link UnifiedSource}.
|
|
18
|
+
*/
|
|
19
|
+
source?: UnifiedSource;
|
|
20
|
+
/**
|
|
21
|
+
* Icon slot. Render any node here (another `Icon`, a custom SVG component,
|
|
22
|
+
* etc.) and it takes precedence over `iconName`/`source`. `modes` cascade
|
|
23
|
+
* into the slotted children automatically.
|
|
24
|
+
*/
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* Override the mode-resolved icon color. When omitted the value comes from
|
|
28
|
+
* the `icon/color` design token.
|
|
29
|
+
*/
|
|
30
|
+
color?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Override the mode-resolved icon size (in px). When omitted the value comes
|
|
33
|
+
* from the `icon/size` design token.
|
|
34
|
+
*/
|
|
35
|
+
size?: number;
|
|
36
|
+
modes?: Record<string, any>;
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Icon component — a design-token-driven wrapper around a single glyph.
|
|
41
|
+
*
|
|
42
|
+
* It mirrors the Figma "Icon" component: a padded, centered container whose
|
|
43
|
+
* color and size are resolved from the `icon/*` design tokens via `modes`.
|
|
44
|
+
* The glyph itself can be supplied three ways, in order of precedence:
|
|
45
|
+
*
|
|
46
|
+
* 1. `children` — a real slot for any node (custom SVG component, nested
|
|
47
|
+
* `Icon`, etc.). `modes` cascade into the slot automatically.
|
|
48
|
+
* 2. `iconName` — a registry icon in the `ic_something` format.
|
|
49
|
+
* 3. `source` — a {@link UnifiedSource} fallback (remote URI, inline SVG XML,
|
|
50
|
+
* `require()` asset, SVG component, or React element), tinted with the
|
|
51
|
+
* mode-resolved icon color.
|
|
52
|
+
*
|
|
53
|
+
* `color` and `size` props let consumers override the token values per
|
|
54
|
+
* instance without touching `modes`.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* // Built-in registry icon (default path).
|
|
59
|
+
* <Icon iconName="ic_card" modes={{ 'Color Mode': 'Light' }} />
|
|
60
|
+
*
|
|
61
|
+
* // Per-instance overrides.
|
|
62
|
+
* <Icon iconName="ic_ccv" color="#5c00b5" size={24} />
|
|
63
|
+
*
|
|
64
|
+
* // Fallback to an external source when the name isn't in the registry.
|
|
65
|
+
* <Icon source="https://cdn.example.com/glyph.svg" />
|
|
66
|
+
*
|
|
67
|
+
* // Slot: render any node as the icon.
|
|
68
|
+
* <Icon><BrandLogo /></Icon>
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function Icon({ iconName, source, children, color, size, modes: propModes, style: styleProp, ...rest }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
72
|
+
declare const _default: React.MemoExoticComponent<typeof Icon>;
|
|
73
|
+
export default _default;
|
|
74
|
+
export type { IconProps };
|
|
75
|
+
//# sourceMappingURL=Icon.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';
|
|
@@ -43,6 +43,7 @@ export { default as MonthlyStatusGrid, CalendarGlyph, type MonthlyStatusGridProp
|
|
|
43
43
|
export { default as Gauge, type GaugeProps } from './Gauge/Gauge';
|
|
44
44
|
export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/HoldingsCard';
|
|
45
45
|
export { default as HStack, type HStackProps } from './HStack/HStack';
|
|
46
|
+
export { default as Icon, type IconProps } from './Icon/Icon';
|
|
46
47
|
export { default as IconButton } from './IconButton/IconButton';
|
|
47
48
|
export { default as IconCapsule } from './IconCapsule/IconCapsule';
|
|
48
49
|
export { default as Image, type ImageProps } from './Image/Image';
|
|
@@ -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-08T14:11:35.036Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
package/src/assets.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare module '*.png' {
|
|
2
|
+
const value: string
|
|
3
|
+
export default value
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module '*.jpg' {
|
|
7
|
+
const value: string
|
|
8
|
+
export default value
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '*.jpeg' {
|
|
12
|
+
const value: string
|
|
13
|
+
export default value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module '*.gif' {
|
|
17
|
+
const value: string
|
|
18
|
+
export default value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module '*.webp' {
|
|
22
|
+
const value: string
|
|
23
|
+
export default value
|
|
24
|
+
}
|
|
@@ -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
|