@gtkx/navigation 0.0.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.
Files changed (147) hide show
  1. package/README.md +174 -0
  2. package/dist/drawer/create-drawer-navigator.d.ts +30 -0
  3. package/dist/drawer/create-drawer-navigator.d.ts.map +1 -0
  4. package/dist/drawer/create-drawer-navigator.js +36 -0
  5. package/dist/drawer/create-drawer-navigator.js.map +1 -0
  6. package/dist/drawer/drawer-collapsed-context.d.ts +4 -0
  7. package/dist/drawer/drawer-collapsed-context.d.ts.map +1 -0
  8. package/dist/drawer/drawer-collapsed-context.js +4 -0
  9. package/dist/drawer/drawer-collapsed-context.js.map +1 -0
  10. package/dist/drawer/drawer-item-list.d.ts +6 -0
  11. package/dist/drawer/drawer-item-list.d.ts.map +1 -0
  12. package/dist/drawer/drawer-item-list.js +40 -0
  13. package/dist/drawer/drawer-item-list.js.map +1 -0
  14. package/dist/drawer/drawer-router.d.ts +5 -0
  15. package/dist/drawer/drawer-router.d.ts.map +1 -0
  16. package/dist/drawer/drawer-router.js +49 -0
  17. package/dist/drawer/drawer-router.js.map +1 -0
  18. package/dist/drawer/drawer-status.d.ts +4 -0
  19. package/dist/drawer/drawer-status.d.ts.map +1 -0
  20. package/dist/drawer/drawer-status.js +11 -0
  21. package/dist/drawer/drawer-status.js.map +1 -0
  22. package/dist/drawer/drawer-view.d.ts +11 -0
  23. package/dist/drawer/drawer-view.d.ts.map +1 -0
  24. package/dist/drawer/drawer-view.js +67 -0
  25. package/dist/drawer/drawer-view.js.map +1 -0
  26. package/dist/drawer/types.d.ts +80 -0
  27. package/dist/drawer/types.d.ts.map +1 -0
  28. package/dist/drawer/types.js +2 -0
  29. package/dist/drawer/types.js.map +1 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +10 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/navigation-container.d.ts +15 -0
  35. package/dist/navigation-container.d.ts.map +1 -0
  36. package/dist/navigation-container.js +11 -0
  37. package/dist/navigation-container.js.map +1 -0
  38. package/dist/shared/header-bar.d.ts +11 -0
  39. package/dist/shared/header-bar.d.ts.map +1 -0
  40. package/dist/shared/header-bar.js +11 -0
  41. package/dist/shared/header-bar.js.map +1 -0
  42. package/dist/shared/routes.d.ts +6 -0
  43. package/dist/shared/routes.d.ts.map +1 -0
  44. package/dist/shared/routes.js +16 -0
  45. package/dist/shared/routes.js.map +1 -0
  46. package/dist/shared/scene-page.d.ts +13 -0
  47. package/dist/shared/scene-page.d.ts.map +1 -0
  48. package/dist/shared/scene-page.js +5 -0
  49. package/dist/shared/scene-page.js.map +1 -0
  50. package/dist/shared/types.d.ts +16 -0
  51. package/dist/shared/types.d.ts.map +1 -0
  52. package/dist/shared/types.js +2 -0
  53. package/dist/shared/types.js.map +1 -0
  54. package/dist/shared/use-loaded-routes.d.ts +3 -0
  55. package/dist/shared/use-loaded-routes.d.ts.map +1 -0
  56. package/dist/shared/use-loaded-routes.js +11 -0
  57. package/dist/shared/use-loaded-routes.js.map +1 -0
  58. package/dist/shared/use-pop-to-top-on-blur.d.ts +10 -0
  59. package/dist/shared/use-pop-to-top-on-blur.d.ts.map +1 -0
  60. package/dist/shared/use-pop-to-top-on-blur.js +27 -0
  61. package/dist/shared/use-pop-to-top-on-blur.js.map +1 -0
  62. package/dist/split-view/create-split-view-navigator.d.ts +33 -0
  63. package/dist/split-view/create-split-view-navigator.d.ts.map +1 -0
  64. package/dist/split-view/create-split-view-navigator.js +20 -0
  65. package/dist/split-view/create-split-view-navigator.js.map +1 -0
  66. package/dist/split-view/split-view-router.d.ts +5 -0
  67. package/dist/split-view/split-view-router.d.ts.map +1 -0
  68. package/dist/split-view/split-view-router.js +42 -0
  69. package/dist/split-view/split-view-router.js.map +1 -0
  70. package/dist/split-view/split-view.d.ts +12 -0
  71. package/dist/split-view/split-view.d.ts.map +1 -0
  72. package/dist/split-view/split-view.js +62 -0
  73. package/dist/split-view/split-view.js.map +1 -0
  74. package/dist/split-view/types.d.ts +43 -0
  75. package/dist/split-view/types.d.ts.map +1 -0
  76. package/dist/split-view/types.js +2 -0
  77. package/dist/split-view/types.js.map +1 -0
  78. package/dist/stack/create-stack-navigator.d.ts +30 -0
  79. package/dist/stack/create-stack-navigator.d.ts.map +1 -0
  80. package/dist/stack/create-stack-navigator.js +16 -0
  81. package/dist/stack/create-stack-navigator.js.map +1 -0
  82. package/dist/stack/stack-pages.d.ts +11 -0
  83. package/dist/stack/stack-pages.d.ts.map +1 -0
  84. package/dist/stack/stack-pages.js +80 -0
  85. package/dist/stack/stack-pages.js.map +1 -0
  86. package/dist/stack/stack-view.d.ts +14 -0
  87. package/dist/stack/stack-view.d.ts.map +1 -0
  88. package/dist/stack/stack-view.js +101 -0
  89. package/dist/stack/stack-view.js.map +1 -0
  90. package/dist/stack/sync-navigation-stack.d.ts +7 -0
  91. package/dist/stack/sync-navigation-stack.d.ts.map +1 -0
  92. package/dist/stack/sync-navigation-stack.js +69 -0
  93. package/dist/stack/sync-navigation-stack.js.map +1 -0
  94. package/dist/stack/types.d.ts +72 -0
  95. package/dist/stack/types.d.ts.map +1 -0
  96. package/dist/stack/types.js +2 -0
  97. package/dist/stack/types.js.map +1 -0
  98. package/dist/static-navigation.d.ts +13 -0
  99. package/dist/static-navigation.d.ts.map +1 -0
  100. package/dist/static-navigation.js +10 -0
  101. package/dist/static-navigation.js.map +1 -0
  102. package/dist/tabs/create-tab-navigator.d.ts +30 -0
  103. package/dist/tabs/create-tab-navigator.d.ts.map +1 -0
  104. package/dist/tabs/create-tab-navigator.js +16 -0
  105. package/dist/tabs/create-tab-navigator.js.map +1 -0
  106. package/dist/tabs/tab-view.d.ts +11 -0
  107. package/dist/tabs/tab-view.d.ts.map +1 -0
  108. package/dist/tabs/tab-view.js +52 -0
  109. package/dist/tabs/tab-view.js.map +1 -0
  110. package/dist/tabs/types.d.ts +67 -0
  111. package/dist/tabs/types.d.ts.map +1 -0
  112. package/dist/tabs/types.js +2 -0
  113. package/dist/tabs/types.js.map +1 -0
  114. package/dist/theme.d.ts +18 -0
  115. package/dist/theme.d.ts.map +1 -0
  116. package/dist/theme.js +15 -0
  117. package/dist/theme.js.map +1 -0
  118. package/package.json +71 -0
  119. package/src/drawer/create-drawer-navigator.tsx +123 -0
  120. package/src/drawer/drawer-collapsed-context.ts +5 -0
  121. package/src/drawer/drawer-item-list.tsx +74 -0
  122. package/src/drawer/drawer-router.ts +90 -0
  123. package/src/drawer/drawer-status.ts +19 -0
  124. package/src/drawer/drawer-view.tsx +171 -0
  125. package/src/drawer/types.ts +142 -0
  126. package/src/index.ts +83 -0
  127. package/src/navigation-container.tsx +35 -0
  128. package/src/shared/header-bar.tsx +34 -0
  129. package/src/shared/routes.ts +25 -0
  130. package/src/shared/scene-page.tsx +27 -0
  131. package/src/shared/types.ts +17 -0
  132. package/src/shared/use-loaded-routes.ts +14 -0
  133. package/src/shared/use-pop-to-top-on-blur.ts +45 -0
  134. package/src/split-view/create-split-view-navigator.tsx +99 -0
  135. package/src/split-view/split-view-router.ts +73 -0
  136. package/src/split-view/split-view.tsx +137 -0
  137. package/src/split-view/types.ts +100 -0
  138. package/src/stack/create-stack-navigator.tsx +80 -0
  139. package/src/stack/stack-pages.ts +140 -0
  140. package/src/stack/stack-view.tsx +202 -0
  141. package/src/stack/sync-navigation-stack.ts +117 -0
  142. package/src/stack/types.ts +131 -0
  143. package/src/static-navigation.tsx +26 -0
  144. package/src/tabs/create-tab-navigator.tsx +69 -0
  145. package/src/tabs/tab-view.tsx +126 -0
  146. package/src/tabs/types.ts +120 -0
  147. package/src/theme.ts +31 -0
