@sigmela/router 0.2.8 → 0.3.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/lib/module/Drawer/Drawer.js +250 -0
- package/lib/module/Drawer/DrawerContext.js +4 -0
- package/lib/module/Drawer/DrawerIcon.web.js +47 -0
- package/lib/module/Drawer/RenderDrawer.native.js +241 -0
- package/lib/module/Drawer/RenderDrawer.web.js +197 -0
- package/lib/module/Drawer/useDrawer.js +11 -0
- package/lib/module/Navigation.js +4 -2
- package/lib/module/NavigationStack.js +14 -4
- package/lib/module/Router.js +214 -60
- package/lib/module/RouterContext.js +1 -1
- package/lib/module/ScreenStack/ScreenStack.web.js +78 -12
- package/lib/module/ScreenStackItem/ScreenStackItem.js +7 -7
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +65 -6
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +4 -5
- package/lib/module/SplitView/RenderSplitView.web.js +5 -7
- package/lib/module/SplitView/SplitView.js +10 -2
- package/lib/module/StackRenderer.js +10 -4
- package/lib/module/TabBar/RenderTabBar.native.js +10 -9
- package/lib/module/TabBar/RenderTabBar.web.js +25 -2
- package/lib/module/TabBar/TabBar.js +8 -1
- package/lib/module/TabBar/TabIcon.web.js +12 -7
- package/lib/module/index.js +2 -0
- package/lib/module/styles.css +246 -91
- package/lib/typescript/src/Drawer/Drawer.d.ts +100 -0
- package/lib/typescript/src/Drawer/DrawerContext.d.ts +3 -0
- package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +7 -0
- package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +8 -0
- package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +8 -0
- package/lib/typescript/src/Drawer/useDrawer.d.ts +2 -0
- package/lib/typescript/src/NavigationStack.d.ts +1 -0
- package/lib/typescript/src/Router.d.ts +13 -0
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +1 -1
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +1 -1
- package/lib/typescript/src/SplitView/SplitView.d.ts +2 -0
- package/lib/typescript/src/TabBar/TabBar.d.ts +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/types.d.ts +1 -1
- package/package.json +15 -4
|
@@ -6,7 +6,7 @@ import { ScreenStackItemsContext, ScreenStackAnimatingContext, useScreenStackCon
|
|
|
6
6
|
import { getPresentationTypeClass, computeAnimationType } from "./animationHelpers.js";
|
|
7
7
|
import { RouterContext } from "../RouterContext.js";
|
|
8
8
|
import { isModalLikePresentation } from "../types.js";
|
|
9
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
10
|
const isScreenStackItemElement = child => {
|
|
11
11
|
if (! /*#__PURE__*/isValidElement(child)) return false;
|
|
12
12
|
const anyProps = child.props;
|
|
@@ -58,8 +58,9 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
58
58
|
const router = useContext(RouterContext);
|
|
59
59
|
const debugEnabled = router?.isDebugEnabled() ?? false;
|
|
60
60
|
const devLog = useCallback((msg, data) => {
|
|
61
|
-
if (
|
|
62
|
-
|
|
61
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && debugEnabled) {
|
|
62
|
+
console.log(msg, data !== undefined ? JSON.stringify(data) : '');
|
|
63
|
+
}
|
|
63
64
|
}, [debugEnabled]);
|
|
64
65
|
devLog('[ScreenStack] Render', {
|
|
65
66
|
transitionTime,
|
|
@@ -121,13 +122,15 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
121
122
|
const key = item?.key || getItemKey(child);
|
|
122
123
|
map.set(key, child);
|
|
123
124
|
}
|
|
124
|
-
childMapRef.current = map;
|
|
125
125
|
devLog('[ScreenStack] childMap updated', {
|
|
126
126
|
size: map.size,
|
|
127
127
|
keys: Array.from(map.keys())
|
|
128
128
|
});
|
|
129
129
|
return map;
|
|
130
130
|
}, [devLog, stackChildren]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
childMapRef.current = childMap;
|
|
133
|
+
}, [childMap]);
|
|
131
134
|
const {
|
|
132
135
|
stateMap,
|
|
133
136
|
toggle,
|
|
@@ -160,7 +163,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
160
163
|
isEnter: state.isEnter,
|
|
161
164
|
isResolved: state.isResolved
|
|
162
165
|
})));
|
|
163
|
-
const stateMapEntries = Array.from(stateMap.entries());
|
|
166
|
+
const stateMapEntries = useMemo(() => Array.from(stateMap.entries()), [stateMap]);
|
|
164
167
|
const prevKeysForDirection = prevKeysRef.current;
|
|
165
168
|
const direction = useMemo(() => {
|
|
166
169
|
const computed = computeDirection(prevKeysForDirection, routeKeys);
|
|
@@ -190,9 +193,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
190
193
|
});
|
|
191
194
|
return result;
|
|
192
195
|
}, [devLog, routeKeys, stateMapEntries]);
|
|
193
|
-
const containerClassName =
|
|
194
|
-
return 'screen-stack';
|
|
195
|
-
}, []);
|
|
196
|
+
const containerClassName = 'screen-stack';
|
|
196
197
|
|
|
197
198
|
// CRITICAL: Calculate bulk removal BEFORE useMemo for itemsContextValue
|
|
198
199
|
// so the flag is available when computing animation types
|
|
@@ -331,6 +332,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
331
332
|
}
|
|
332
333
|
}, [routeKeys, animateFirstScreenAfterEmpty]);
|
|
333
334
|
useEffect(() => {
|
|
335
|
+
if (!debugEnabled) return;
|
|
334
336
|
if (!containerRef.current) return;
|
|
335
337
|
const items = containerRef.current.querySelectorAll('.screen-stack-item');
|
|
336
338
|
if (items.length === 0) return;
|
|
@@ -340,7 +342,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
340
342
|
containerDataDirection: containerRef.current.dataset.direction,
|
|
341
343
|
itemCount: items.length
|
|
342
344
|
});
|
|
343
|
-
});
|
|
345
|
+
}, [debugEnabled, devLog]);
|
|
344
346
|
const topKey = routeKeys[routeKeys.length - 1] ?? null;
|
|
345
347
|
const routeKeySet = useMemo(() => new Set(routeKeys), [routeKeys]);
|
|
346
348
|
const hasExitingItems = useMemo(() => {
|
|
@@ -353,6 +355,47 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
353
355
|
return isModalLikePresentation(item?.options?.stackPresentation);
|
|
354
356
|
});
|
|
355
357
|
}, [childMap, routeKeySet, stateMapEntries]);
|
|
358
|
+
|
|
359
|
+
// D4: Compute active modal/sheet state for data attributes
|
|
360
|
+
const hasActiveModal = useMemo(() => {
|
|
361
|
+
for (const key of routeKeys) {
|
|
362
|
+
const child = childMap.get(key);
|
|
363
|
+
if (!child) continue;
|
|
364
|
+
const presentation = child.props.item?.options?.stackPresentation;
|
|
365
|
+
if (presentation && presentation !== 'push' && presentation !== 'formSheet' && presentation !== 'pageSheet' && presentation !== 'sheet' && isModalLikePresentation(presentation)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}, [routeKeys, childMap]);
|
|
371
|
+
const hasActiveSheet = useMemo(() => {
|
|
372
|
+
for (const key of routeKeys) {
|
|
373
|
+
const child = childMap.get(key);
|
|
374
|
+
if (!child) continue;
|
|
375
|
+
const presentation = child.props.item?.options?.stackPresentation;
|
|
376
|
+
if (presentation === 'formSheet' || presentation === 'pageSheet' || presentation === 'sheet') {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}, [routeKeys, childMap]);
|
|
382
|
+
|
|
383
|
+
// D5: Scroll lock during active modal (ref-counted for nested stacks)
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!hasActiveModal) return;
|
|
386
|
+
const g = globalThis;
|
|
387
|
+
g.__scrollLockCount = (g.__scrollLockCount ?? 0) + 1;
|
|
388
|
+
if (g.__scrollLockCount === 1) {
|
|
389
|
+
g.__scrollLockPrev = document.body.style.overflow;
|
|
390
|
+
document.body.style.overflow = 'hidden';
|
|
391
|
+
}
|
|
392
|
+
return () => {
|
|
393
|
+
g.__scrollLockCount = Math.max(0, (g.__scrollLockCount ?? 1) - 1);
|
|
394
|
+
if (g.__scrollLockCount === 0) {
|
|
395
|
+
document.body.style.overflow = g.__scrollLockPrev ?? '';
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}, [hasActiveModal]);
|
|
356
399
|
const animationDirection = hasExitingItems ? lastDirectionRef.current : direction;
|
|
357
400
|
const stackDismissedByRouter = router?.isStackBeingDismissed?.(currentStackId ?? undefined) ?? false;
|
|
358
401
|
if (stackDismissedByRouter) {
|
|
@@ -459,17 +502,40 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
459
502
|
const animating = useMemo(() => {
|
|
460
503
|
return stateMapEntries.some(([, state]) => state.isMounted && (state.status === 'entering' || state.status === 'exiting' || state.status === 'preEnter' || state.status === 'preExit'));
|
|
461
504
|
}, [stateMapEntries]);
|
|
505
|
+
|
|
506
|
+
// D6: Compute current screen label for accessibility
|
|
507
|
+
const currentScreenLabel = useMemo(() => {
|
|
508
|
+
if (!topKey) return '';
|
|
509
|
+
const child = childMap.get(topKey);
|
|
510
|
+
if (!child) return '';
|
|
511
|
+
const item = child.props.item;
|
|
512
|
+
return item?.routeId || topKey;
|
|
513
|
+
}, [topKey, childMap]);
|
|
462
514
|
return /*#__PURE__*/_jsx(ScreenStackItemsContext.Provider, {
|
|
463
515
|
value: itemsContextValue,
|
|
464
516
|
children: /*#__PURE__*/_jsx(ScreenStackAnimatingContext.Provider, {
|
|
465
517
|
value: animating,
|
|
466
|
-
children: /*#__PURE__*/
|
|
518
|
+
children: /*#__PURE__*/_jsxs("div", {
|
|
467
519
|
ref: containerRef,
|
|
468
520
|
className: containerClassName + (animating ? ' animating' : ''),
|
|
521
|
+
"data-has-active-modal": hasActiveModal ? 'true' : undefined,
|
|
522
|
+
"data-has-active-sheet": hasActiveSheet ? 'true' : undefined,
|
|
523
|
+
"data-animation": animating ? 'true' : undefined,
|
|
469
524
|
style: {
|
|
470
525
|
'--stack-transition-time': `${transitionTime}ms`
|
|
471
526
|
},
|
|
472
|
-
children:
|
|
527
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
528
|
+
"aria-live": "polite",
|
|
529
|
+
role: "status",
|
|
530
|
+
style: {
|
|
531
|
+
position: 'absolute',
|
|
532
|
+
width: 1,
|
|
533
|
+
height: 1,
|
|
534
|
+
overflow: 'hidden',
|
|
535
|
+
clip: 'rect(0,0,0,0)'
|
|
536
|
+
},
|
|
537
|
+
children: currentScreenLabel
|
|
538
|
+
}), keysToRender.map(key => {
|
|
473
539
|
const transitionState = stateMap.get(key);
|
|
474
540
|
if (!transitionState || !transitionState.isMounted) {
|
|
475
541
|
devLog(`[ScreenStack] Skipping ${key} - no state or not mounted`, {
|
|
@@ -488,7 +554,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
488
554
|
return /*#__PURE__*/_jsx(Fragment, {
|
|
489
555
|
children: child
|
|
490
556
|
}, child.key || key);
|
|
491
|
-
})
|
|
557
|
+
})]
|
|
492
558
|
})
|
|
493
559
|
})
|
|
494
560
|
});
|
|
@@ -4,7 +4,7 @@ import { ScreenStackItem as RNSScreenStackItem } from 'react-native-screens';
|
|
|
4
4
|
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
5
5
|
import { ScreenStackSheetItem } from "../ScreenStackSheetItem/index.js";
|
|
6
6
|
import { StyleSheet } from 'react-native';
|
|
7
|
-
import { memo } from 'react';
|
|
7
|
+
import { memo, useMemo, useCallback } from 'react';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
10
10
|
item,
|
|
@@ -20,15 +20,15 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
20
20
|
|
|
21
21
|
// On native, modalRight behaves as regular modal
|
|
22
22
|
const nativePresentation = stackPresentation === 'modalRight' ? 'modal' : stackPresentation;
|
|
23
|
-
const route = {
|
|
23
|
+
const route = useMemo(() => ({
|
|
24
24
|
presentation: stackPresentation ?? 'push',
|
|
25
25
|
params: item.params,
|
|
26
26
|
query: item.query,
|
|
27
27
|
pattern: item.pattern,
|
|
28
28
|
path: item.path
|
|
29
|
-
};
|
|
29
|
+
}), [stackPresentation, item.params, item.query, item.pattern, item.path]);
|
|
30
30
|
const router = useRouter();
|
|
31
|
-
const onDismissed = () => {
|
|
31
|
+
const onDismissed = useCallback(() => {
|
|
32
32
|
if (stackId) {
|
|
33
33
|
const history = router.getStackHistory(stackId);
|
|
34
34
|
const topKey = history.length ? history[history.length - 1]?.key : null;
|
|
@@ -36,12 +36,12 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
36
36
|
router.goBack();
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
};
|
|
40
|
-
const headerConfig = {
|
|
39
|
+
}, [stackId, router, item.key]);
|
|
40
|
+
const headerConfig = useMemo(() => ({
|
|
41
41
|
...header,
|
|
42
42
|
hidden: !header?.title || header?.hidden,
|
|
43
43
|
backgroundColor: appearance?.header?.backgroundColor ?? 'transparent'
|
|
44
|
-
};
|
|
44
|
+
}), [header, appearance?.header?.backgroundColor]);
|
|
45
45
|
if (route.presentation === 'sheet') {
|
|
46
46
|
return /*#__PURE__*/_jsx(ScreenStackSheetItem, {
|
|
47
47
|
appearance: appearance?.sheet,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
4
4
|
import { isModalLikePresentation } from "../types.js";
|
|
5
|
-
import { memo, useMemo, useCallback } from 'react';
|
|
5
|
+
import { memo, useMemo, useCallback, useEffect, useRef } from 'react';
|
|
6
6
|
import { StyleSheet, View } from 'react-native';
|
|
7
7
|
import { useScreenStackItemsContext } from "../ScreenStack/ScreenStackContext.js";
|
|
8
8
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -13,6 +13,7 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
13
13
|
}) => {
|
|
14
14
|
const itemsContext = useScreenStackItemsContext();
|
|
15
15
|
const router = useRouter();
|
|
16
|
+
const containerRef = useRef(null);
|
|
16
17
|
const debugEnabled = router.isDebugEnabled();
|
|
17
18
|
const devLog = useCallback((msg, data) => {
|
|
18
19
|
if (!debugEnabled) return;
|
|
@@ -65,36 +66,94 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
65
66
|
maxWidth: `${item.options.maxWidth}px`
|
|
66
67
|
};
|
|
67
68
|
}, [isModalLike, item.options?.maxWidth]);
|
|
68
|
-
const
|
|
69
|
+
const routeValue = useMemo(() => ({
|
|
69
70
|
presentation,
|
|
70
71
|
params: item.params,
|
|
71
72
|
query: item.query,
|
|
72
73
|
pattern: item.pattern,
|
|
73
74
|
path: item.path
|
|
74
|
-
};
|
|
75
|
+
}), [presentation, item.params, item.query, item.pattern, item.path]);
|
|
76
|
+
const handleDismiss = useCallback(() => router.dismiss(), [router]);
|
|
77
|
+
const handleDismissKeyDown = useCallback(e => {
|
|
78
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
79
|
+
e.preventDefault?.();
|
|
80
|
+
router.dismiss();
|
|
81
|
+
}
|
|
82
|
+
}, [router]);
|
|
83
|
+
|
|
84
|
+
// Escape key handler for modals — only dismiss if this is the topmost modal
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!isModalLike) return;
|
|
87
|
+
const handler = e => {
|
|
88
|
+
if (e.key !== 'Escape') return;
|
|
89
|
+
const allModals = document.querySelectorAll('[aria-modal="true"]');
|
|
90
|
+
if (allModals.length > 0) {
|
|
91
|
+
const last = allModals[allModals.length - 1] ?? null;
|
|
92
|
+
if (!containerRef.current?.contains(last)) return;
|
|
93
|
+
}
|
|
94
|
+
router.dismiss();
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener('keydown', handler);
|
|
97
|
+
return () => document.removeEventListener('keydown', handler);
|
|
98
|
+
}, [isModalLike, router]);
|
|
99
|
+
|
|
100
|
+
// Focus trap for modals — re-queries focusable elements on each Tab
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!isModalLike || !containerRef.current) return;
|
|
103
|
+
const container = containerRef.current;
|
|
104
|
+
const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
105
|
+
const focusable = container.querySelectorAll(FOCUSABLE);
|
|
106
|
+
focusable[0]?.focus();
|
|
107
|
+
const trap = e => {
|
|
108
|
+
if (e.key !== 'Tab') return;
|
|
109
|
+
const els = container.querySelectorAll(FOCUSABLE);
|
|
110
|
+
if (!els.length) return;
|
|
111
|
+
const first = els[0];
|
|
112
|
+
const last = els[els.length - 1];
|
|
113
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
last?.focus();
|
|
116
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
first?.focus();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
container.addEventListener('keydown', trap);
|
|
122
|
+
return () => container.removeEventListener('keydown', trap);
|
|
123
|
+
}, [isModalLike]);
|
|
75
124
|
if (!itemState) {
|
|
76
125
|
return null;
|
|
77
126
|
}
|
|
78
127
|
return /*#__PURE__*/_jsxs("div", {
|
|
128
|
+
ref: containerRef,
|
|
79
129
|
style: mergedStyle,
|
|
80
130
|
className: className,
|
|
81
131
|
children: [isModalLike && /*#__PURE__*/_jsx("div", {
|
|
82
132
|
className: "stack-modal-overlay",
|
|
83
|
-
onClick:
|
|
133
|
+
onClick: handleDismiss,
|
|
134
|
+
onKeyDown: handleDismissKeyDown,
|
|
135
|
+
role: "button",
|
|
136
|
+
"aria-label": "Close modal",
|
|
137
|
+
tabIndex: 0
|
|
84
138
|
}), /*#__PURE__*/_jsx("div", {
|
|
85
139
|
className: isModalLike ? 'stack-modal-container' : 'stack-screen-container',
|
|
86
140
|
style: modalContainerStyle,
|
|
141
|
+
...(isModalLike ? {
|
|
142
|
+
'role': 'dialog',
|
|
143
|
+
'aria-modal': true,
|
|
144
|
+
'aria-label': item.pattern || 'Dialog'
|
|
145
|
+
} : undefined),
|
|
87
146
|
children: appearance?.screen ? /*#__PURE__*/_jsx(View, {
|
|
88
147
|
style: [appearance?.screen, styles.flex],
|
|
89
148
|
children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
90
|
-
value:
|
|
149
|
+
value: routeValue,
|
|
91
150
|
children: /*#__PURE__*/_jsx(item.component, {
|
|
92
151
|
...(item.passProps || {}),
|
|
93
152
|
appearance: appearance
|
|
94
153
|
})
|
|
95
154
|
})
|
|
96
155
|
}) : /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
97
|
-
value:
|
|
156
|
+
value: routeValue,
|
|
98
157
|
children: /*#__PURE__*/_jsx(item.component, {
|
|
99
158
|
...(item.passProps || {}),
|
|
100
159
|
appearance: appearance
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ScreenStackItem as RNSScreenStackItem } from 'react-native-screens';
|
|
4
4
|
import { NativeSheetView, Commands } from '@sigmela/native-sheet';
|
|
5
5
|
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
6
|
-
import { memo, useRef, useEffect } from 'react';
|
|
6
|
+
import { memo, useRef, useEffect, useCallback } from 'react';
|
|
7
7
|
import { StyleSheet } from 'react-native';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
export const ScreenStackSheetItem = /*#__PURE__*/memo(props => {
|
|
@@ -31,12 +31,11 @@ export const ScreenStackSheetItem = /*#__PURE__*/memo(props => {
|
|
|
31
31
|
return () => router.unregisterSheetDismisser(item.key);
|
|
32
32
|
}
|
|
33
33
|
return undefined;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const handleSheetDismissed = () => {
|
|
34
|
+
}, [route.presentation, router, item.key]);
|
|
35
|
+
const handleSheetDismissed = useCallback(() => {
|
|
37
36
|
router.unregisterSheetDismisser(item.key);
|
|
38
37
|
onDismissed();
|
|
39
|
-
};
|
|
38
|
+
}, [router, item.key, onDismissed]);
|
|
40
39
|
return /*#__PURE__*/_jsx(RNSScreenStackItem, {
|
|
41
40
|
stackPresentation: "transparentModal",
|
|
42
41
|
headerConfig: headerConfig,
|
|
@@ -77,6 +77,9 @@ export const RenderSplitView = /*#__PURE__*/memo(({
|
|
|
77
77
|
'--split-view-primary-max-width': `${splitView.primaryMaxWidth}px`
|
|
78
78
|
};
|
|
79
79
|
}, [splitView.primaryMaxWidth]);
|
|
80
|
+
const secondaryConfig = useMemo(() => ({
|
|
81
|
+
animateFirstScreenAfterEmpty: !isWide
|
|
82
|
+
}), [isWide]);
|
|
80
83
|
return /*#__PURE__*/_jsx(SplitViewContext.Provider, {
|
|
81
84
|
value: splitView,
|
|
82
85
|
children: /*#__PURE__*/_jsxs("div", {
|
|
@@ -91,17 +94,12 @@ export const RenderSplitView = /*#__PURE__*/memo(({
|
|
|
91
94
|
})
|
|
92
95
|
}), /*#__PURE__*/_jsx("div", {
|
|
93
96
|
className: "split-view-secondary",
|
|
94
|
-
children:
|
|
95
|
-
value:
|
|
96
|
-
animateFirstScreenAfterEmpty: false
|
|
97
|
-
},
|
|
97
|
+
children: /*#__PURE__*/_jsx(ScreenStackConfigContext.Provider, {
|
|
98
|
+
value: secondaryConfig,
|
|
98
99
|
children: /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
99
100
|
appearance: appearance,
|
|
100
101
|
stack: splitView.secondary
|
|
101
102
|
})
|
|
102
|
-
}) : /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
103
|
-
appearance: appearance,
|
|
104
|
-
stack: splitView.secondary
|
|
105
103
|
})
|
|
106
104
|
})]
|
|
107
105
|
})
|
|
@@ -35,12 +35,14 @@ class SecondaryStackWrapper {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
export class SplitView {
|
|
38
|
+
_cachedRenderer = null;
|
|
38
39
|
constructor(options) {
|
|
39
40
|
this.splitViewId = `splitview-${Math.random().toString(36).slice(2)}`;
|
|
40
41
|
this.primary = options.primary;
|
|
41
42
|
this.secondary = options.secondary;
|
|
42
43
|
this.minWidth = options.minWidth;
|
|
43
44
|
this.primaryMaxWidth = options.primaryMaxWidth ?? 390;
|
|
45
|
+
this.secondaryWrapper = new SecondaryStackWrapper(this.secondary);
|
|
44
46
|
}
|
|
45
47
|
getId() {
|
|
46
48
|
return this.splitViewId;
|
|
@@ -54,18 +56,24 @@ export class SplitView {
|
|
|
54
56
|
node: this.primary
|
|
55
57
|
}, {
|
|
56
58
|
prefix: '',
|
|
57
|
-
node:
|
|
59
|
+
node: this.secondaryWrapper
|
|
58
60
|
}];
|
|
59
61
|
}
|
|
60
62
|
getRenderer() {
|
|
63
|
+
if (this._cachedRenderer) {
|
|
64
|
+
return this._cachedRenderer;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
// eslint-disable-next-line consistent-this
|
|
62
68
|
const instance = this;
|
|
63
|
-
|
|
69
|
+
const renderer = function SplitViewScreen(props) {
|
|
64
70
|
return /*#__PURE__*/React.createElement(RenderSplitView, {
|
|
65
71
|
splitView: instance,
|
|
66
72
|
appearance: props?.appearance
|
|
67
73
|
});
|
|
68
74
|
};
|
|
75
|
+
this._cachedRenderer = renderer;
|
|
76
|
+
return renderer;
|
|
69
77
|
}
|
|
70
78
|
hasRoute(routeId) {
|
|
71
79
|
return this.primary.getRoutes().some(r => r.routeId === routeId) || this.secondary.getRoutes().some(r => r.routeId === routeId);
|
|
@@ -12,10 +12,16 @@ export const StackRenderer = /*#__PURE__*/memo(({
|
|
|
12
12
|
history
|
|
13
13
|
}) => {
|
|
14
14
|
const router = useRouter();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const hasHistoryProp = history != null;
|
|
16
|
+
const subscribe = useCallback(cb => {
|
|
17
|
+
if (hasHistoryProp) return () => {};
|
|
18
|
+
return router.subscribeStack(stackId, cb);
|
|
19
|
+
}, [router, stackId, hasHistoryProp]);
|
|
20
|
+
const get = useCallback(() => {
|
|
21
|
+
if (hasHistoryProp) return history;
|
|
22
|
+
return router.getStackHistory(stackId);
|
|
23
|
+
}, [router, stackId, hasHistoryProp, history]);
|
|
24
|
+
const historyForThisStack = useSyncExternalStore(subscribe, get, get);
|
|
19
25
|
return /*#__PURE__*/_jsx(ScreenStack, {
|
|
20
26
|
style: [styles.flex, appearance?.screen],
|
|
21
27
|
children: historyForThisStack.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
@@ -259,7 +259,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
}, [tabs, tabBar, index, router]);
|
|
262
|
-
const containerProps = {
|
|
262
|
+
const containerProps = useMemo(() => ({
|
|
263
263
|
tabBarBackgroundColor: backgroundColor,
|
|
264
264
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
265
265
|
tabBarItemTitleFontSize: title?.fontSize,
|
|
@@ -273,19 +273,19 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
273
273
|
tabBarItemActiveIndicatorEnabled: androidActiveIndicatorEnabled,
|
|
274
274
|
tabBarItemRippleColor: androidRippleColor,
|
|
275
275
|
tabBarItemLabelVisibilityMode: labelVisibilityMode
|
|
276
|
-
};
|
|
277
|
-
const iosState = {
|
|
276
|
+
}), [backgroundColor, title?.fontFamily, title?.fontSize, title?.fontWeight, title?.fontStyle, title?.color, title?.activeColor, iconColor, iconColorActive, androidActiveIndicatorColor, androidActiveIndicatorEnabled, androidRippleColor, labelVisibilityMode]);
|
|
277
|
+
const iosState = useMemo(() => ({
|
|
278
278
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
279
279
|
tabBarItemTitleFontSize: title?.fontSize,
|
|
280
280
|
tabBarItemTitleFontWeight: title?.fontWeight,
|
|
281
281
|
tabBarItemTitleFontStyle: title?.fontStyle,
|
|
282
282
|
tabBarItemTitleFontColor: title?.color,
|
|
283
283
|
tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
|
|
284
|
-
tabBarItemTitleFontColorActive: title?.color,
|
|
284
|
+
tabBarItemTitleFontColorActive: title?.activeColor ?? title?.color,
|
|
285
285
|
tabBarItemIconColorActive: iconColorActive,
|
|
286
286
|
tabBarItemIconColor: iconColor
|
|
287
|
-
};
|
|
288
|
-
const iosAppearance = Platform.select({
|
|
287
|
+
}), [title?.fontFamily, title?.fontSize, title?.fontWeight, title?.fontStyle, title?.color, title?.activeColor, badgeBackgroundColor, iconColorActive, iconColor]);
|
|
288
|
+
const iosAppearance = useMemo(() => Platform.select({
|
|
289
289
|
default: undefined,
|
|
290
290
|
ios: {
|
|
291
291
|
tabBarBackgroundColor: backgroundColor,
|
|
@@ -300,8 +300,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
300
300
|
normal: iosState
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
-
});
|
|
303
|
+
}), [backgroundColor, iOSShadowColor, iosState]);
|
|
304
304
|
const CustomTabBar = config.component;
|
|
305
|
+
const tabIcons = useMemo(() => tabs.map(tab => getTabIcon(tab)), [tabs]);
|
|
305
306
|
const [visited, setVisited] = useState({});
|
|
306
307
|
useEffect(() => {
|
|
307
308
|
const key = tabs[index]?.tabKey;
|
|
@@ -360,12 +361,12 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
360
361
|
children: /*#__PURE__*/_jsx(BottomTabs, {
|
|
361
362
|
onNativeFocusChange: onNativeFocusChange,
|
|
362
363
|
...containerProps,
|
|
363
|
-
children: tabs.map(tab => {
|
|
364
|
+
children: tabs.map((tab, i) => {
|
|
364
365
|
const isFocused = tab.tabKey === tabs[index]?.tabKey;
|
|
365
366
|
const stack = tabBar.stacks[tab.tabKey];
|
|
366
367
|
const node = tabBar.nodes[tab.tabKey];
|
|
367
368
|
const Screen = tabBar.screens[tab.tabKey];
|
|
368
|
-
const icon =
|
|
369
|
+
const icon = tabIcons[i];
|
|
369
370
|
return /*#__PURE__*/_jsx(BottomTabsScreen, {
|
|
370
371
|
scrollEdgeAppearance: iosAppearance,
|
|
371
372
|
standardAppearance: iosAppearance,
|
|
@@ -106,6 +106,15 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
106
106
|
tabBar.onIndexChange(nextIndex);
|
|
107
107
|
}
|
|
108
108
|
}, [router, tabBar, tabs, index]);
|
|
109
|
+
const onKeyDown = useCallback((e, currentIndex) => {
|
|
110
|
+
let nextIndex = currentIndex;
|
|
111
|
+
if (e.key === 'ArrowRight') nextIndex = (currentIndex + 1) % tabs.length;else if (e.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;else return;
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
onTabClick(nextIndex);
|
|
114
|
+
const container = e.currentTarget.parentElement;
|
|
115
|
+
const buttons = container?.querySelectorAll('[role="tab"]');
|
|
116
|
+
buttons?.[nextIndex]?.focus();
|
|
117
|
+
}, [tabs.length, onTabClick]);
|
|
109
118
|
const tabBarStyle = useMemo(() => {
|
|
110
119
|
const tabBarBg = toColorString(appearance?.tabBar?.backgroundColor);
|
|
111
120
|
const style = {
|
|
@@ -125,6 +134,14 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
125
134
|
fontWeight: appearance?.tabBar?.title?.fontWeight,
|
|
126
135
|
fontStyle: appearance?.tabBar?.title?.fontStyle
|
|
127
136
|
}), [appearance?.tabBar?.title?.fontFamily, appearance?.tabBar?.title?.fontSize, appearance?.tabBar?.title?.fontWeight, appearance?.tabBar?.title?.fontStyle]);
|
|
137
|
+
const handleTabClick = useCallback(e => {
|
|
138
|
+
const index = Number(e.currentTarget.dataset.index);
|
|
139
|
+
onTabClick(index);
|
|
140
|
+
}, [onTabClick]);
|
|
141
|
+
const handleKeyDown = useCallback(e => {
|
|
142
|
+
const index = Number(e.currentTarget.dataset.index);
|
|
143
|
+
onKeyDown(e, index);
|
|
144
|
+
}, [onKeyDown]);
|
|
128
145
|
const CustomTabBar = config.component;
|
|
129
146
|
return /*#__PURE__*/_jsx(TabBarContext.Provider, {
|
|
130
147
|
value: tabBar,
|
|
@@ -152,6 +169,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
152
169
|
"aria-hidden": "true"
|
|
153
170
|
}), /*#__PURE__*/_jsxs("div", {
|
|
154
171
|
className: "tab-bar-content",
|
|
172
|
+
role: "tablist",
|
|
155
173
|
children: [/*#__PURE__*/_jsx("div", {
|
|
156
174
|
className: "tab-bar-active-indicator",
|
|
157
175
|
"aria-hidden": "true"
|
|
@@ -166,11 +184,15 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
166
184
|
};
|
|
167
185
|
return /*#__PURE__*/_jsxs("button", {
|
|
168
186
|
type: "button",
|
|
187
|
+
role: "tab",
|
|
188
|
+
id: `tab-${tab.tabKey}`,
|
|
169
189
|
"data-index": i,
|
|
170
190
|
"data-active": isActive ? 'true' : 'false',
|
|
171
|
-
"aria-
|
|
191
|
+
"aria-selected": isActive,
|
|
192
|
+
tabIndex: isActive ? 0 : -1,
|
|
172
193
|
className: `tab-item${isActive ? ' active' : ''}`,
|
|
173
|
-
onClick:
|
|
194
|
+
onClick: handleTabClick,
|
|
195
|
+
onKeyDown: handleKeyDown,
|
|
174
196
|
children: [/*#__PURE__*/_jsx("div", {
|
|
175
197
|
className: "tab-item-icon",
|
|
176
198
|
children: isImageSource(tab.icon) ? /*#__PURE__*/_jsx(TabIcon, {
|
|
@@ -183,6 +205,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
183
205
|
children: tab.title
|
|
184
206
|
}), tab.badgeValue ? /*#__PURE__*/_jsx("span", {
|
|
185
207
|
className: "tab-item-label-badge",
|
|
208
|
+
"aria-label": `${tab.badgeValue} notifications`,
|
|
186
209
|
children: tab.badgeValue
|
|
187
210
|
}) : null]
|
|
188
211
|
}, tab.tabKey);
|
|
@@ -10,6 +10,7 @@ export class TabBar {
|
|
|
10
10
|
stacks = {};
|
|
11
11
|
nodes = {};
|
|
12
12
|
listeners = new Set();
|
|
13
|
+
_cachedRenderer = null;
|
|
13
14
|
constructor(options = {}) {
|
|
14
15
|
this.tabBarId = `tabbar-${Math.random().toString(36).slice(2)}`;
|
|
15
16
|
this.state = {
|
|
@@ -146,14 +147,20 @@ export class TabBar {
|
|
|
146
147
|
return children;
|
|
147
148
|
}
|
|
148
149
|
getRenderer() {
|
|
150
|
+
if (this._cachedRenderer) {
|
|
151
|
+
return this._cachedRenderer;
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
// eslint-disable-next-line consistent-this
|
|
150
155
|
const tabBarInstance = this;
|
|
151
|
-
|
|
156
|
+
const renderer = function TabBarScreen(props) {
|
|
152
157
|
return /*#__PURE__*/React.createElement(RenderTabBar, {
|
|
153
158
|
tabBar: tabBarInstance,
|
|
154
159
|
appearance: props?.appearance
|
|
155
160
|
});
|
|
156
161
|
};
|
|
162
|
+
this._cachedRenderer = renderer;
|
|
163
|
+
return renderer;
|
|
157
164
|
}
|
|
158
165
|
seed() {
|
|
159
166
|
const activeTab = this.state.tabs[this.state.index];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { memo } from 'react';
|
|
3
|
+
import { memo, useMemo } from 'react';
|
|
4
4
|
import { Image } from 'react-native';
|
|
5
5
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
6
|
const resolveImageUri = source => {
|
|
@@ -20,18 +20,23 @@ export const TabIcon = /*#__PURE__*/memo(({
|
|
|
20
20
|
}) => {
|
|
21
21
|
const iconUri = resolveImageUri(source);
|
|
22
22
|
const useMask = Boolean(tintColor && iconUri);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const maskStyle = useMemo(() => {
|
|
24
|
+
if (!useMask || !iconUri) return undefined;
|
|
25
|
+
return {
|
|
25
26
|
backgroundColor: tintColor,
|
|
26
27
|
WebkitMaskImage: `url(${iconUri})`,
|
|
27
|
-
maskImage: `url(${iconUri})`,
|
|
28
28
|
WebkitMaskSize: 'contain',
|
|
29
|
-
maskSize: 'contain',
|
|
30
29
|
WebkitMaskRepeat: 'no-repeat',
|
|
31
|
-
maskRepeat: 'no-repeat',
|
|
32
30
|
WebkitMaskPosition: 'center',
|
|
33
|
-
|
|
31
|
+
maskImage: `url(${iconUri})`,
|
|
32
|
+
maskSize: 'contain',
|
|
33
|
+
maskRepeat: 'no-repeat',
|
|
34
|
+
maskPosition: 'center',
|
|
35
|
+
width: '100%',
|
|
36
|
+
height: '100%'
|
|
34
37
|
};
|
|
38
|
+
}, [tintColor, iconUri, useMask]);
|
|
39
|
+
if (maskStyle) {
|
|
35
40
|
return /*#__PURE__*/_jsx("div", {
|
|
36
41
|
style: maskStyle
|
|
37
42
|
});
|
package/lib/module/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
export { useTabBar } from "./TabBar/useTabBar.js";
|
|
4
4
|
export { useTabBarHeight, TAB_BAR_HEIGHT } from "./TabBar/useTabBarHeight.js";
|
|
5
5
|
export { TabBar } from "./TabBar/TabBar.js";
|
|
6
|
+
export { Drawer } from "./Drawer/Drawer.js";
|
|
7
|
+
export { useDrawer } from "./Drawer/useDrawer.js";
|
|
6
8
|
export { SplitView } from "./SplitView/SplitView.js";
|
|
7
9
|
export { useSplitView } from "./SplitView/useSplitView.js";
|
|
8
10
|
export { Router } from "./Router.js";
|