@sigmela/router 0.2.8 → 0.3.1
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/README.md +107 -1
- 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 +244 -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 +55 -24
- 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 +255 -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 +10 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/types.d.ts +12 -3
- package/package.json +28 -12
|
@@ -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, {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { StackRenderer } from "../StackRenderer.js";
|
|
4
4
|
import { TabBarContext } from "./TabBarContext.js";
|
|
5
5
|
import { useRouter } from "../RouterContext.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Tabs, ScreenStackItem } from 'react-native-screens';
|
|
7
7
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
8
8
|
import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -178,7 +178,13 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
178
178
|
title,
|
|
179
179
|
backgroundColor,
|
|
180
180
|
badgeBackgroundColor,
|
|
181
|
-
iOSShadowColor
|
|
181
|
+
iOSShadowColor,
|
|
182
|
+
hidden,
|
|
183
|
+
tintColor,
|
|
184
|
+
controllerMode,
|
|
185
|
+
minimizeBehavior,
|
|
186
|
+
nativeContainerBackgroundColor,
|
|
187
|
+
iOSBlurEffect
|
|
182
188
|
} = appearance?.tabBar ?? {};
|
|
183
189
|
const onNativeFocusChange = useCallback(event => {
|
|
184
190
|
const tabKey = event.nativeEvent.tabKey;
|
|
@@ -259,10 +265,11 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
}, [tabs, tabBar, index, router]);
|
|
262
|
-
const containerProps = {
|
|
268
|
+
const containerProps = useMemo(() => ({
|
|
263
269
|
tabBarBackgroundColor: backgroundColor,
|
|
264
270
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
265
271
|
tabBarItemTitleFontSize: title?.fontSize,
|
|
272
|
+
tabBarItemTitleFontSizeActive: title?.activeFontSize,
|
|
266
273
|
tabBarItemTitleFontWeight: title?.fontWeight,
|
|
267
274
|
tabBarItemTitleFontStyle: title?.fontStyle,
|
|
268
275
|
tabBarItemTitleFontColor: title?.color,
|
|
@@ -272,36 +279,55 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
272
279
|
tabBarItemActiveIndicatorColor: androidActiveIndicatorColor,
|
|
273
280
|
tabBarItemActiveIndicatorEnabled: androidActiveIndicatorEnabled,
|
|
274
281
|
tabBarItemRippleColor: androidRippleColor,
|
|
275
|
-
tabBarItemLabelVisibilityMode: labelVisibilityMode
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
tabBarItemLabelVisibilityMode: labelVisibilityMode,
|
|
283
|
+
tabBarHidden: hidden,
|
|
284
|
+
tabBarTintColor: tintColor,
|
|
285
|
+
tabBarControllerMode: controllerMode,
|
|
286
|
+
tabBarMinimizeBehavior: minimizeBehavior,
|
|
287
|
+
nativeContainerStyle: nativeContainerBackgroundColor ? {
|
|
288
|
+
backgroundColor: nativeContainerBackgroundColor
|
|
289
|
+
} : undefined
|
|
290
|
+
}), [backgroundColor, title?.fontFamily, title?.fontSize, title?.activeFontSize, title?.fontWeight, title?.fontStyle, title?.color, title?.activeColor, iconColor, iconColorActive, androidActiveIndicatorColor, androidActiveIndicatorEnabled, androidRippleColor, labelVisibilityMode, hidden, tintColor, controllerMode, minimizeBehavior, nativeContainerBackgroundColor]);
|
|
291
|
+
const iosNormalState = useMemo(() => ({
|
|
278
292
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
279
293
|
tabBarItemTitleFontSize: title?.fontSize,
|
|
280
294
|
tabBarItemTitleFontWeight: title?.fontWeight,
|
|
281
295
|
tabBarItemTitleFontStyle: title?.fontStyle,
|
|
282
296
|
tabBarItemTitleFontColor: title?.color,
|
|
283
297
|
tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
|
|
284
|
-
tabBarItemTitleFontColorActive: title?.color,
|
|
285
|
-
tabBarItemIconColorActive: iconColorActive,
|
|
286
298
|
tabBarItemIconColor: iconColor
|
|
287
|
-
};
|
|
288
|
-
const
|
|
299
|
+
}), [title?.fontFamily, title?.fontSize, title?.fontWeight, title?.fontStyle, title?.color, badgeBackgroundColor, iconColor]);
|
|
300
|
+
const iosSelectedState = useMemo(() => ({
|
|
301
|
+
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
302
|
+
tabBarItemTitleFontSize: title?.fontSize,
|
|
303
|
+
tabBarItemTitleFontWeight: title?.fontWeight,
|
|
304
|
+
tabBarItemTitleFontStyle: title?.fontStyle,
|
|
305
|
+
tabBarItemTitleFontColor: title?.activeColor ?? title?.color,
|
|
306
|
+
tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
|
|
307
|
+
tabBarItemIconColor: iconColorActive ?? iconColor
|
|
308
|
+
}), [title?.fontFamily, title?.fontSize, title?.fontWeight, title?.fontStyle, title?.color, title?.activeColor, badgeBackgroundColor, iconColorActive, iconColor]);
|
|
309
|
+
const iosAppearance = useMemo(() => Platform.select({
|
|
289
310
|
default: undefined,
|
|
290
311
|
ios: {
|
|
291
312
|
tabBarBackgroundColor: backgroundColor,
|
|
292
313
|
tabBarShadowColor: iOSShadowColor,
|
|
314
|
+
tabBarBlurEffect: iOSBlurEffect,
|
|
293
315
|
compactInline: {
|
|
294
|
-
normal:
|
|
316
|
+
normal: iosNormalState,
|
|
317
|
+
selected: iosSelectedState
|
|
295
318
|
},
|
|
296
319
|
stacked: {
|
|
297
|
-
normal:
|
|
320
|
+
normal: iosNormalState,
|
|
321
|
+
selected: iosSelectedState
|
|
298
322
|
},
|
|
299
323
|
inline: {
|
|
300
|
-
normal:
|
|
324
|
+
normal: iosNormalState,
|
|
325
|
+
selected: iosSelectedState
|
|
301
326
|
}
|
|
302
327
|
}
|
|
303
|
-
});
|
|
328
|
+
}), [backgroundColor, iOSShadowColor, iOSBlurEffect, iosNormalState, iosSelectedState]);
|
|
304
329
|
const CustomTabBar = config.component;
|
|
330
|
+
const tabIcons = useMemo(() => tabs.map(tab => getTabIcon(tab)), [tabs]);
|
|
305
331
|
const [visited, setVisited] = useState({});
|
|
306
332
|
useEffect(() => {
|
|
307
333
|
const key = tabs[index]?.tabKey;
|
|
@@ -357,25 +383,30 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
357
383
|
stackAnimation: "slide_from_right",
|
|
358
384
|
children: /*#__PURE__*/_jsx(TabBarContext.Provider, {
|
|
359
385
|
value: tabBar,
|
|
360
|
-
children: /*#__PURE__*/_jsx(
|
|
386
|
+
children: /*#__PURE__*/_jsx(Tabs.Host, {
|
|
361
387
|
onNativeFocusChange: onNativeFocusChange,
|
|
388
|
+
bottomAccessory: config.bottomAccessory,
|
|
389
|
+
experimentalControlNavigationStateInJS: config.experimentalControlNavigationStateInJS,
|
|
362
390
|
...containerProps,
|
|
363
|
-
children: tabs.map(tab => {
|
|
391
|
+
children: tabs.map((tab, i) => {
|
|
364
392
|
const isFocused = tab.tabKey === tabs[index]?.tabKey;
|
|
365
393
|
const stack = tabBar.stacks[tab.tabKey];
|
|
366
394
|
const node = tabBar.nodes[tab.tabKey];
|
|
367
395
|
const Screen = tabBar.screens[tab.tabKey];
|
|
368
|
-
const
|
|
369
|
-
|
|
396
|
+
const convertedIcon = tabIcons[i];
|
|
397
|
+
const {
|
|
398
|
+
icon: _icon,
|
|
399
|
+
selectedIcon: _selectedIcon,
|
|
400
|
+
tabPrefix: _prefix,
|
|
401
|
+
...tabScreenProps
|
|
402
|
+
} = tab;
|
|
403
|
+
return /*#__PURE__*/_jsx(Tabs.Screen, {
|
|
404
|
+
...tabScreenProps,
|
|
370
405
|
scrollEdgeAppearance: iosAppearance,
|
|
371
406
|
standardAppearance: iosAppearance,
|
|
372
407
|
isFocused: isFocused,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
badgeValue: tab.badgeValue,
|
|
376
|
-
specialEffects: tab.specialEffects,
|
|
377
|
-
icon: icon?.icon,
|
|
378
|
-
selectedIcon: icon?.selectedIcon,
|
|
408
|
+
icon: convertedIcon?.icon,
|
|
409
|
+
selectedIcon: convertedIcon?.selectedIcon,
|
|
379
410
|
children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
380
411
|
appearance: appearance,
|
|
381
412
|
stack: stack
|
|
@@ -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);
|