@@ -0,0 +1,123 @@
1
+ import type {
2
+ DrawerActionHelpers,
3
+ DrawerNavigationState,
4
+ DrawerRouterOptions,
5
+ NavigatorTypeBagBase,
6
+ ParamListBase,
7
+ StaticConfig,
8
+ StaticScreenFactory,
9
+ TypedNavigator,
10
+ } from "@react-navigation/core";
11
+ import type { ReactNode } from "react";
12
+ import { createNavigatorFactory, createScreenFactory, useNavigationBuilder } from "@react-navigation/core";
13
+ import { useEffect, useState } from "react";
14
+ import type { DrawerRouterFactory } from "./drawer-router.js";
15
+ import type {
16
+ DrawerNavigationEventMap,
17
+ DrawerNavigationOptions,
18
+ DrawerNavigationProp,
19
+ DrawerNavigatorProps,
20
+ } from "./types.js";
21
+ import { createDrawerRouter } from "./drawer-router.js";
22
+ import { DrawerView } from "./drawer-view.js";
23
+
24
+ /** Type bag describing the drawer navigator to React Navigation's typed APIs. */
25
+ type DrawerTypeBag<
26
+ ParamList extends ParamListBase = ParamListBase,
27
+ NavigatorID extends string | undefined = string | undefined,
28
+ > = {
29
+ /** Route names and their params. */
30
+ ParamList: ParamList;
31
+ /** Optional navigator id. */
32
+ NavigatorID: NavigatorID;
33
+ /** Shape of the navigator state. */
34
+ State: DrawerNavigationState<ParamList>;
35
+ /** Per-screen options. */
36
+ ScreenOptions: DrawerNavigationOptions;
37
+ /** Events the navigator emits. */
38
+ EventMap: DrawerNavigationEventMap;
39
+ /** Navigation object per route. */
40
+ NavigationList: {
41
+ [RouteName in keyof ParamList]: DrawerNavigationProp<ParamList, RouteName, NavigatorID>;
42
+ };
43
+ /** The navigator component. */
44
+ Navigator: typeof DrawerNavigator;
45
+ };
46
+
47
+ /** Declares a typed screen of a drawer navigator for the static configuration API. */
48
+ const createDrawerScreen: StaticScreenFactory<DrawerTypeBag> = createScreenFactory<DrawerTypeBag>();
49
+
50
+ const useDrawerRouter = (isCollapsed: boolean): DrawerRouterFactory => {
51
+ const [flag] = useState(() => new CollapsedFlag(isCollapsed));
52
+ const [createRouter] = useState(() => createDrawerRouter(() => flag.isCollapsed));
53
+
54
+ useEffect(() => {
55
+ flag.update(isCollapsed);
56
+ }, [flag, isCollapsed]);
57
+
58
+ return createRouter;
59
+ };
60
+
61
+ /** Renders the screens of a {@link createDrawerNavigator} beside a sidebar in an `AdwOverlaySplitView`. */
62
+ function DrawerNavigator({
63
+ drawerContent,
64
+ collapsed,
65
+ sidebarPosition,
66
+ pinSidebar,
67
+ minSidebarWidth,
68
+ maxSidebarWidth,
69
+ sidebarWidthFraction,
70
+ defaultStatus,
71
+ ...options
72
+ }: DrawerNavigatorProps): ReactNode {
73
+ const createRouter = useDrawerRouter(collapsed === true);
74
+
75
+ const { state, descriptors, navigation, NavigationContent } = useNavigationBuilder<
76
+ DrawerNavigationState<ParamListBase>,
77
+ DrawerRouterOptions,
78
+ DrawerActionHelpers<ParamListBase>,
79
+ DrawerNavigationOptions,
80
+ DrawerNavigationEventMap
81
+ >(createRouter, { ...options, defaultStatus: defaultStatus ?? (collapsed === true ? "closed" : "open") });
82
+
83
+ return (
84
+ <NavigationContent>
85
+ <DrawerView
86
+ state={state}
87
+ navigation={navigation}
88
+ descriptors={descriptors}
89
+ drawerContent={drawerContent}
90
+ collapsed={collapsed}
91
+ sidebarPosition={sidebarPosition}
92
+ pinSidebar={pinSidebar}
93
+ minSidebarWidth={minSidebarWidth}
94
+ maxSidebarWidth={maxSidebarWidth}
95
+ sidebarWidthFraction={sidebarWidthFraction}
96
+ />
97
+ </NavigationContent>
98
+ );
99
+ }
100
+
101
+ /** Creates a drawer navigator rendered with `AdwOverlaySplitView`: a sidebar listing the screens beside the content. */
102
+ function createDrawerNavigator<
103
+ const ParamList extends ParamListBase,
104
+ const NavigatorID extends string | undefined = string | undefined,
105
+ const TypeBag extends NavigatorTypeBagBase = DrawerTypeBag<ParamList, NavigatorID>,
106
+ const Config extends StaticConfig<TypeBag> | undefined = undefined,
107
+ >(config?: Config): TypedNavigator<TypeBag, Config> {
108
+ return createNavigatorFactory(DrawerNavigator)(config) as TypedNavigator<TypeBag, Config>;
109
+ }
110
+
111
+ class CollapsedFlag {
112
+ isCollapsed: boolean;
113
+
114
+ constructor(isCollapsed: boolean) {
115
+ this.isCollapsed = isCollapsed;
116
+ }
117
+
118
+ update(isCollapsed: boolean): void {
119
+ this.isCollapsed = isCollapsed;
120
+ }
121
+ }
122
+
123
+ export { createDrawerNavigator, createDrawerScreen, DrawerNavigator, type DrawerTypeBag };
@@ -0,0 +1,5 @@
1
+ import { type Context, createContext } from "react";
2
+
3
+ const DrawerCollapsedContext: Context<boolean> = createContext(false);
4
+
5
+ export { DrawerCollapsedContext };
@@ -0,0 +1,74 @@
1
+ import type { NavigationState } from "@react-navigation/core";
2
+ import type { ReactNode } from "react";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { GtkBox, GtkImage, GtkLabel, GtkListBox } from "@gtkx/jsx/gtk";
5
+ import { CommonActions, DrawerActions } from "@react-navigation/core";
6
+ import { useCallback, useContext } from "react";
7
+ import type { DrawerContentProps, DrawerDescriptor, DrawerNavigationHelpers } from "./types.js";
8
+ import { requireDescriptor } from "../shared/routes.js";
9
+ import { DrawerCollapsedContext } from "./drawer-collapsed-context.js";
10
+
11
+ type RowActivated = (row: Gtk.ListBoxRow) => void;
12
+
13
+ const didNavigateToRow = (navigation: DrawerNavigationHelpers, state: NavigationState, rowIndex: number): boolean => {
14
+ const route = state.routes[rowIndex];
15
+
16
+ if (route === undefined) {
17
+ return false;
18
+ }
19
+
20
+ const event = navigation.emit({ type: "drawerItemPress", target: route.key, canPreventDefault: true });
21
+
22
+ if (event.defaultPrevented) {
23
+ return false;
24
+ }
25
+
26
+ navigation.dispatch({ ...CommonActions.navigate(route.name, route.params), target: state.key });
27
+
28
+ return true;
29
+ };
30
+
31
+ const useDrawerItemPress = (navigation: DrawerNavigationHelpers, isCollapsed: boolean): RowActivated =>
32
+ useCallback((row: Gtk.ListBoxRow) => {
33
+ const state = navigation.getState();
34
+
35
+ if (!didNavigateToRow(navigation, state, row.getIndex())) {
36
+ return;
37
+ }
38
+
39
+ if (isCollapsed) {
40
+ navigation.dispatch({ ...DrawerActions.closeDrawer(), target: state.key });
41
+ }
42
+ }, [navigation, isCollapsed]);
43
+
44
+ const DrawerItem = ({ descriptor }: { descriptor: DrawerDescriptor }): ReactNode => {
45
+ const { route, options } = descriptor;
46
+
47
+ return (
48
+ <GtkBox spacing={12}>
49
+ {options.drawerIcon === undefined ? null : <GtkImage iconName={options.drawerIcon} />}
50
+ <GtkLabel label={options.drawerLabel ?? options.title ?? route.name} xalign={0} />
51
+ </GtkBox>
52
+ );
53
+ };
54
+
55
+ /** Lists the drawer routes as rows of a `GtkListBox`; activating a row navigates to its route. */
56
+ const DrawerItemList = ({ state, navigation, descriptors }: DrawerContentProps): ReactNode => {
57
+ const isCollapsed = useContext(DrawerCollapsedContext);
58
+ const onRowActivated = useDrawerItemPress(navigation, isCollapsed);
59
+
60
+ return (
61
+ <GtkListBox
62
+ cssClasses={["navigation-sidebar"]}
63
+ selectedIndex={state.index}
64
+ selectionMode={Gtk.SelectionMode.SINGLE}
65
+ onRowActivated={onRowActivated}
66
+ >
67
+ {state.routes.map((route) => (
68
+ <DrawerItem key={route.key} descriptor={requireDescriptor(descriptors, route.key)} />
69
+ ))}
70
+ </GtkListBox>
71
+ );
72
+ };
73
+
74
+ export { DrawerItemList };
@@ -0,0 +1,90 @@
1
+ import type { DrawerNavigationState, NavigationAction, ParamListBase, PartialState } from "@react-navigation/core";
2
+ import { DrawerRouter } from "@react-navigation/routers";
3
+ import { getDrawerStatus } from "./drawer-status.js";
4
+
5
+ type DrawerState = DrawerNavigationState<ParamListBase>;
6
+ type DrawerRouterFactory = typeof DrawerRouter;
7
+ type DrawerRouterInstance = ReturnType<DrawerRouterFactory>;
8
+ type StateResult = DrawerState | PartialState<DrawerState> | null;
9
+ type Retry = (state: DrawerState) => StateResult;
10
+
11
+ type ResolvedAction = {
12
+ state: DrawerState;
13
+ action: NavigationAction;
14
+ after: StateResult;
15
+ isCollapsed: boolean;
16
+ retry: Retry;
17
+ };
18
+
19
+ const NAVIGATING_ACTIONS = new Set(["JUMP_TO", "NAVIGATE", "NAVIGATE_DEPRECATED"]);
20
+
21
+ const isSameRoutes = (left: DrawerState, right: DrawerState): boolean =>
22
+ left.index === right.index &&
23
+ left.routes.length === right.routes.length &&
24
+ left.routes.every((route, index) => right.routes[index]?.key === route.key);
25
+
26
+ const reopenSidebar = (state: DrawerState): DrawerState => {
27
+ if (state.default === "open") {
28
+ return { ...state, history: state.history.filter((entry) => entry.type !== "drawer") };
29
+ }
30
+
31
+ return { ...state, history: [...state.history, { type: "drawer", status: "open" }] };
32
+ };
33
+
34
+ const wasSidebarHidden = (before: DrawerState, after: DrawerState): boolean =>
35
+ getDrawerStatus(before) === "open" && getDrawerStatus(after) === "closed";
36
+
37
+ const wasSidebarShown = (before: DrawerState, after: DrawerState): boolean =>
38
+ getDrawerStatus(before) === "closed" && getDrawerStatus(after) === "open";
39
+
40
+ const keepSidebarShown = (before: DrawerState, after: DrawerState, isCollapsed: boolean): DrawerState =>
41
+ !isCollapsed && wasSidebarHidden(before, after) ? reopenSidebar(after) : after;
42
+
43
+ const keepSidebarShownInResult = (before: DrawerState, after: StateResult, isCollapsed: boolean): StateResult =>
44
+ after?.stale === false ? keepSidebarShown(before, after, isCollapsed) : after;
45
+
46
+ const shouldPassBackThrough = (state: DrawerState, after: StateResult, isCollapsed: boolean): boolean =>
47
+ !isCollapsed && after?.stale === false && isSameRoutes(state, after) && wasSidebarShown(state, after);
48
+
49
+ const withoutDrawerEntry = (state: DrawerState): DrawerState => ({
50
+ ...state,
51
+ history: state.history.filter((entry) => entry.type !== "drawer"),
52
+ });
53
+
54
+ const closedAgain = (state: DrawerState): DrawerState => ({
55
+ ...state,
56
+ history: [...state.history, { type: "drawer", status: "closed" }],
57
+ });
58
+
59
+ const backPastDrawer = (state: DrawerState, retry: Retry): StateResult => {
60
+ const after = retry(withoutDrawerEntry(state));
61
+
62
+ return after?.stale === false ? closedAgain(after) : after;
63
+ };
64
+
65
+ const resolveAction = ({ state, action, after, isCollapsed, retry }: ResolvedAction): StateResult => {
66
+ if (action.type === "GO_BACK") {
67
+ return shouldPassBackThrough(state, after, isCollapsed) ? backPastDrawer(state, retry) : after;
68
+ }
69
+
70
+ return NAVIGATING_ACTIONS.has(action.type) ? keepSidebarShownInResult(state, after, isCollapsed) : after;
71
+ };
72
+
73
+ const withShownSidebar = (router: DrawerRouterInstance, isCollapsed: () => boolean): DrawerRouterInstance => ({
74
+ ...router,
75
+ getStateForAction: (state, action, config) =>
76
+ resolveAction({
77
+ state,
78
+ action,
79
+ after: router.getStateForAction(state, action, config),
80
+ isCollapsed: isCollapsed(),
81
+ retry: (retried) => router.getStateForAction(retried, action, config),
82
+ }),
83
+ getStateForRouteFocus: (state, key) =>
84
+ keepSidebarShown(state, router.getStateForRouteFocus(state, key), isCollapsed()),
85
+ });
86
+
87
+ const createDrawerRouter = (isCollapsed: () => boolean): DrawerRouterFactory => (options) =>
88
+ withShownSidebar(DrawerRouter(options), isCollapsed);
89
+
90
+ export { createDrawerRouter, type DrawerRouterFactory };
@@ -0,0 +1,19 @@
1
+ import type { DrawerStatus, NavigationState } from "@react-navigation/core";
2
+
3
+ type DrawerHistoryEntry = { type: "drawer"; status: DrawerStatus };
4
+
5
+ const isDrawerEntry = (entry: unknown): entry is DrawerHistoryEntry =>
6
+ typeof entry === "object" && entry !== null && "type" in entry && entry.type === "drawer";
7
+
8
+ const getDrawerStatus = (state: NavigationState): DrawerStatus => {
9
+ const history: readonly unknown[] = state.history ?? [];
10
+ const entry = history.findLast(isDrawerEntry);
11
+
12
+ if (entry !== undefined) {
13
+ return entry.status;
14
+ }
15
+
16
+ return "default" in state && state.default === "open" ? "open" : "closed";
17
+ };
18
+
19
+ export { getDrawerStatus };
@@ -0,0 +1,171 @@
1
+ import type { DrawerNavigationState, ParamListBase } from "@react-navigation/core";
2
+ import type { ReactNode } from "react";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import {
5
+ AdwHeaderBar,
6
+ AdwOverlaySplitView,
7
+ AdwToolbarView,
8
+ AdwViewStack,
9
+ AdwWindowTitle,
10
+ } from "@gtkx/jsx/adw";
11
+ import { GtkButton, GtkScrolledWindow } from "@gtkx/jsx/gtk";
12
+ import { DrawerActions } from "@react-navigation/core";
13
+ import { useCallback, useEffect, useRef } from "react";
14
+ import type {
15
+ DrawerContentProps,
16
+ DrawerDescriptor,
17
+ DrawerDescriptorMap,
18
+ DrawerNavigationConfig,
19
+ DrawerNavigationHelpers,
20
+ } from "./types.js";
21
+ import { HeaderBar } from "../shared/header-bar.js";
22
+ import { getFocusedRoute, requireDescriptor } from "../shared/routes.js";
23
+ import { ScenePage } from "../shared/scene-page.js";
24
+ import { useLoadedRoutes } from "../shared/use-loaded-routes.js";
25
+ import { usePopToTopOnBlur } from "../shared/use-pop-to-top-on-blur.js";
26
+ import { DrawerCollapsedContext } from "./drawer-collapsed-context.js";
27
+ import { DrawerItemList } from "./drawer-item-list.js";
28
+ import { getDrawerStatus } from "./drawer-status.js";
29
+
30
+ type DrawerViewProps = DrawerNavigationConfig & {
31
+ state: DrawerNavigationState<ParamListBase>;
32
+ navigation: DrawerNavigationHelpers;
33
+ descriptors: DrawerDescriptorMap;
34
+ };
35
+
36
+ type DrawerHeaderProps = {
37
+ descriptor: DrawerDescriptor;
38
+ navigation: DrawerNavigationHelpers;
39
+ };
40
+
41
+ type DrawerPageProps = {
42
+ descriptor: DrawerDescriptor;
43
+ isLoaded: boolean;
44
+ };
45
+
46
+ const PACK_TYPES = { start: Gtk.PackType.START, end: Gtk.PackType.END } as const;
47
+
48
+ const useSidebarSync = (navigation: DrawerNavigationHelpers): ((isShown: boolean | null) => void) =>
49
+ useCallback((isShown: boolean | null) => {
50
+ const current = navigation.getState();
51
+ const isOpen = getDrawerStatus(current) === "open";
52
+
53
+ if (isShown === null || isShown === isOpen) {
54
+ return;
55
+ }
56
+
57
+ const action = isShown ? DrawerActions.openDrawer() : DrawerActions.closeDrawer();
58
+ navigation.dispatch({ ...action, target: current.key });
59
+ }, [navigation]);
60
+
61
+ const useCloseOnNavigate = (
62
+ state: DrawerNavigationState<ParamListBase>,
63
+ navigation: DrawerNavigationHelpers,
64
+ isCollapsed: boolean,
65
+ ): void => {
66
+ const focusedKey = getFocusedRoute(state).key;
67
+ const previousKeyRef = useRef(focusedKey);
68
+
69
+ useEffect(() => {
70
+ const hasChanged = previousKeyRef.current !== focusedKey;
71
+ previousKeyRef.current = focusedKey;
72
+
73
+ if (hasChanged && isCollapsed && getDrawerStatus(navigation.getState()) === "open") {
74
+ navigation.dispatch({ ...DrawerActions.closeDrawer(), target: navigation.getState().key });
75
+ }
76
+ }, [focusedKey, isCollapsed, navigation]);
77
+ };
78
+
79
+ const DrawerContent = (props: DrawerContentProps): ReactNode => (
80
+ <AdwToolbarView topBar={<AdwHeaderBar />}>
81
+ <GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER}>
82
+ <DrawerItemList {...props} />
83
+ </GtkScrolledWindow>
84
+ </AdwToolbarView>
85
+ );
86
+
87
+ const DrawerToggle = ({ navigation }: { navigation: DrawerNavigationHelpers }): ReactNode => (
88
+ <GtkButton
89
+ iconName="sidebar-show-symbolic"
90
+ tooltipText="Toggle Sidebar"
91
+ accessibleLabel="Toggle Sidebar"
92
+ onClicked={() => {
93
+ navigation.dispatch(DrawerActions.toggleDrawer());
94
+ }}
95
+ />
96
+ );
97
+
98
+ const DrawerHeader = ({ descriptor, navigation }: DrawerHeaderProps): ReactNode => {
99
+ const { route, options } = descriptor;
100
+
101
+ if (options.headerShown === false) {
102
+ return null;
103
+ }
104
+
105
+ if (options.header !== undefined) {
106
+ return <>{options.header({ route, navigation: descriptor.navigation, options })}</>;
107
+ }
108
+
109
+ return (
110
+ <HeaderBar
111
+ options={options}
112
+ start={<DrawerToggle navigation={navigation} />}
113
+ titleWidget={<AdwWindowTitle title={options.title ?? route.name} />}
114
+ />
115
+ );
116
+ };
117
+
118
+ const DrawerPage = ({ descriptor, isLoaded }: DrawerPageProps): ReactNode => {
119
+ const { route, options } = descriptor;
120
+
121
+ return (
122
+ <ScenePage
123
+ name={route.key}
124
+ title={options.title ?? route.name}
125
+ isLoaded={isLoaded || options.lazy === false}
126
+ render={() => descriptor.render()}
127
+ />
128
+ );
129
+ };
130
+
131
+ const DrawerView = ({ state, navigation, descriptors, drawerContent, ...config }: DrawerViewProps): ReactNode => {
132
+ const { collapsed = false, sidebarPosition = "start", pinSidebar, minSidebarWidth, maxSidebarWidth } = config;
133
+ const focused = getFocusedRoute(state);
134
+ const descriptor = requireDescriptor(descriptors, focused.key);
135
+ const loaded = useLoadedRoutes(focused.key, state.preloadedRouteKeys);
136
+ const onShowSidebarChanged = useSidebarSync(navigation);
137
+ const contentProps = { state, navigation, descriptors };
138
+ const sidebar = drawerContent === undefined ? <DrawerContent {...contentProps} /> : drawerContent(contentProps);
139
+ usePopToTopOnBlur(state, descriptors, navigation);
140
+ useCloseOnNavigate(state, navigation, collapsed);
141
+
142
+ return (
143
+ <DrawerCollapsedContext value={collapsed}>
144
+ <AdwOverlaySplitView
145
+ collapsed={collapsed}
146
+ showSidebar={getDrawerStatus(state) === "open"}
147
+ sidebarPosition={PACK_TYPES[sidebarPosition]}
148
+ pinSidebar={pinSidebar}
149
+ minSidebarWidth={minSidebarWidth}
150
+ maxSidebarWidth={maxSidebarWidth}
151
+ sidebarWidthFraction={config.sidebarWidthFraction}
152
+ onNotifyShowSidebar={onShowSidebarChanged}
153
+ sidebar={<>{sidebar}</>}
154
+ >
155
+ <AdwToolbarView topBar={<DrawerHeader descriptor={descriptor} navigation={navigation} />}>
156
+ <AdwViewStack visibleChildName={focused.key}>
157
+ {state.routes.map((route) => (
158
+ <DrawerPage
159
+ key={route.key}
160
+ descriptor={requireDescriptor(descriptors, route.key)}
161
+ isLoaded={loaded.has(route.key)}
162
+ />
163
+ ))}
164
+ </AdwViewStack>
165
+ </AdwToolbarView>
166
+ </AdwOverlaySplitView>
167
+ </DrawerCollapsedContext>
168
+ );
169
+ };
170
+
171
+ export { DrawerView };
@@ -0,0 +1,142 @@
1
+ import type {
2
+ DefaultNavigatorOptions,
3
+ Descriptor,
4
+ DrawerActionHelpers,
5
+ DrawerNavigationState,
6
+ DrawerRouterOptions,
7
+ NavigationHelpers,
8
+ NavigationProp,
9
+ ParamListBase,
10
+ RouteProp,
11
+ } from "@react-navigation/core";
12
+ import type { ReactNode } from "react";
13
+ import type { HeaderOptions } from "../shared/types.js";
14
+
15
+ /** Events the drawer navigator emits on top of React Navigation's core events. */
16
+ type DrawerNavigationEventMap = {
17
+ /** The user activated an item in the sidebar. */
18
+ drawerItemPress: {
19
+ /** No payload. */
20
+ data: undefined;
21
+ /** Calling `preventDefault` keeps the current screen. */
22
+ canPreventDefault: true;
23
+ };
24
+ };
25
+
26
+ /** Per-screen options of the drawer navigator. */
27
+ type DrawerNavigationOptions = HeaderOptions & {
28
+ /** Renders a custom top bar instead of the default `AdwHeaderBar`. */
29
+ header?: (props: DrawerHeaderProps) => ReactNode;
30
+ /** Label shown in the sidebar; defaults to `title`, then to the route name. */
31
+ drawerLabel?: string;
32
+ /** Icon name shown next to the sidebar label. */
33
+ drawerIcon?: string;
34
+ /** Whether the screen mounts on first focus instead of at startup; defaults to `true`. */
35
+ lazy?: boolean;
36
+ /** Whether a nested stack pops to its first screen when this screen loses focus; defaults to `false`. */
37
+ popToTopOnBlur?: boolean;
38
+ };
39
+
40
+ /** Navigation object of a drawer screen. */
41
+ type DrawerNavigationProp<
42
+ ParamList extends ParamListBase,
43
+ RouteName extends keyof ParamList = keyof ParamList,
44
+ NavigatorID extends string | undefined = undefined,
45
+ > = NavigationProp<
46
+ ParamList,
47
+ RouteName,
48
+ NavigatorID,
49
+ DrawerNavigationState<ParamList>,
50
+ DrawerNavigationOptions,
51
+ DrawerNavigationEventMap
52
+ > & DrawerActionHelpers<ParamList>;
53
+
54
+ /** Props a drawer screen component receives. */
55
+ type DrawerScreenProps<
56
+ ParamList extends ParamListBase,
57
+ RouteName extends keyof ParamList = keyof ParamList,
58
+ NavigatorID extends string | undefined = undefined,
59
+ > = {
60
+ /** Navigation object of the screen. */
61
+ navigation: DrawerNavigationProp<ParamList, RouteName, NavigatorID>;
62
+ /** The route the screen renders. */
63
+ route: RouteProp<ParamList, RouteName>;
64
+ };
65
+
66
+ /** Arguments of a custom drawer `header` renderer. */
67
+ type DrawerHeaderProps = {
68
+ /** The focused route. */
69
+ route: RouteProp<ParamListBase>;
70
+ /** Navigation object of the focused route. */
71
+ navigation: DrawerNavigationProp<ParamListBase>;
72
+ /** Resolved options of the focused route. */
73
+ options: DrawerNavigationOptions;
74
+ };
75
+
76
+ /** Navigation helpers of the drawer navigator itself. */
77
+ type DrawerNavigationHelpers = NavigationHelpers<ParamListBase, DrawerNavigationEventMap> &
78
+ DrawerActionHelpers<ParamListBase>;
79
+
80
+ /** Descriptor of one drawer route. */
81
+ type DrawerDescriptor = Descriptor<
82
+ DrawerNavigationOptions,
83
+ DrawerNavigationProp<ParamListBase>,
84
+ RouteProp<ParamListBase>
85
+ >;
86
+
87
+ /** Descriptors of the drawer routes, keyed by route key. */
88
+ type DrawerDescriptorMap = Record<string, DrawerDescriptor>;
89
+
90
+ /** Arguments of a custom `drawerContent` renderer and props of {@link DrawerItemList}. */
91
+ type DrawerContentProps = {
92
+ /** State of the drawer navigator. */
93
+ state: DrawerNavigationState<ParamListBase>;
94
+ /** Navigation helpers of the drawer navigator. */
95
+ navigation: DrawerNavigationHelpers;
96
+ /** Descriptors of the drawer routes. */
97
+ descriptors: DrawerDescriptorMap;
98
+ };
99
+
100
+ /** Props of the drawer navigator beyond the options every navigator shares. */
101
+ type DrawerNavigationConfig = {
102
+ /** Renders the sidebar; defaults to a header bar above a {@link DrawerItemList}. */
103
+ drawerContent?: (props: DrawerContentProps) => ReactNode;
104
+ /** Whether the sidebar overlays the content instead of sitting beside it; defaults to `false`. */
105
+ collapsed?: boolean;
106
+ /** Which side the sidebar sits on; defaults to `"start"`. */
107
+ sidebarPosition?: "start" | "end";
108
+ /** Whether collapsing and uncollapsing leave the sidebar's visibility alone; defaults to `false`. */
109
+ pinSidebar?: boolean;
110
+ /** Minimum sidebar width, in the unit libadwaita uses for the split view. */
111
+ minSidebarWidth?: number;
112
+ /** Maximum sidebar width, in the unit libadwaita uses for the split view. */
113
+ maxSidebarWidth?: number;
114
+ /** Preferred sidebar width as a fraction of the total width. */
115
+ sidebarWidthFraction?: number;
116
+ };
117
+
118
+ /** Props of the drawer `Navigator` component. */
119
+ type DrawerNavigatorProps = DefaultNavigatorOptions<
120
+ ParamListBase,
121
+ string | undefined,
122
+ DrawerNavigationState<ParamListBase>,
123
+ DrawerNavigationOptions,
124
+ DrawerNavigationEventMap,
125
+ DrawerNavigationProp<ParamListBase>
126
+ > &
127
+ DrawerRouterOptions &
128
+ DrawerNavigationConfig;
129
+
130
+ export type {
131
+ DrawerContentProps,
132
+ DrawerDescriptor,
133
+ DrawerDescriptorMap,
134
+ DrawerHeaderProps,
135
+ DrawerNavigationConfig,
136
+ DrawerNavigationEventMap,
137
+ DrawerNavigationHelpers,
138
+ DrawerNavigationOptions,
139
+ DrawerNavigationProp,
140
+ DrawerNavigatorProps,
141
+ DrawerScreenProps,
142
+ };