@natsuneko-laboratory/react-native-desktop-navigation 0.1.0-alpha.12
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/DesktopNavigation.podspec +18 -0
- package/LICENSE +21 -0
- package/NATIVE.md +300 -0
- package/README.md +127 -0
- package/dist/core/NavigationContainer.d.ts +23 -0
- package/dist/core/NavigationContainer.js +68 -0
- package/dist/core/NavigationItem.d.ts +27 -0
- package/dist/core/NavigationItem.js +71 -0
- package/dist/core/Scene.d.ts +19 -0
- package/dist/core/Scene.js +124 -0
- package/dist/core/SelectionNavigator.d.ts +38 -0
- package/dist/core/SelectionNavigator.js +194 -0
- package/dist/core/builder.d.ts +40 -0
- package/dist/core/builder.js +81 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/context.js +36 -0
- package/dist/core/focus.d.ts +14 -0
- package/dist/core/focus.js +39 -0
- package/dist/core/hooks.d.ts +8 -0
- package/dist/core/hooks.js +48 -0
- package/dist/core/navigation.d.ts +6 -0
- package/dist/core/navigation.js +57 -0
- package/dist/core/store.d.ts +39 -0
- package/dist/core/store.js +391 -0
- package/dist/core/types.d.ts +155 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +46 -0
- package/dist/native/host.d.ts +100 -0
- package/dist/native/host.js +190 -0
- package/dist/native/icons.d.ts +56 -0
- package/dist/native/icons.js +55 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.js +44 -0
- package/dist/native/sidebar.d.ts +39 -0
- package/dist/native/sidebar.js +126 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.d.ts +10 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.js +4 -0
- package/dist/native/split.d.ts +27 -0
- package/dist/native/split.js +105 -0
- package/dist/native/stack.d.ts +26 -0
- package/dist/native/stack.js +66 -0
- package/dist/platform/index.d.ts +52 -0
- package/dist/platform/index.js +28 -0
- package/dist/platform/macos.d.ts +2 -0
- package/dist/platform/macos.js +18 -0
- package/dist/platform/windows.d.ts +2 -0
- package/dist/platform/windows.js +18 -0
- package/dist/routers/index.d.ts +23 -0
- package/dist/routers/index.js +393 -0
- package/dist/routers/types.d.ts +116 -0
- package/dist/routers/types.js +2 -0
- package/dist/sidebar/index.d.ts +17 -0
- package/dist/sidebar/index.js +18 -0
- package/dist/split/index.d.ts +42 -0
- package/dist/split/index.js +199 -0
- package/dist/stack/index.d.ts +23 -0
- package/dist/stack/index.js +88 -0
- package/dist/tabs/index.d.ts +13 -0
- package/dist/tabs/index.js +15 -0
- package/docs/GUIDE.md +384 -0
- package/macos/DDNNavigationComponentView.h +4 -0
- package/macos/DDNNavigationComponentView.mm +40 -0
- package/macos/DDNNavigationView.swift +398 -0
- package/macos/DDNNavigationViewManager.mm +11 -0
- package/package.json +82 -0
- package/react-native.config.js +19 -0
- package/src/core/NavigationContainer.tsx +137 -0
- package/src/core/NavigationItem.tsx +149 -0
- package/src/core/Scene.tsx +194 -0
- package/src/core/SelectionNavigator.tsx +371 -0
- package/src/core/builder.tsx +147 -0
- package/src/core/context.tsx +43 -0
- package/src/core/focus.tsx +50 -0
- package/src/core/hooks.ts +42 -0
- package/src/core/navigation.ts +77 -0
- package/src/core/store.ts +471 -0
- package/src/core/types.ts +176 -0
- package/src/index.ts +36 -0
- package/src/native/host.tsx +377 -0
- package/src/native/icons.ts +110 -0
- package/src/native/index.ts +56 -0
- package/src/native/sidebar.tsx +230 -0
- package/src/native/specs/DesktopNavigationHostNativeComponent.ts +12 -0
- package/src/native/split.tsx +160 -0
- package/src/native/stack.tsx +157 -0
- package/src/platform/index.tsx +76 -0
- package/src/platform/macos.ts +15 -0
- package/src/platform/windows.ts +15 -0
- package/src/routers/index.ts +442 -0
- package/src/routers/types.ts +117 -0
- package/src/sidebar/index.tsx +42 -0
- package/src/split/index.tsx +350 -0
- package/src/stack/index.tsx +213 -0
- package/src/tabs/index.tsx +40 -0
- package/windows/DesktopNavigation/DesktopNavigation.def +3 -0
- package/windows/DesktopNavigation/DesktopNavigation.vcxproj +125 -0
- package/windows/DesktopNavigation/NavigationHost.cpp +432 -0
- package/windows/DesktopNavigation/ReactPackageProvider.cpp +9 -0
- package/windows/DesktopNavigation/ReactPackageProvider.h +10 -0
- package/windows/DesktopNavigation/ReactPackageProvider.idl +6 -0
- package/windows/DesktopNavigation/pch.cpp +1 -0
- package/windows/DesktopNavigation/pch.h +27 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.navigationItemStyle = void 0;
|
|
7
|
+
exports.NavigationItem = NavigationItem;
|
|
8
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
|
+
const react_1 = __importDefault(require("react"));
|
|
10
|
+
const react_native_1 = require("react-native");
|
|
11
|
+
const context_1 = require("./context");
|
|
12
|
+
function resolveStyle(style, state) {
|
|
13
|
+
return typeof style === 'function' ? style(state) : style;
|
|
14
|
+
}
|
|
15
|
+
/** The interactive shell remains owned by navigation when its content is customized. */
|
|
16
|
+
function NavigationItem({ route, options, selected, focused, collapsed, sidebar, onFocus, onBlur, onPress, itemRef, }) {
|
|
17
|
+
const { colors } = (0, context_1.useNavigationTheme)();
|
|
18
|
+
const [pressed, setPressed] = react_1.default.useState(false);
|
|
19
|
+
const [hovered, setHovered] = react_1.default.useState(false);
|
|
20
|
+
const state = {
|
|
21
|
+
selected,
|
|
22
|
+
focused,
|
|
23
|
+
collapsed,
|
|
24
|
+
hovered,
|
|
25
|
+
pressed: pressed && !options.disabled,
|
|
26
|
+
disabled: !!options.disabled,
|
|
27
|
+
};
|
|
28
|
+
const label = options.label ?? options.title ?? route.name;
|
|
29
|
+
const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [options.icon != null && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: resolveStyle(options.iconContainerStyle, state), children: typeof options.icon === 'function'
|
|
30
|
+
? options.icon({ focused: selected, disabled: state.disabled })
|
|
31
|
+
: options.icon })), (!collapsed || !options.icon) && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
32
|
+
{ color: colors.text, flexShrink: 1 },
|
|
33
|
+
resolveStyle(options.labelStyle, state),
|
|
34
|
+
], children: collapsed ? label.slice(0, 1) : label })), !collapsed &&
|
|
35
|
+
options.badge != null &&
|
|
36
|
+
(typeof options.badge === 'string' ||
|
|
37
|
+
typeof options.badge === 'number' ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
38
|
+
{ color: colors.mutedText },
|
|
39
|
+
resolveStyle(options.badgeStyle, state),
|
|
40
|
+
], children: options.badge })) : (options.badge))] }));
|
|
41
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { ref: (target) => itemRef(target), accessible: true, focusable: !options.disabled, disabled: options.disabled, accessibilityRole: sidebar ? 'menuitem' : 'tab', accessibilityLabel: label, accessibilityState: { selected, disabled: state.disabled }, onFocus: onFocus, onBlur: () => {
|
|
42
|
+
setPressed(false);
|
|
43
|
+
onBlur();
|
|
44
|
+
}, onPress: onPress, onHoverIn: () => setHovered(true), onHoverOut: () => setHovered(false), onPressIn: () => setPressed(true), onPressOut: () => setPressed(false), style: [
|
|
45
|
+
exports.navigationItemStyle,
|
|
46
|
+
{
|
|
47
|
+
opacity: state.disabled ? 0.45 : 1,
|
|
48
|
+
backgroundColor: selected ? colors.selectedBackground : 'transparent',
|
|
49
|
+
borderColor: focused ? colors.accent : 'transparent',
|
|
50
|
+
},
|
|
51
|
+
resolveStyle(options.itemStyle, state),
|
|
52
|
+
], children: options.renderItemContent
|
|
53
|
+
? options.renderItemContent({
|
|
54
|
+
...state,
|
|
55
|
+
route,
|
|
56
|
+
label,
|
|
57
|
+
children: content,
|
|
58
|
+
})
|
|
59
|
+
: content }));
|
|
60
|
+
}
|
|
61
|
+
exports.navigationItemStyle = react_native_1.StyleSheet.create({
|
|
62
|
+
item: {
|
|
63
|
+
flexDirection: 'row',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
gap: 8,
|
|
66
|
+
padding: 10,
|
|
67
|
+
margin: 2,
|
|
68
|
+
borderRadius: 4,
|
|
69
|
+
borderWidth: 1,
|
|
70
|
+
},
|
|
71
|
+
}).item;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { NavigationRoute } from '../routers';
|
|
4
|
+
import type { CommonScreenOptions, StackAnimation } from './types';
|
|
5
|
+
export interface SceneProps {
|
|
6
|
+
nodeId: string;
|
|
7
|
+
route: NavigationRoute;
|
|
8
|
+
component: React.ComponentType<any>;
|
|
9
|
+
visible: boolean;
|
|
10
|
+
/** Native content slots must be laid out before restoring keyboard focus. */
|
|
11
|
+
focusReady?: boolean;
|
|
12
|
+
options: CommonScreenOptions;
|
|
13
|
+
animation?: StackAnimation;
|
|
14
|
+
overlay?: 'modal' | 'dialog';
|
|
15
|
+
overlayStyle?: StyleProp<ViewStyle>;
|
|
16
|
+
dialogStyle?: StyleProp<ViewStyle>;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare function Scene({ nodeId, route, component: Component, visible, focusReady, options, animation, overlay, overlayStyle, dialogStyle, children, }: SceneProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Scene = Scene;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = __importDefault(require("react"));
|
|
9
|
+
const react_native_1 = require("react-native");
|
|
10
|
+
const platform_1 = require("../platform");
|
|
11
|
+
const context_1 = require("./context");
|
|
12
|
+
const navigation_1 = require("./navigation");
|
|
13
|
+
const focus_1 = require("./focus");
|
|
14
|
+
function Scene({ nodeId, route, component: Component, visible, focusReady = true, options, animation = 'none', overlay, overlayStyle, dialogStyle, children, }) {
|
|
15
|
+
const store = (0, context_1.useStore)();
|
|
16
|
+
const { colors } = (0, context_1.useNavigationTheme)();
|
|
17
|
+
const platform = react_1.default.useContext(context_1.PlatformContext);
|
|
18
|
+
const snapshot = react_1.default.useCallback(() => store.isRouteFocused(nodeId, route.key), [store, nodeId, route.key]);
|
|
19
|
+
const focused = react_1.default.useSyncExternalStore(store.subscribe, snapshot, snapshot);
|
|
20
|
+
const navigation = react_1.default.useMemo(() => (0, navigation_1.createNavigation)(store, nodeId, route.key), [store, nodeId, route.key]);
|
|
21
|
+
const targets = react_1.default.useRef(new Map());
|
|
22
|
+
const last = react_1.default.useRef(undefined);
|
|
23
|
+
const registry = react_1.default.useMemo(() => ({
|
|
24
|
+
register(id, target) {
|
|
25
|
+
targets.current.set(id, target);
|
|
26
|
+
return () => {
|
|
27
|
+
targets.current.delete(id);
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
record(id) {
|
|
31
|
+
last.current = id;
|
|
32
|
+
store.setFocusedNode(nodeId);
|
|
33
|
+
},
|
|
34
|
+
}), [store, nodeId]);
|
|
35
|
+
const mounted = visible || options.inactiveBehavior !== 'unmount';
|
|
36
|
+
react_1.default.useEffect(() => {
|
|
37
|
+
if (!focused || !visible || !focusReady || options.focusBehavior === 'none')
|
|
38
|
+
return;
|
|
39
|
+
const previous = options.focusBehavior !== 'first' && last.current
|
|
40
|
+
? targets.current.get(last.current)
|
|
41
|
+
: undefined;
|
|
42
|
+
(previous ?? targets.current.values().next().value)?.focus();
|
|
43
|
+
}, [focused, visible, focusReady, options.focusBehavior, mounted]);
|
|
44
|
+
const [progress] = react_1.default.useState(() => new react_native_1.Animated.Value(1));
|
|
45
|
+
react_1.default.useEffect(() => {
|
|
46
|
+
if (!visible || animation === 'none') {
|
|
47
|
+
progress.setValue(1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
progress.setValue(0);
|
|
51
|
+
const transition = react_native_1.Animated.timing(progress, {
|
|
52
|
+
toValue: 1,
|
|
53
|
+
duration: platform.defaultTransitions.push.duration,
|
|
54
|
+
// react-native-macos's Fabric/bridgeless host does not reliably
|
|
55
|
+
// connect native-driven Animated values, leaving views stuck at
|
|
56
|
+
// their initial (invisible) frame. Drive this on the JS thread
|
|
57
|
+
// instead so the entrance transition actually completes.
|
|
58
|
+
useNativeDriver: false,
|
|
59
|
+
});
|
|
60
|
+
transition.start();
|
|
61
|
+
return () => transition.stop();
|
|
62
|
+
}, [visible, animation, progress, platform]);
|
|
63
|
+
if (!mounted)
|
|
64
|
+
return null;
|
|
65
|
+
const offset = platform.defaultTransitions.push.offset;
|
|
66
|
+
const animatedStyle = animation === 'none'
|
|
67
|
+
? undefined
|
|
68
|
+
: {
|
|
69
|
+
opacity: progress,
|
|
70
|
+
transform: animation === 'fade'
|
|
71
|
+
? []
|
|
72
|
+
: [
|
|
73
|
+
{
|
|
74
|
+
[animation === 'slide-vertical'
|
|
75
|
+
? 'translateY'
|
|
76
|
+
: 'translateX']: progress.interpolate({
|
|
77
|
+
inputRange: [0, 1],
|
|
78
|
+
outputRange: [offset, 0],
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
return ((0, jsx_runtime_1.jsx)(context_1.ScopeContext.Provider, { value: { nodeId, routeKey: route.key }, children: (0, jsx_runtime_1.jsx)(context_1.ScreenContext.Provider, { value: { navigation, route, focused }, children: (0, jsx_runtime_1.jsx)(focus_1.FocusContext.Provider, { value: registry, children: (0, jsx_runtime_1.jsx)(platform_1.DesktopView, { testID: `scene-${route.name}`, onFocus: () => store.focusRoute(nodeId, route.key), pointerEvents: visible ? 'auto' : 'none', accessibilityElementsHidden: !visible, importantForAccessibility: visible ? 'auto' : 'no-hide-descendants', accessibilityViewIsModal: !!overlay && visible, style: [
|
|
84
|
+
styles.scene,
|
|
85
|
+
options.sceneStyle,
|
|
86
|
+
overlay && styles.overlay,
|
|
87
|
+
overlay === 'dialog' && styles.dialogBackdrop,
|
|
88
|
+
overlay && overlayStyle,
|
|
89
|
+
!visible && styles.hidden,
|
|
90
|
+
], children: (0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { style: [
|
|
91
|
+
styles.content,
|
|
92
|
+
overlay === 'dialog' && [
|
|
93
|
+
styles.dialog,
|
|
94
|
+
{ backgroundColor: colors.surface },
|
|
95
|
+
],
|
|
96
|
+
options.contentStyle,
|
|
97
|
+
overlay === 'dialog' && dialogStyle,
|
|
98
|
+
animatedStyle,
|
|
99
|
+
], children: [children, (0, jsx_runtime_1.jsx)(Component, { navigation: navigation, route: route })] }) }) }) }) }));
|
|
100
|
+
}
|
|
101
|
+
const styles = react_native_1.StyleSheet.create({
|
|
102
|
+
scene: { flex: 1 },
|
|
103
|
+
content: { flex: 1 },
|
|
104
|
+
hidden: { display: 'none' },
|
|
105
|
+
overlay: {
|
|
106
|
+
...react_native_1.StyleSheet.absoluteFillObject,
|
|
107
|
+
backgroundColor: 'rgba(0,0,0,0.25)',
|
|
108
|
+
zIndex: 1,
|
|
109
|
+
},
|
|
110
|
+
dialogBackdrop: {
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
padding: 32,
|
|
114
|
+
},
|
|
115
|
+
dialog: {
|
|
116
|
+
flex: 0,
|
|
117
|
+
width: '80%',
|
|
118
|
+
maxWidth: 640,
|
|
119
|
+
maxHeight: '90%',
|
|
120
|
+
minHeight: 160,
|
|
121
|
+
borderRadius: 8,
|
|
122
|
+
overflow: 'hidden',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
|
|
3
|
+
import { type Definition } from './builder';
|
|
4
|
+
import type { SidebarScreenOptions, SidebarFooterProps } from './types';
|
|
5
|
+
export interface SelectionNavigatorProps {
|
|
6
|
+
id?: string;
|
|
7
|
+
initialRouteName?: string;
|
|
8
|
+
screenOptions?: SidebarScreenOptions;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
barStyle?: StyleProp<ViewStyle>;
|
|
11
|
+
barContentStyle?: StyleProp<ViewStyle>;
|
|
12
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
13
|
+
sectionStyle?: StyleProp<ViewStyle>;
|
|
14
|
+
sectionTitleStyle?: StyleProp<TextStyle>;
|
|
15
|
+
/** Hide the default button independently of programmatic collapse actions. */
|
|
16
|
+
collapseButtonShown?: boolean;
|
|
17
|
+
sidebarFooterStyle?: StyleProp<ViewStyle>;
|
|
18
|
+
/** Replace the entire footer. Return null to remove it without reserving space. */
|
|
19
|
+
renderSidebarFooter?: (props: SidebarFooterProps) => React.ReactNode;
|
|
20
|
+
collapseButtonStyle?: StyleProp<ViewStyle>;
|
|
21
|
+
collapseLabelStyle?: StyleProp<TextStyle>;
|
|
22
|
+
renderCollapseButtonContent?: (props: {
|
|
23
|
+
collapsed: boolean;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) => React.ReactNode;
|
|
26
|
+
position?: 'left' | 'right';
|
|
27
|
+
/** Use 'fill' to follow the width of a containing Split.Column. */
|
|
28
|
+
width?: number | 'fill';
|
|
29
|
+
minWidth?: number;
|
|
30
|
+
maxWidth?: number;
|
|
31
|
+
collapsible?: boolean;
|
|
32
|
+
collapsedWidth?: number;
|
|
33
|
+
defaultCollapsed?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function SelectionNavigator({ type, definitions, ...props }: SelectionNavigatorProps & {
|
|
36
|
+
type: 'sidebar' | 'tabs';
|
|
37
|
+
definitions: Definition<SidebarScreenOptions>[];
|
|
38
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SelectionNavigator = SelectionNavigator;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = __importDefault(require("react"));
|
|
9
|
+
const react_native_1 = require("react-native");
|
|
10
|
+
const builder_1 = require("./builder");
|
|
11
|
+
const NavigationItem_1 = require("./NavigationItem");
|
|
12
|
+
const Scene_1 = require("./Scene");
|
|
13
|
+
const navigation_1 = require("./navigation");
|
|
14
|
+
const context_1 = require("./context");
|
|
15
|
+
const platform_1 = require("../platform");
|
|
16
|
+
function SelectionNavigator({ type, definitions, ...props }) {
|
|
17
|
+
const platform = react_1.default.useContext(context_1.PlatformContext);
|
|
18
|
+
const { colors } = (0, context_1.useNavigationTheme)();
|
|
19
|
+
const configs = definitions.map((d) => ({
|
|
20
|
+
...d,
|
|
21
|
+
...props.screenOptions,
|
|
22
|
+
...(typeof d.options === 'object' ? d.options : {}),
|
|
23
|
+
}));
|
|
24
|
+
const { id, state, store, navigation } = (0, builder_1.useNavigator)(type, {
|
|
25
|
+
routes: configs,
|
|
26
|
+
initialRouteName: props.initialRouteName,
|
|
27
|
+
defaultCollapsed: props.defaultCollapsed,
|
|
28
|
+
}, props.id);
|
|
29
|
+
const items = state.routes.map((route) => {
|
|
30
|
+
const definition = definitions.find((d) => d.name === route.name);
|
|
31
|
+
if (!definition)
|
|
32
|
+
throw new Error(`Missing Screen: ${route.name}. Remount to change the screen list.`);
|
|
33
|
+
return {
|
|
34
|
+
route,
|
|
35
|
+
definition,
|
|
36
|
+
options: (0, builder_1.resolveOptions)(definition, route, (0, navigation_1.createNavigation)(store, id, route.key), props.screenOptions),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
react_1.default.useLayoutEffect(() => {
|
|
40
|
+
store.updateOptions(id, {
|
|
41
|
+
key: id,
|
|
42
|
+
routes: items.map((i) => ({ name: i.route.name, ...i.options })),
|
|
43
|
+
});
|
|
44
|
+
const current = items.find((i) => i.route.key === state.activeRouteKey);
|
|
45
|
+
if (current?.options.hidden || current?.options.disabled) {
|
|
46
|
+
const next = items.find((i) => !i.options.hidden && !i.options.disabled);
|
|
47
|
+
if (next)
|
|
48
|
+
navigation.select(next.route.name);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const visibleItems = items.filter((i) => !i.options.hidden);
|
|
52
|
+
const enabled = visibleItems.filter((i) => !i.options.disabled);
|
|
53
|
+
const [focusedKey, setFocusedKey] = react_1.default.useState('');
|
|
54
|
+
const refs = react_1.default.useRef(new Map());
|
|
55
|
+
const sidebar = type === 'sidebar';
|
|
56
|
+
const collapsed = sidebar && state.collapsed;
|
|
57
|
+
const splitColumn = react_1.default.useContext(context_1.SplitColumnContext);
|
|
58
|
+
const scope = react_1.default.useContext(context_1.ScopeContext);
|
|
59
|
+
const syncColumn = sidebar && splitColumn && scope.nodeId === splitColumn.nodeId
|
|
60
|
+
? splitColumn.onSidebarChange
|
|
61
|
+
: undefined;
|
|
62
|
+
const columnId = splitColumn?.columnId;
|
|
63
|
+
const collapsedWidth = props.collapsedWidth ?? 56;
|
|
64
|
+
react_1.default.useLayoutEffect(() => {
|
|
65
|
+
if (syncColumn && columnId)
|
|
66
|
+
syncColumn(columnId, collapsed ? collapsedWidth : null);
|
|
67
|
+
}, [syncColumn, columnId, collapsed, collapsedWidth]);
|
|
68
|
+
react_1.default.useLayoutEffect(() => () => {
|
|
69
|
+
if (syncColumn && columnId)
|
|
70
|
+
syncColumn(columnId, null);
|
|
71
|
+
}, [syncColumn, columnId]);
|
|
72
|
+
const focusItem = (index) => {
|
|
73
|
+
const item = enabled[(index + enabled.length) % enabled.length];
|
|
74
|
+
if (!item)
|
|
75
|
+
return;
|
|
76
|
+
setFocusedKey(item.route.key);
|
|
77
|
+
refs.current.get(item.route.key)?.focus();
|
|
78
|
+
};
|
|
79
|
+
const bindings = sidebar
|
|
80
|
+
? ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'].map((key) => ({ key }))
|
|
81
|
+
: [
|
|
82
|
+
...['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter'].map((key) => ({
|
|
83
|
+
key,
|
|
84
|
+
})),
|
|
85
|
+
...platform.keyboardShortcuts.nextTab,
|
|
86
|
+
...platform.keyboardShortcuts.previousTab,
|
|
87
|
+
];
|
|
88
|
+
const collapseContent = ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [{ color: colors.text }, props.collapseLabelStyle], children: collapsed ? '»' : '«' }));
|
|
89
|
+
const collapseButton = sidebar &&
|
|
90
|
+
props.collapsible !== false &&
|
|
91
|
+
props.collapseButtonShown !== false ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessible: true, focusable: true, accessibilityRole: "button", accessibilityLabel: collapsed ? 'Expand sidebar' : 'Collapse sidebar', onPress: () => navigation.toggleSidebar(), style: [NavigationItem_1.navigationItemStyle, props.collapseButtonStyle], children: props.renderCollapseButtonContent
|
|
92
|
+
? props.renderCollapseButtonContent({
|
|
93
|
+
collapsed,
|
|
94
|
+
children: collapseContent,
|
|
95
|
+
})
|
|
96
|
+
: collapseContent })) : null;
|
|
97
|
+
const footer = sidebar && props.renderSidebarFooter
|
|
98
|
+
? props.renderSidebarFooter({
|
|
99
|
+
collapsed,
|
|
100
|
+
collapseSidebar: navigation.collapseSidebar,
|
|
101
|
+
expandSidebar: navigation.expandSidebar,
|
|
102
|
+
toggleSidebar: navigation.toggleSidebar,
|
|
103
|
+
children: collapseButton,
|
|
104
|
+
})
|
|
105
|
+
: collapseButton;
|
|
106
|
+
const rail = ((0, jsx_runtime_1.jsxs)(platform_1.DesktopView, { accessibilityRole: sidebar ? 'menu' : 'tablist', keyDownEvents: bindings, focusable: true, onKeyDown: (event) => {
|
|
107
|
+
if ((0, platform_1.eventHandled)(event) ||
|
|
108
|
+
platform.isTextInputEvent(event) ||
|
|
109
|
+
!bindings.some((key) => (0, platform_1.matchesShortcut)(event, key)))
|
|
110
|
+
return;
|
|
111
|
+
store.setFocusedNode(id);
|
|
112
|
+
const index = enabled.findIndex((i) => i.route.key === (focusedKey || state.activeRouteKey));
|
|
113
|
+
const key = event.nativeEvent.key;
|
|
114
|
+
if (!sidebar &&
|
|
115
|
+
platform.keyboardShortcuts.nextTab.some((k) => (0, platform_1.matchesShortcut)(event, k)))
|
|
116
|
+
navigation.nextTab();
|
|
117
|
+
else if (!sidebar &&
|
|
118
|
+
platform.keyboardShortcuts.previousTab.some((k) => (0, platform_1.matchesShortcut)(event, k)))
|
|
119
|
+
navigation.previousTab();
|
|
120
|
+
else if (key === 'ArrowUp' || key === 'ArrowLeft')
|
|
121
|
+
focusItem(index - 1);
|
|
122
|
+
else if (key === 'ArrowDown' || key === 'ArrowRight')
|
|
123
|
+
focusItem(index + 1);
|
|
124
|
+
else if (key === 'Home')
|
|
125
|
+
focusItem(0);
|
|
126
|
+
else if (key === 'End')
|
|
127
|
+
focusItem(enabled.length - 1);
|
|
128
|
+
else if (key === 'Enter') {
|
|
129
|
+
const item = enabled.find((i) => i.route.key === (focusedKey || state.activeRouteKey));
|
|
130
|
+
if (item)
|
|
131
|
+
navigation.select(item.route.name);
|
|
132
|
+
}
|
|
133
|
+
(0, platform_1.consumeKey)(event);
|
|
134
|
+
}, style: [
|
|
135
|
+
{ backgroundColor: colors.surface, borderColor: colors.border },
|
|
136
|
+
sidebar
|
|
137
|
+
? {
|
|
138
|
+
width: collapsed
|
|
139
|
+
? (props.collapsedWidth ?? 56)
|
|
140
|
+
: props.width === 'fill'
|
|
141
|
+
? '100%'
|
|
142
|
+
: Math.min(props.maxWidth ?? Infinity, Math.max(props.minWidth ?? 120, props.width ?? platform.sidebar.defaultWidth)),
|
|
143
|
+
maxWidth: '100%',
|
|
144
|
+
...(props.position === 'right'
|
|
145
|
+
? { borderLeftWidth: react_native_1.StyleSheet.hairlineWidth }
|
|
146
|
+
: { borderRightWidth: react_native_1.StyleSheet.hairlineWidth }),
|
|
147
|
+
}
|
|
148
|
+
: { borderBottomWidth: react_native_1.StyleSheet.hairlineWidth },
|
|
149
|
+
props.barStyle,
|
|
150
|
+
], children: [(0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: !sidebar, contentContainerStyle: [
|
|
151
|
+
!sidebar && styles.tabItems,
|
|
152
|
+
props.barContentStyle,
|
|
153
|
+
], children: visibleItems.map((item, index) => {
|
|
154
|
+
const { route, options, definition } = item;
|
|
155
|
+
const selected = route.key === state.activeRouteKey;
|
|
156
|
+
const section = definition.section;
|
|
157
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [sidebar &&
|
|
158
|
+
!collapsed &&
|
|
159
|
+
section?.title &&
|
|
160
|
+
visibleItems[index - 1]?.definition.section?.key !==
|
|
161
|
+
section.key && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [props.sectionStyle, section.style], children: section.renderTitle ? (section.renderTitle({ title: section.title })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { accessibilityRole: "header", style: [
|
|
162
|
+
styles.section,
|
|
163
|
+
{ color: colors.mutedText },
|
|
164
|
+
props.sectionTitleStyle,
|
|
165
|
+
section.titleStyle,
|
|
166
|
+
], children: section.title })) })), (0, jsx_runtime_1.jsx)(NavigationItem_1.NavigationItem, { route: route, options: options, selected: selected, focused: focusedKey === route.key, collapsed: collapsed, sidebar: sidebar, itemRef: (value) => {
|
|
167
|
+
if (value)
|
|
168
|
+
refs.current.set(route.key, value);
|
|
169
|
+
else
|
|
170
|
+
refs.current.delete(route.key);
|
|
171
|
+
}, onFocus: () => {
|
|
172
|
+
setFocusedKey(route.key);
|
|
173
|
+
store.setFocusedNode(id);
|
|
174
|
+
}, onBlur: () => setFocusedKey((current) => current === route.key ? '' : current), onPress: () => {
|
|
175
|
+
store.setFocusedNode(id);
|
|
176
|
+
navigation.select(route.name);
|
|
177
|
+
} })] }, route.key));
|
|
178
|
+
}) }), sidebar && footer != null && footer !== false && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "sidebar-footer", style: props.sidebarFooterStyle, children: footer }))] }));
|
|
179
|
+
return ((0, jsx_runtime_1.jsx)(builder_1.NavigatorStateContext.Provider, { value: state, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
180
|
+
styles.container,
|
|
181
|
+
sidebar && {
|
|
182
|
+
flexDirection: props.position === 'right' ? 'row-reverse' : 'row',
|
|
183
|
+
},
|
|
184
|
+
props.style,
|
|
185
|
+
], children: [rail, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.content, props.contentStyle], children: items.map(({ route, definition, options }) => ((0, jsx_runtime_1.jsx)(Scene_1.Scene, { nodeId: id, route: route, component: definition.component, visible: route.key === state.activeRouteKey &&
|
|
186
|
+
!options.hidden &&
|
|
187
|
+
!options.disabled, options: options }, route.key))) })] }) }));
|
|
188
|
+
}
|
|
189
|
+
const styles = react_native_1.StyleSheet.create({
|
|
190
|
+
container: { flex: 1 },
|
|
191
|
+
content: { flex: 1, minWidth: 0 },
|
|
192
|
+
tabItems: { flexDirection: 'row' },
|
|
193
|
+
section: { padding: 10, fontSize: 12, fontWeight: '600' },
|
|
194
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type NavigationState, type RouterOptions, type ParamListBase, type RouteName, type NavigationRoute } from '../routers';
|
|
3
|
+
import type { SidebarSectionOptions } from './types';
|
|
4
|
+
import { type AnyNavigation } from './navigation';
|
|
5
|
+
export declare const NavigatorStateContext: React.Context<NavigationState | null>;
|
|
6
|
+
export interface ScreenConfig<P extends ParamListBase, N extends RouteName<P>, O, Nav = AnyNavigation<P>> {
|
|
7
|
+
name: N;
|
|
8
|
+
component: React.ComponentType<{
|
|
9
|
+
navigation: Nav;
|
|
10
|
+
route: NavigationRoute<N, P[N]>;
|
|
11
|
+
}>;
|
|
12
|
+
initialParams?: P[N];
|
|
13
|
+
options?: O | ((context: {
|
|
14
|
+
navigation: Nav;
|
|
15
|
+
route: NavigationRoute<N, P[N]>;
|
|
16
|
+
}) => O);
|
|
17
|
+
}
|
|
18
|
+
export interface Definition<O> {
|
|
19
|
+
name: string;
|
|
20
|
+
component: React.ComponentType<any>;
|
|
21
|
+
initialParams?: unknown;
|
|
22
|
+
options?: O | ((context: {
|
|
23
|
+
navigation: AnyNavigation;
|
|
24
|
+
route: NavigationRoute;
|
|
25
|
+
}) => O);
|
|
26
|
+
section?: SidebarSectionOptions & {
|
|
27
|
+
key: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function readScreens<O>(children: React.ReactNode, Screen: React.ElementType, Section?: React.ElementType): Definition<O>[];
|
|
31
|
+
export declare function useNavigator<S extends NavigationState>(type: S['type'], options: Omit<RouterOptions, 'key'>, explicitId?: string): {
|
|
32
|
+
id: string;
|
|
33
|
+
state: S;
|
|
34
|
+
store: import("./store").NavigationStore;
|
|
35
|
+
navigation: AnyNavigation<ParamListBase>;
|
|
36
|
+
};
|
|
37
|
+
export declare function resolveOptions<O>(definition: Definition<O>, route: NavigationRoute, navigation: AnyNavigation, defaults?: O | ((context: {
|
|
38
|
+
route: NavigationRoute;
|
|
39
|
+
navigation: AnyNavigation;
|
|
40
|
+
}) => O)): O;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NavigatorStateContext = void 0;
|
|
7
|
+
exports.readScreens = readScreens;
|
|
8
|
+
exports.useNavigator = useNavigator;
|
|
9
|
+
exports.resolveOptions = resolveOptions;
|
|
10
|
+
const react_1 = __importDefault(require("react"));
|
|
11
|
+
const routers_1 = require("../routers");
|
|
12
|
+
const context_1 = require("./context");
|
|
13
|
+
const navigation_1 = require("./navigation");
|
|
14
|
+
exports.NavigatorStateContext = react_1.default.createContext(null);
|
|
15
|
+
function readScreens(children, Screen, Section) {
|
|
16
|
+
const result = [];
|
|
17
|
+
function visit(items, section) {
|
|
18
|
+
react_1.default.Children.forEach(items, (child) => {
|
|
19
|
+
if (!react_1.default.isValidElement(child)) {
|
|
20
|
+
if (child != null && typeof child !== 'boolean')
|
|
21
|
+
throw new Error('Navigator children must be Screen elements.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const props = child.props;
|
|
25
|
+
if (child.type === react_1.default.Fragment)
|
|
26
|
+
visit(props.children, section);
|
|
27
|
+
else if (Section && child.type === Section)
|
|
28
|
+
visit(props.children, {
|
|
29
|
+
key: String(child.key ?? result.length),
|
|
30
|
+
title: props.title,
|
|
31
|
+
style: props.style,
|
|
32
|
+
titleStyle: props.titleStyle,
|
|
33
|
+
renderTitle: props.renderTitle,
|
|
34
|
+
});
|
|
35
|
+
else if (child.type === Screen)
|
|
36
|
+
result.push({ ...child.props, section });
|
|
37
|
+
else
|
|
38
|
+
throw new Error('Navigator children must be Screen, Section, or Fragment elements.');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
visit(children);
|
|
42
|
+
if (!result.length ||
|
|
43
|
+
new Set(result.map((r) => r.name)).size !== result.length)
|
|
44
|
+
throw new Error('Navigator requires unique Screen names.');
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function useNavigator(type, options, explicitId) {
|
|
48
|
+
const store = (0, context_1.useStore)();
|
|
49
|
+
const parent = react_1.default.useContext(context_1.ScopeContext);
|
|
50
|
+
const id = parent.nodeId
|
|
51
|
+
? `${parent.nodeId}/${parent.routeKey}/${explicitId ?? type}`
|
|
52
|
+
: (explicitId ?? 'root');
|
|
53
|
+
const config = { ...options, key: id };
|
|
54
|
+
const [initial] = react_1.default.useState(() => store.getNode(id)?.state ??
|
|
55
|
+
routers_1.routers[type].getInitialState(config));
|
|
56
|
+
const getSnapshot = react_1.default.useCallback(() => store.getNode(id)?.state ?? initial, [store, id, initial]);
|
|
57
|
+
const state = react_1.default.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
58
|
+
react_1.default.useLayoutEffect(() => store.register({
|
|
59
|
+
id,
|
|
60
|
+
parentId: parent.nodeId,
|
|
61
|
+
parentRouteKey: parent.routeKey,
|
|
62
|
+
type,
|
|
63
|
+
state: initial,
|
|
64
|
+
}, config), [store, id]);
|
|
65
|
+
react_1.default.useLayoutEffect(() => {
|
|
66
|
+
store.updateOptions(id, config);
|
|
67
|
+
});
|
|
68
|
+
const navigation = react_1.default.useMemo(() => (0, navigation_1.createNavigation)(store, id), [store, id]);
|
|
69
|
+
return { id, state, store, navigation };
|
|
70
|
+
}
|
|
71
|
+
function resolveOptions(definition, route, navigation, defaults) {
|
|
72
|
+
const context = { route, navigation };
|
|
73
|
+
return {
|
|
74
|
+
...(typeof defaults === 'function'
|
|
75
|
+
? defaults(context)
|
|
76
|
+
: defaults),
|
|
77
|
+
...(typeof definition.options === 'function'
|
|
78
|
+
? definition.options(context)
|
|
79
|
+
: definition.options),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavigationStore } from './store';
|
|
3
|
+
import type { NavigationRoute } from '../routers';
|
|
4
|
+
import type { AnyNavigation } from './navigation';
|
|
5
|
+
import type { NavigationTheme } from './types';
|
|
6
|
+
export declare const StoreContext: React.Context<NavigationStore | null>;
|
|
7
|
+
export declare const ScopeContext: React.Context<{
|
|
8
|
+
nodeId?: string;
|
|
9
|
+
routeKey?: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Only a Sidebar directly owned by this Split column may resize it. */
|
|
12
|
+
export declare const SplitColumnContext: React.Context<{
|
|
13
|
+
nodeId: string;
|
|
14
|
+
columnId: string;
|
|
15
|
+
onSidebarChange: (columnId: string, width: number | null) => void;
|
|
16
|
+
} | null>;
|
|
17
|
+
export declare const ScreenContext: React.Context<{
|
|
18
|
+
navigation: AnyNavigation;
|
|
19
|
+
route: NavigationRoute;
|
|
20
|
+
focused: boolean;
|
|
21
|
+
} | null>;
|
|
22
|
+
export declare const PlatformContext: React.Context<import("../platform").NavigationPlatformAdapter>;
|
|
23
|
+
export declare const DefaultTheme: NavigationTheme;
|
|
24
|
+
export declare const ThemeContext: React.Context<NavigationTheme>;
|
|
25
|
+
export declare const useNavigationTheme: () => NavigationTheme;
|
|
26
|
+
export declare function useStore(): NavigationStore;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useNavigationTheme = exports.ThemeContext = exports.DefaultTheme = exports.PlatformContext = exports.ScreenContext = exports.SplitColumnContext = exports.ScopeContext = exports.StoreContext = void 0;
|
|
7
|
+
exports.useStore = useStore;
|
|
8
|
+
const react_1 = __importDefault(require("react"));
|
|
9
|
+
const platform_1 = require("../platform");
|
|
10
|
+
exports.StoreContext = react_1.default.createContext(null);
|
|
11
|
+
exports.ScopeContext = react_1.default.createContext({});
|
|
12
|
+
/** Only a Sidebar directly owned by this Split column may resize it. */
|
|
13
|
+
exports.SplitColumnContext = react_1.default.createContext(null);
|
|
14
|
+
exports.ScreenContext = react_1.default.createContext(null);
|
|
15
|
+
exports.PlatformContext = react_1.default.createContext(platform_1.defaultPlatformAdapter);
|
|
16
|
+
exports.DefaultTheme = {
|
|
17
|
+
dark: false,
|
|
18
|
+
colors: {
|
|
19
|
+
background: '#ffffff',
|
|
20
|
+
surface: '#f5f5f7',
|
|
21
|
+
text: '#202124',
|
|
22
|
+
mutedText: '#64656b',
|
|
23
|
+
border: '#d8d8dc',
|
|
24
|
+
accent: '#0067c0',
|
|
25
|
+
selectedBackground: '#dcecff',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
exports.ThemeContext = react_1.default.createContext(exports.DefaultTheme);
|
|
29
|
+
const useNavigationTheme = () => react_1.default.useContext(exports.ThemeContext);
|
|
30
|
+
exports.useNavigationTheme = useNavigationTheme;
|
|
31
|
+
function useStore() {
|
|
32
|
+
const store = react_1.default.useContext(exports.StoreContext);
|
|
33
|
+
if (!store)
|
|
34
|
+
throw new Error('Navigation must be rendered inside NavigationContainer.');
|
|
35
|
+
return store;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface FocusTarget {
|
|
3
|
+
focus(): void;
|
|
4
|
+
}
|
|
5
|
+
export interface FocusRegistry {
|
|
6
|
+
register(id: string, target: FocusTarget): () => void;
|
|
7
|
+
record(id: string): void;
|
|
8
|
+
}
|
|
9
|
+
export declare const FocusContext: React.Context<FocusRegistry | null>;
|
|
10
|
+
/** Wrap a ref-forwarding native control (Pressable, TextInput, or a custom control). */
|
|
11
|
+
export declare function NavigationFocusable({ id, children, }: {
|
|
12
|
+
id: string;
|
|
13
|
+
children: React.ReactElement<any>;
|
|
14
|
+
}): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|