jfs-components 0.0.95 → 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/lib/commonjs/components/Drawer/Drawer.js +146 -82
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +21 -1
- package/lib/commonjs/components/Spinner/Spinner.js +5 -1
- 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/FullscreenModal/FullscreenModal.js +21 -1
- package/lib/module/components/Spinner/Spinner.js +6 -2
- 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/index.d.ts +1 -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/FullscreenModal/FullscreenModal.tsx +22 -2
- package/src/components/Spinner/Spinner.tsx +2 -2
- package/src/components/index.ts +1 -1
- package/src/icons/registry.ts +1 -1
- package/src/skeleton/Skeleton.tsx +10 -3
|
@@ -45,11 +45,17 @@ function rubberBand(value, min, max, friction = 0.55) {
|
|
|
45
45
|
}
|
|
46
46
|
return value;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Imperative handle exposed via `ref` for programmatic control of the drawer.
|
|
51
|
+
* Each method animates using the same spring as gesture-driven transitions.
|
|
52
|
+
*/
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Drawer component with nested scrolling support.
|
|
50
56
|
* Uses react-native-gesture-handler and react-native-reanimated.
|
|
51
57
|
*/
|
|
52
|
-
function
|
|
58
|
+
function DrawerInner({
|
|
53
59
|
modes = _reactUtils.EMPTY_MODES,
|
|
54
60
|
style,
|
|
55
61
|
title,
|
|
@@ -58,6 +64,7 @@ function Drawer({
|
|
|
58
64
|
collapsedHeight = 200,
|
|
59
65
|
expandedRatio = 0.90,
|
|
60
66
|
initialState = 'collapsed',
|
|
67
|
+
state,
|
|
61
68
|
contentStyle,
|
|
62
69
|
sheetStyle,
|
|
63
70
|
accessibilityLabel,
|
|
@@ -66,7 +73,7 @@ function Drawer({
|
|
|
66
73
|
showsVerticalScrollIndicator = false,
|
|
67
74
|
bottomInset = 80,
|
|
68
75
|
onStateChange
|
|
69
|
-
}) {
|
|
76
|
+
}, ref) {
|
|
70
77
|
const {
|
|
71
78
|
height: screenHeight
|
|
72
79
|
} = (0, _reactNative.useWindowDimensions)();
|
|
@@ -128,15 +135,58 @@ function Drawer({
|
|
|
128
135
|
translateY.value = (0, _reactNativeReanimated.withSpring)(destination, SPRING_CONFIG);
|
|
129
136
|
}, [translateY]);
|
|
130
137
|
|
|
131
|
-
// Update JS
|
|
138
|
+
// Update the JS-side mode. Pure: only schedules the state update. Side
|
|
139
|
+
// effects (notifying the parent via onStateChange) MUST NOT live inside the
|
|
140
|
+
// setState updater — doing so calls the parent's setState during React's
|
|
141
|
+
// render phase, which throws "Cannot update a component while rendering a
|
|
142
|
+
// different component" and causes an infinite update loop. We report changes
|
|
143
|
+
// from a dedicated effect below instead.
|
|
132
144
|
const updateMode = (0, _react.useCallback)(newMode => {
|
|
133
|
-
setMode(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
setMode(newMode);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
// Notify the parent exactly once per settled mode change, after commit.
|
|
149
|
+
const reportedModeRef = (0, _react.useRef)(mode);
|
|
150
|
+
(0, _react.useEffect)(() => {
|
|
151
|
+
if (reportedModeRef.current !== mode) {
|
|
152
|
+
reportedModeRef.current = mode;
|
|
153
|
+
onStateChange?.(mode);
|
|
154
|
+
}
|
|
155
|
+
}, [mode, onStateChange]);
|
|
156
|
+
|
|
157
|
+
// Programmatic transition (JS thread). Reuses the same spring as gestures:
|
|
158
|
+
// animates `translateY`, keeps the scroll-gate (`isFullyExpanded`) in sync,
|
|
159
|
+
// and updates `mode` (which notifies via the onStateChange effect above).
|
|
160
|
+
const applyMode = (0, _react.useCallback)(newMode => {
|
|
161
|
+
const target = newMode === 'expanded' ? minTranslateY : maxTranslateY;
|
|
162
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(target, SPRING_CONFIG);
|
|
163
|
+
isFullyExpanded.value = newMode === 'expanded';
|
|
164
|
+
setMode(newMode);
|
|
165
|
+
}, [minTranslateY, maxTranslateY, translateY, isFullyExpanded]);
|
|
166
|
+
|
|
167
|
+
// Controlled mode: react ONLY to genuine changes of the `state` prop, tracked
|
|
168
|
+
// against its previous value. We must NOT reconcile `state` against the
|
|
169
|
+
// internal `mode` on every render: a gesture updates `mode` optimistically
|
|
170
|
+
// one render before the parent echoes it back through `onStateChange` into
|
|
171
|
+
// `state`, so a `state !== mode` check would "correct" the gesture and fight
|
|
172
|
+
// the echo, ping-ponging forever (Maximum update depth exceeded).
|
|
173
|
+
// `applyMode` is idempotent, so re-applying the already-current mode is a
|
|
174
|
+
// no-op when the parent simply mirrors our own change back.
|
|
175
|
+
const prevStateProp = (0, _react.useRef)(state);
|
|
176
|
+
(0, _react.useEffect)(() => {
|
|
177
|
+
if (state === undefined) return;
|
|
178
|
+
if (state === prevStateProp.current) return;
|
|
179
|
+
prevStateProp.current = state;
|
|
180
|
+
applyMode(state);
|
|
181
|
+
}, [state, applyMode]);
|
|
182
|
+
|
|
183
|
+
// Imperative API for parents holding a ref.
|
|
184
|
+
(0, _react.useImperativeHandle)(ref, () => ({
|
|
185
|
+
expand: () => applyMode('expanded'),
|
|
186
|
+
collapse: () => applyMode('collapsed'),
|
|
187
|
+
toggle: () => applyMode(mode === 'expanded' ? 'collapsed' : 'expanded'),
|
|
188
|
+
getState: () => mode
|
|
189
|
+
}), [applyMode, mode]);
|
|
140
190
|
|
|
141
191
|
// Gesture policy:
|
|
142
192
|
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
@@ -309,84 +359,96 @@ function Drawer({
|
|
|
309
359
|
default: {}
|
|
310
360
|
});
|
|
311
361
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
return (
|
|
363
|
+
/*#__PURE__*/
|
|
364
|
+
// IMPORTANT: the host is a plain box-none View, NOT a GestureHandlerRootView.
|
|
365
|
+
// On Android, GestureHandlerRootView renders a *native* root view that
|
|
366
|
+
// intercepts every touch within its bounds (to feed the gesture system) and
|
|
367
|
+
// does NOT honor pointerEvents="box-none". Because this host fills the whole
|
|
368
|
+
// screen as an overlay, using GestureHandlerRootView here swallowed all
|
|
369
|
+
// touches to content rendered behind the drawer (buttons, page content).
|
|
370
|
+
// Per the standard react-native-gesture-handler architecture, a single
|
|
371
|
+
// GestureHandlerRootView must wrap the app root; this overlay only needs to
|
|
372
|
+
// let touches fall through where the sheet isn't.
|
|
373
|
+
(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
374
|
+
style: [styles.host, style],
|
|
375
|
+
pointerEvents: "box-none",
|
|
376
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
|
|
377
|
+
gesture: gesture,
|
|
378
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
379
|
+
style: [styles.sheet, {
|
|
380
|
+
// Constraint the height strictly to the expanded height
|
|
381
|
+
// This ensures the ScrollView has a finite frame to scroll within
|
|
382
|
+
height: expandedHeight,
|
|
383
|
+
backgroundColor,
|
|
334
384
|
borderTopLeftRadius: radius,
|
|
335
|
-
borderTopRightRadius: radius
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
385
|
+
borderTopRightRadius: radius
|
|
386
|
+
}, shadowStyle, sheetStyle, animatedStyle],
|
|
387
|
+
accessible: true,
|
|
388
|
+
...(_reactNative.Platform.OS === 'web' ? {
|
|
389
|
+
accessibilityRole: 'dialog'
|
|
390
|
+
} : undefined),
|
|
391
|
+
accessibilityLabel: undefined,
|
|
392
|
+
accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
|
|
393
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
394
|
+
style: [styles.sheetInner, {
|
|
395
|
+
borderTopLeftRadius: radius,
|
|
396
|
+
borderTopRightRadius: radius,
|
|
397
|
+
paddingLeft,
|
|
398
|
+
paddingRight,
|
|
399
|
+
paddingBottom,
|
|
400
|
+
rowGap: drawerGap
|
|
344
401
|
}],
|
|
345
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
402
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
403
|
+
style: [styles.handleArea, !title && !header && {
|
|
404
|
+
paddingBottom: 0
|
|
405
|
+
}],
|
|
406
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
407
|
+
style: [{
|
|
408
|
+
backgroundColor: handleColor,
|
|
409
|
+
width: handleWidth,
|
|
410
|
+
height: handleHeight,
|
|
411
|
+
borderRadius: handleRadius
|
|
412
|
+
}]
|
|
413
|
+
})
|
|
414
|
+
}), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
346
415
|
style: [{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
overScrollMode: "always",
|
|
380
|
-
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
381
|
-
scrollY.value = event.contentOffset.y;
|
|
382
|
-
}),
|
|
383
|
-
scrollEventThrottle: 16,
|
|
384
|
-
children: children
|
|
385
|
-
})]
|
|
416
|
+
color: titleColor,
|
|
417
|
+
fontSize: titleSize,
|
|
418
|
+
fontWeight: titleWeight,
|
|
419
|
+
lineHeight: titleLineHeight,
|
|
420
|
+
marginBottom: titlePaddingBottom
|
|
421
|
+
}],
|
|
422
|
+
children: title
|
|
423
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
|
|
424
|
+
ref: scrollRef,
|
|
425
|
+
style: [styles.content, contentStyle],
|
|
426
|
+
contentContainerStyle: [{
|
|
427
|
+
paddingBottom: paddingBottom + bottomInset,
|
|
428
|
+
gap: drawerGap,
|
|
429
|
+
flexDirection: 'column',
|
|
430
|
+
alignItems: 'stretch'
|
|
431
|
+
}, contentContainerStyle],
|
|
432
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator
|
|
433
|
+
// Let a tap on an input inside the sheet focus it on the FIRST tap
|
|
434
|
+
// even while the keyboard is already open (default 'never' would
|
|
435
|
+
// eat that tap just to dismiss the keyboard).
|
|
436
|
+
,
|
|
437
|
+
keyboardShouldPersistTaps: "handled",
|
|
438
|
+
animatedProps: animatedScrollProps,
|
|
439
|
+
alwaysBounceVertical: false,
|
|
440
|
+
overScrollMode: "always",
|
|
441
|
+
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
442
|
+
scrollY.value = event.contentOffset.y;
|
|
443
|
+
}),
|
|
444
|
+
scrollEventThrottle: 16,
|
|
445
|
+
children: children
|
|
446
|
+
})]
|
|
447
|
+
})
|
|
386
448
|
})
|
|
387
449
|
})
|
|
388
450
|
})
|
|
389
|
-
|
|
451
|
+
);
|
|
390
452
|
}
|
|
391
453
|
const styles = _reactNative.StyleSheet.create({
|
|
392
454
|
host: {
|
|
@@ -416,4 +478,6 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
416
478
|
flex: 1
|
|
417
479
|
}
|
|
418
480
|
});
|
|
481
|
+
const Drawer = /*#__PURE__*/(0, _react.forwardRef)(DrawerInner);
|
|
482
|
+
Drawer.displayName = 'Drawer';
|
|
419
483
|
var _default = exports.default = Drawer;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
|
+
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
|
|
9
10
|
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
10
11
|
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
11
12
|
var _reactUtils = require("../../utils/react-utils");
|
|
@@ -217,6 +218,24 @@ function FullscreenModal({
|
|
|
217
218
|
}), [globalModes, propModes]);
|
|
218
219
|
const rootGap = Number((0, _figmaVariablesResolver.getVariableByName)('fullScreenModal/gap', modes)) || 16;
|
|
219
220
|
|
|
221
|
+
// Safe-area insets so the floating chrome clears the system bars: the close
|
|
222
|
+
// button drops below the status bar / notch, and the sticky footer keeps its
|
|
223
|
+
// designed bottom padding ON TOP of the bottom inset (home indicator /
|
|
224
|
+
// Android gesture or nav bar). On web — and anywhere without a
|
|
225
|
+
// SafeAreaProvider — every inset is 0, so the layout is unchanged.
|
|
226
|
+
const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
227
|
+
const closeButtonInsetStyle = (0, _react.useMemo)(() => ({
|
|
228
|
+
top: 12 + insets.top
|
|
229
|
+
}), [insets.top]);
|
|
230
|
+
// Extend (not replace) the footer's token bottom padding by the bottom inset
|
|
231
|
+
// so the action button never sits under the system navigation area.
|
|
232
|
+
const footerInsetStyle = (0, _react.useMemo)(() => {
|
|
233
|
+
const base = Number((0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/bottom', modes)) || 41;
|
|
234
|
+
return {
|
|
235
|
+
paddingBottom: base + insets.bottom
|
|
236
|
+
};
|
|
237
|
+
}, [modes, insets.bottom]);
|
|
238
|
+
|
|
220
239
|
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
221
240
|
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
222
241
|
// behind the transparent footer), but we translate it up by the exact scroll
|
|
@@ -316,12 +335,13 @@ function FullscreenModal({
|
|
|
316
335
|
})
|
|
317
336
|
}), footerContent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ActionFooter.default, {
|
|
318
337
|
modes: modes,
|
|
338
|
+
style: footerInsetStyle,
|
|
319
339
|
children: footerContent
|
|
320
340
|
}) : null, showClose ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconButton.default, {
|
|
321
341
|
iconName: "ic_close",
|
|
322
342
|
modes: modes,
|
|
323
343
|
accessibilityLabel: closeAccessibilityLabel,
|
|
324
|
-
style: closeButtonStyle,
|
|
344
|
+
style: [closeButtonStyle, closeButtonInsetStyle],
|
|
325
345
|
...(onClose ? {
|
|
326
346
|
onPress: onClose
|
|
327
347
|
} : {})
|
|
@@ -120,7 +120,11 @@ const useSegmentRotation = (clock, index, gravity, spreadMinRad, spreadMaxRad, s
|
|
|
120
120
|
};
|
|
121
121
|
}, [gravity, index, spreadMinRad, spreadMaxRad, spreadOutFrac]);
|
|
122
122
|
const fullSize = {
|
|
123
|
-
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
top: 0,
|
|
125
|
+
left: 0,
|
|
126
|
+
right: 0,
|
|
127
|
+
bottom: 0
|
|
124
128
|
};
|
|
125
129
|
function Spinner({
|
|
126
130
|
size = DEFAULT_SIZE,
|