1import {
2 createNavigatorFactory,
3 StackRouter,
4 useNavigationBuilder,
5 type NavigatorTypeBagBase,
6 type ParamListBase,
7 type StackActionHelpers,
8 type StackNavigationState,
9 type StackRouterOptions,
10 type StaticConfig,
11 type TypedNavigator,
12} from "@react-navigation/native";
13import {
14 NativeStackView,
15 type NativeStackNavigationEventMap,
16 type NativeStackNavigationOptions,
17 type NativeStackNavigatorProps,
18 type NativeStackTypeBag,
19} from "@react-navigation/native-stack";
20import { use, type ComponentProps } from "react";
21
22import { BottomSheetContext } from "../../components/ui/BottomSheetContext";
23import { RoutedFormSheetContext } from "../../components/ui/RoutedFormSheetContext";
24
25import {
26 isPresentedRoute,
27 projectStackPresentation,
28} from "../../native/workspace-stack-projection";
29import {
30 WorkspaceColumnsRendererContext,
31 WorkspacePresentationHostContext,
32} from "./workspace-presentation-context";
33
34type ViewProps = ComponentProps<typeof NativeStackView>;
35type Descriptor = ViewProps["descriptors"][string];
36type Route = ViewProps["state"]["routes"][number];
37
38function WorkspaceStackView(props: ViewProps) {
39 const renderColumns = use(WorkspaceColumnsRendererContext);
40 if (!renderColumns) return <NativeStackView {...props} />;
41
42 const isOverlay = (route: Route) => {
43 const descriptor = props.descriptors[route.key] ?? props.describe(route, true);
44 return isPresentedRoute(descriptor.options);
45 };
46 const projection = projectStackPresentation(props.state, isOverlay);
47 const workspaceRoutes = projection.workspace;
48 const visibleRoute = workspaceRoutes.at(-1);
49 if (!visibleRoute) return <NativeStackView {...props} />;
50
51 const visibleDescriptor = props.descriptors[visibleRoute.key]!;
52 // Home is the sidebar beside the pages, and under them the page shown when none is selected.
53 const home = workspaceRoutes.find((route) => route.name === "Home");
54 const baseRoute = { key: `${props.state.key}:workspace`, name: "Workspace" };
55 const baseDescriptor: Descriptor = {
56 ...visibleDescriptor,
57 options: { headerShown: false, freezeOnBlur: false },
58 render: () =>
59 renderColumns(
60 <NativeStackView
61 {...props}
62 state={{
63 ...props.state,
64 routes: workspaceRoutes,
65 index: workspaceRoutes.length - 1,
66 preloadedRoutes: props.state.preloadedRoutes.filter((route) => !isOverlay(route)),
67 }}
68 />,
69 home ? props.descriptors[home.key]?.render() : null,
70 ),
71 };
72 const routes = [baseRoute, ...projection.overlays];
73 return (
74 <NativeStackView
75 {...props}
76 presentationEnvelope
77 state={{
78 ...props.state,
79 routes,
80 index: routes.length - 1,
81 preloadedRoutes: props.state.preloadedRoutes.filter(isOverlay),
82 }}
83 descriptors={{ ...props.descriptors, [baseRoute.key]: baseDescriptor }}
84 />
85 );
86}
87
88function WorkspaceStackNavigator({
89 id,
90 initialRouteName,
91 UNSTABLE_routeNamesChangeBehavior,
92 children,
93 layout,
94 screenListeners,
95 screenOptions,
96 screenLayout,
97 UNSTABLE_router,
98 ...rest
99}: NativeStackNavigatorProps) {
100 const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder<
101 StackNavigationState<ParamListBase>,
102 StackRouterOptions,
103 StackActionHelpers<ParamListBase>,
104 NativeStackNavigationOptions,
105 NativeStackNavigationEventMap
106 >(StackRouter, {
107 id,
108 initialRouteName,
109 UNSTABLE_routeNamesChangeBehavior,
110 children,
111 layout,
112 screenListeners,
113 screenOptions,
114 screenLayout,
115 UNSTABLE_router,
116 });
117 return (
118 <WorkspacePresentationHostContext value>
119 <NavigationContent>
120 <WorkspaceStackView
121 {...rest}
122 state={state}
123 describe={describe}
124 descriptors={Object.fromEntries(
125 Object.entries(descriptors).map(([key, descriptor]) => [
126 key,
127 {
128 ...descriptor,
129 // Screens draw their bar in-tree; hiding the native one only after mount
130 // shows it for the first frame and leaves the page measured short.
131 options: { ...descriptor.options, headerShown: false },
132 ...(descriptor.options.presentation === "formSheet"
133 ? {
134 render: () => (
135 <BottomSheetContext value>
136 <RoutedFormSheetContext value>
137 {descriptor.render()}
138 </RoutedFormSheetContext>
139 </BottomSheetContext>
140 ),
141 }
142 : undefined),
143 },
144 ]),
145 )}
146 navigation={navigation}
147 />
148 </NavigationContent>
149 </WorkspacePresentationHostContext>
150 );
151}
152
153type WorkspaceTypeBag<
154 ParamList extends ParamListBase,
155 NavigatorID extends string | undefined,
156> = Omit<NativeStackTypeBag<ParamList, NavigatorID>, "Navigator"> & {
157 Navigator: typeof WorkspaceStackNavigator;
158};
159
160export function createWorkspaceStackNavigator<
161 const ParamList extends ParamListBase,
162 const NavigatorID extends string | undefined = string | undefined,
163 const TypeBag extends NavigatorTypeBagBase = WorkspaceTypeBag<ParamList, NavigatorID>,
164 const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
165>(config?: Config): TypedNavigator<TypeBag, Config> {
166 return createNavigatorFactory(WorkspaceStackNavigator)(config);
167}