@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.
- package/README.md +174 -0
- package/dist/drawer/create-drawer-navigator.d.ts +30 -0
- package/dist/drawer/create-drawer-navigator.d.ts.map +1 -0
- package/dist/drawer/create-drawer-navigator.js +36 -0
- package/dist/drawer/create-drawer-navigator.js.map +1 -0
- package/dist/drawer/drawer-collapsed-context.d.ts +4 -0
- package/dist/drawer/drawer-collapsed-context.d.ts.map +1 -0
- package/dist/drawer/drawer-collapsed-context.js +4 -0
- package/dist/drawer/drawer-collapsed-context.js.map +1 -0
- package/dist/drawer/drawer-item-list.d.ts +6 -0
- package/dist/drawer/drawer-item-list.d.ts.map +1 -0
- package/dist/drawer/drawer-item-list.js +40 -0
- package/dist/drawer/drawer-item-list.js.map +1 -0
- package/dist/drawer/drawer-router.d.ts +5 -0
- package/dist/drawer/drawer-router.d.ts.map +1 -0
- package/dist/drawer/drawer-router.js +49 -0
- package/dist/drawer/drawer-router.js.map +1 -0
- package/dist/drawer/drawer-status.d.ts +4 -0
- package/dist/drawer/drawer-status.d.ts.map +1 -0
- package/dist/drawer/drawer-status.js +11 -0
- package/dist/drawer/drawer-status.js.map +1 -0
- package/dist/drawer/drawer-view.d.ts +11 -0
- package/dist/drawer/drawer-view.d.ts.map +1 -0
- package/dist/drawer/drawer-view.js +67 -0
- package/dist/drawer/drawer-view.js.map +1 -0
- package/dist/drawer/types.d.ts +80 -0
- package/dist/drawer/types.d.ts.map +1 -0
- package/dist/drawer/types.js +2 -0
- package/dist/drawer/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation-container.d.ts +15 -0
- package/dist/navigation-container.d.ts.map +1 -0
- package/dist/navigation-container.js +11 -0
- package/dist/navigation-container.js.map +1 -0
- package/dist/shared/header-bar.d.ts +11 -0
- package/dist/shared/header-bar.d.ts.map +1 -0
- package/dist/shared/header-bar.js +11 -0
- package/dist/shared/header-bar.js.map +1 -0
- package/dist/shared/routes.d.ts +6 -0
- package/dist/shared/routes.d.ts.map +1 -0
- package/dist/shared/routes.js +16 -0
- package/dist/shared/routes.js.map +1 -0
- package/dist/shared/scene-page.d.ts +13 -0
- package/dist/shared/scene-page.d.ts.map +1 -0
- package/dist/shared/scene-page.js +5 -0
- package/dist/shared/scene-page.js.map +1 -0
- package/dist/shared/types.d.ts +16 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/use-loaded-routes.d.ts +3 -0
- package/dist/shared/use-loaded-routes.d.ts.map +1 -0
- package/dist/shared/use-loaded-routes.js +11 -0
- package/dist/shared/use-loaded-routes.js.map +1 -0
- package/dist/shared/use-pop-to-top-on-blur.d.ts +10 -0
- package/dist/shared/use-pop-to-top-on-blur.d.ts.map +1 -0
- package/dist/shared/use-pop-to-top-on-blur.js +27 -0
- package/dist/shared/use-pop-to-top-on-blur.js.map +1 -0
- package/dist/split-view/create-split-view-navigator.d.ts +33 -0
- package/dist/split-view/create-split-view-navigator.d.ts.map +1 -0
- package/dist/split-view/create-split-view-navigator.js +20 -0
- package/dist/split-view/create-split-view-navigator.js.map +1 -0
- package/dist/split-view/split-view-router.d.ts +5 -0
- package/dist/split-view/split-view-router.d.ts.map +1 -0
- package/dist/split-view/split-view-router.js +42 -0
- package/dist/split-view/split-view-router.js.map +1 -0
- package/dist/split-view/split-view.d.ts +12 -0
- package/dist/split-view/split-view.d.ts.map +1 -0
- package/dist/split-view/split-view.js +62 -0
- package/dist/split-view/split-view.js.map +1 -0
- package/dist/split-view/types.d.ts +43 -0
- package/dist/split-view/types.d.ts.map +1 -0
- package/dist/split-view/types.js +2 -0
- package/dist/split-view/types.js.map +1 -0
- package/dist/stack/create-stack-navigator.d.ts +30 -0
- package/dist/stack/create-stack-navigator.d.ts.map +1 -0
- package/dist/stack/create-stack-navigator.js +16 -0
- package/dist/stack/create-stack-navigator.js.map +1 -0
- package/dist/stack/stack-pages.d.ts +11 -0
- package/dist/stack/stack-pages.d.ts.map +1 -0
- package/dist/stack/stack-pages.js +80 -0
- package/dist/stack/stack-pages.js.map +1 -0
- package/dist/stack/stack-view.d.ts +14 -0
- package/dist/stack/stack-view.d.ts.map +1 -0
- package/dist/stack/stack-view.js +101 -0
- package/dist/stack/stack-view.js.map +1 -0
- package/dist/stack/sync-navigation-stack.d.ts +7 -0
- package/dist/stack/sync-navigation-stack.d.ts.map +1 -0
- package/dist/stack/sync-navigation-stack.js +69 -0
- package/dist/stack/sync-navigation-stack.js.map +1 -0
- package/dist/stack/types.d.ts +72 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +2 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/static-navigation.d.ts +13 -0
- package/dist/static-navigation.d.ts.map +1 -0
- package/dist/static-navigation.js +10 -0
- package/dist/static-navigation.js.map +1 -0
- package/dist/tabs/create-tab-navigator.d.ts +30 -0
- package/dist/tabs/create-tab-navigator.d.ts.map +1 -0
- package/dist/tabs/create-tab-navigator.js +16 -0
- package/dist/tabs/create-tab-navigator.js.map +1 -0
- package/dist/tabs/tab-view.d.ts +11 -0
- package/dist/tabs/tab-view.d.ts.map +1 -0
- package/dist/tabs/tab-view.js +52 -0
- package/dist/tabs/tab-view.js.map +1 -0
- package/dist/tabs/types.d.ts +67 -0
- package/dist/tabs/types.d.ts.map +1 -0
- package/dist/tabs/types.js +2 -0
- package/dist/tabs/types.js.map +1 -0
- package/dist/theme.d.ts +18 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +15 -0
- package/dist/theme.js.map +1 -0
- package/package.json +71 -0
- package/src/drawer/create-drawer-navigator.tsx +123 -0
- package/src/drawer/drawer-collapsed-context.ts +5 -0
- package/src/drawer/drawer-item-list.tsx +74 -0
- package/src/drawer/drawer-router.ts +90 -0
- package/src/drawer/drawer-status.ts +19 -0
- package/src/drawer/drawer-view.tsx +171 -0
- package/src/drawer/types.ts +142 -0
- package/src/index.ts +83 -0
- package/src/navigation-container.tsx +35 -0
- package/src/shared/header-bar.tsx +34 -0
- package/src/shared/routes.ts +25 -0
- package/src/shared/scene-page.tsx +27 -0
- package/src/shared/types.ts +17 -0
- package/src/shared/use-loaded-routes.ts +14 -0
- package/src/shared/use-pop-to-top-on-blur.ts +45 -0
- package/src/split-view/create-split-view-navigator.tsx +99 -0
- package/src/split-view/split-view-router.ts +73 -0
- package/src/split-view/split-view.tsx +137 -0
- package/src/split-view/types.ts +100 -0
- package/src/stack/create-stack-navigator.tsx +80 -0
- package/src/stack/stack-pages.ts +140 -0
- package/src/stack/stack-view.tsx +202 -0
- package/src/stack/sync-navigation-stack.ts +117 -0
- package/src/stack/types.ts +131 -0
- package/src/static-navigation.tsx +26 -0
- package/src/tabs/create-tab-navigator.tsx +69 -0
- package/src/tabs/tab-view.tsx +126 -0
- package/src/tabs/types.ts +120 -0
- package/src/theme.ts +31 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createDrawerNavigator,
|
|
3
|
+
createDrawerScreen,
|
|
4
|
+
DrawerNavigator,
|
|
5
|
+
type DrawerTypeBag,
|
|
6
|
+
} from "./drawer/create-drawer-navigator.js";
|
|
7
|
+
export { DrawerItemList } from "./drawer/drawer-item-list.js";
|
|
8
|
+
export type {
|
|
9
|
+
DrawerContentProps,
|
|
10
|
+
DrawerDescriptor,
|
|
11
|
+
DrawerDescriptorMap,
|
|
12
|
+
DrawerHeaderProps,
|
|
13
|
+
DrawerNavigationConfig,
|
|
14
|
+
DrawerNavigationEventMap,
|
|
15
|
+
DrawerNavigationHelpers,
|
|
16
|
+
DrawerNavigationOptions,
|
|
17
|
+
DrawerNavigationProp,
|
|
18
|
+
DrawerNavigatorProps,
|
|
19
|
+
DrawerScreenProps,
|
|
20
|
+
} from "./drawer/types.js";
|
|
21
|
+
export {
|
|
22
|
+
NavigationContainer,
|
|
23
|
+
type NavigationContainerComponent,
|
|
24
|
+
type NavigationContainerProps,
|
|
25
|
+
} from "./navigation-container.js";
|
|
26
|
+
export type { HeaderOptions } from "./shared/types.js";
|
|
27
|
+
export {
|
|
28
|
+
createSplitViewNavigator,
|
|
29
|
+
createSplitViewScreen,
|
|
30
|
+
SplitViewNavigator,
|
|
31
|
+
type SplitViewTypeBag,
|
|
32
|
+
} from "./split-view/create-split-view-navigator.js";
|
|
33
|
+
export type {
|
|
34
|
+
SplitViewDescriptor,
|
|
35
|
+
SplitViewDescriptorMap,
|
|
36
|
+
SplitViewNavigationConfig,
|
|
37
|
+
SplitViewNavigationEventMap,
|
|
38
|
+
SplitViewNavigationHelpers,
|
|
39
|
+
SplitViewNavigationOptions,
|
|
40
|
+
SplitViewNavigationProp,
|
|
41
|
+
SplitViewNavigatorProps,
|
|
42
|
+
SplitViewScreenProps,
|
|
43
|
+
} from "./split-view/types.js";
|
|
44
|
+
export {
|
|
45
|
+
createStackNavigator,
|
|
46
|
+
createStackScreen,
|
|
47
|
+
StackNavigator,
|
|
48
|
+
type StackTypeBag,
|
|
49
|
+
} from "./stack/create-stack-navigator.js";
|
|
50
|
+
export type {
|
|
51
|
+
StackDescriptor,
|
|
52
|
+
StackDescriptorMap,
|
|
53
|
+
StackHeaderBack,
|
|
54
|
+
StackHeaderProps,
|
|
55
|
+
StackNavigationConfig,
|
|
56
|
+
StackNavigationEventMap,
|
|
57
|
+
StackNavigationHelpers,
|
|
58
|
+
StackNavigationOptions,
|
|
59
|
+
StackNavigationProp,
|
|
60
|
+
StackNavigatorProps,
|
|
61
|
+
StackScreenProps,
|
|
62
|
+
StackTransitionData,
|
|
63
|
+
} from "./stack/types.js";
|
|
64
|
+
export {
|
|
65
|
+
createStaticNavigation,
|
|
66
|
+
type StaticNavigationProps,
|
|
67
|
+
type StaticNavigationTree,
|
|
68
|
+
} from "./static-navigation.js";
|
|
69
|
+
export { createTabNavigator, createTabScreen, TabNavigator, type TabTypeBag } from "./tabs/create-tab-navigator.js";
|
|
70
|
+
export type {
|
|
71
|
+
TabDescriptor,
|
|
72
|
+
TabDescriptorMap,
|
|
73
|
+
TabHeaderProps,
|
|
74
|
+
TabNavigationConfig,
|
|
75
|
+
TabNavigationEventMap,
|
|
76
|
+
TabNavigationHelpers,
|
|
77
|
+
TabNavigationOptions,
|
|
78
|
+
TabNavigationProp,
|
|
79
|
+
TabNavigatorProps,
|
|
80
|
+
TabScreenProps,
|
|
81
|
+
} from "./tabs/types.js";
|
|
82
|
+
export { DarkTheme, DefaultTheme, type Theme } from "./theme.js";
|
|
83
|
+
export * from "@react-navigation/core";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigationContainerProps as CoreContainerProps,
|
|
3
|
+
NavigationContainerRef,
|
|
4
|
+
ParamListBase,
|
|
5
|
+
} from "@react-navigation/core";
|
|
6
|
+
import type { ReactNode, Ref } from "react";
|
|
7
|
+
import { BaseNavigationContainer } from "@react-navigation/core";
|
|
8
|
+
import { useAdwaitaTheme } from "./theme.js";
|
|
9
|
+
|
|
10
|
+
/** Props of {@link NavigationContainer}. */
|
|
11
|
+
type NavigationContainerProps<ParamList extends object = ReactNavigation.RootParamList> = Omit<
|
|
12
|
+
CoreContainerProps,
|
|
13
|
+
"navigationInChildEnabled" | "theme"
|
|
14
|
+
> & {
|
|
15
|
+
/** Theme handed to `useTheme` and option callbacks; defaults to the live Adwaita style state. */
|
|
16
|
+
theme?: ReactNavigation.Theme;
|
|
17
|
+
/** Receives the container's imperative navigation API. */
|
|
18
|
+
ref?: Ref<NavigationContainerRef<ParamList>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The {@link NavigationContainer} component, generic over the root param list. */
|
|
22
|
+
type NavigationContainerComponent = <ParamList extends object = ReactNavigation.RootParamList>(
|
|
23
|
+
props: NavigationContainerProps<ParamList>,
|
|
24
|
+
) => ReactNode;
|
|
25
|
+
|
|
26
|
+
/** Hosts the navigation tree: render it once at the root, around the navigators. */
|
|
27
|
+
const NavigationContainer: NavigationContainerComponent = NavigationContainerInner as NavigationContainerComponent;
|
|
28
|
+
|
|
29
|
+
function NavigationContainerInner({ ref, theme, ...rest }: NavigationContainerProps<ParamListBase>): ReactNode {
|
|
30
|
+
const adwaitaTheme = useAdwaitaTheme();
|
|
31
|
+
|
|
32
|
+
return <BaseNavigationContainer {...rest} ref={ref} theme={theme ?? adwaitaTheme} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { NavigationContainer, type NavigationContainerComponent, type NavigationContainerProps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { AdwHeaderBar, AdwWindowTitle } from "@gtkx/jsx/adw";
|
|
3
|
+
import type { HeaderOptions } from "./types.js";
|
|
4
|
+
|
|
5
|
+
type HeaderBarProps = {
|
|
6
|
+
options: HeaderOptions;
|
|
7
|
+
titleWidget?: ReactElement;
|
|
8
|
+
start?: ReactNode;
|
|
9
|
+
showBackButton?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const resolveTitleWidget = (options: HeaderOptions, fallback: ReactElement | undefined): ReactElement | undefined => {
|
|
13
|
+
if (typeof options.headerTitle === "string") {
|
|
14
|
+
return <AdwWindowTitle title={options.headerTitle} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return options.headerTitle ?? fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const HeaderBar = ({ options, titleWidget, start, showBackButton }: HeaderBarProps): ReactNode => (
|
|
21
|
+
<AdwHeaderBar
|
|
22
|
+
showBackButton={showBackButton ?? true}
|
|
23
|
+
titleWidget={resolveTitleWidget(options, titleWidget)}
|
|
24
|
+
start={(
|
|
25
|
+
<>
|
|
26
|
+
{start}
|
|
27
|
+
{options.headerStart}
|
|
28
|
+
</>
|
|
29
|
+
)}
|
|
30
|
+
end={options.headerEnd}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export { HeaderBar };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NavigationState } from "@react-navigation/core";
|
|
2
|
+
|
|
3
|
+
type FocusedRoute<State extends NavigationState> = State["routes"][number];
|
|
4
|
+
|
|
5
|
+
const getFocusedRoute = <State extends NavigationState>(state: State): FocusedRoute<State> => {
|
|
6
|
+
const route = state.routes[state.index];
|
|
7
|
+
|
|
8
|
+
if (route === undefined) {
|
|
9
|
+
throw new Error(`Navigation state has no route at index ${String(state.index)}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return route;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const requireDescriptor = <Descriptor>(descriptors: Record<string, Descriptor>, key: string): Descriptor => {
|
|
16
|
+
const descriptor = descriptors[key];
|
|
17
|
+
|
|
18
|
+
if (descriptor === undefined) {
|
|
19
|
+
throw new Error(`No descriptor for route "${key}"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return descriptor;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { getFocusedRoute, requireDescriptor };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { AdwBin, AdwViewStackPage } from "@gtkx/jsx/adw";
|
|
3
|
+
|
|
4
|
+
type ScenePageProps = {
|
|
5
|
+
name: string;
|
|
6
|
+
title: string;
|
|
7
|
+
iconName?: string;
|
|
8
|
+
badgeNumber?: number;
|
|
9
|
+
needsAttention?: boolean;
|
|
10
|
+
isLoaded: boolean;
|
|
11
|
+
render: () => ReactElement;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ScenePage = ({ name, title, isLoaded, render, ...page }: ScenePageProps): ReactNode => (
|
|
15
|
+
<AdwViewStackPage
|
|
16
|
+
name={name}
|
|
17
|
+
title={title}
|
|
18
|
+
iconName={page.iconName}
|
|
19
|
+
badgeNumber={page.badgeNumber}
|
|
20
|
+
needsAttention={page.needsAttention}
|
|
21
|
+
useUnderline={false}
|
|
22
|
+
>
|
|
23
|
+
<AdwBin>{isLoaded ? render() : null}</AdwBin>
|
|
24
|
+
</AdwViewStackPage>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export { ScenePage };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** Header options shared by every navigator. */
|
|
4
|
+
type HeaderOptions = {
|
|
5
|
+
/** Title of the screen, shown in the header bar; defaults to the route name. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Whether to render a header bar above the screen; defaults to `true`. */
|
|
8
|
+
headerShown?: boolean;
|
|
9
|
+
/** Replaces the header bar title: a string becomes an `AdwWindowTitle`, an element is used as the title widget. */
|
|
10
|
+
headerTitle?: string | ReactElement;
|
|
11
|
+
/** Widgets packed at the start of the header bar. */
|
|
12
|
+
headerStart?: ReactNode;
|
|
13
|
+
/** Widgets packed at the end of the header bar. */
|
|
14
|
+
headerEnd?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type { HeaderOptions };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
const useLoadedRoutes = (focusedKey: string, preloadedKeys: readonly string[]): ReadonlySet<string> => {
|
|
4
|
+
const [loaded, setLoaded] = useState<ReadonlySet<string>>(() => new Set([focusedKey]));
|
|
5
|
+
const pending = [focusedKey, ...preloadedKeys].filter((key) => !loaded.has(key));
|
|
6
|
+
|
|
7
|
+
if (pending.length > 0) {
|
|
8
|
+
setLoaded(new Set([...loaded, ...pending]));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return loaded;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { useLoadedRoutes };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NavigationHelpers, NavigationState, ParamListBase } from "@react-navigation/core";
|
|
2
|
+
import { StackActions } from "@react-navigation/core";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
type BlurOptions = { popToTopOnBlur?: boolean };
|
|
6
|
+
type BlurDescriptors = Record<string, { options: BlurOptions }>;
|
|
7
|
+
|
|
8
|
+
const nestedStackKey = (state: NavigationState, routeKey: string): string | null => {
|
|
9
|
+
const nested = state.routes.find((route) => route.key === routeKey)?.state;
|
|
10
|
+
|
|
11
|
+
if (nested?.type === "stack" && typeof nested.key === "string") {
|
|
12
|
+
return nested.key;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const popToTopTarget = (state: NavigationState, descriptors: BlurDescriptors, blurredKey: string): string | null =>
|
|
19
|
+
descriptors[blurredKey]?.options.popToTopOnBlur === true ? nestedStackKey(state, blurredKey) : null;
|
|
20
|
+
|
|
21
|
+
const usePopToTopOnBlur = (
|
|
22
|
+
state: NavigationState,
|
|
23
|
+
descriptors: BlurDescriptors,
|
|
24
|
+
navigation: NavigationHelpers<ParamListBase>,
|
|
25
|
+
): void => {
|
|
26
|
+
const focusedKey = state.routes[state.index]?.key;
|
|
27
|
+
const previousKeyRef = useRef(focusedKey);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const previousKey = previousKeyRef.current;
|
|
31
|
+
previousKeyRef.current = focusedKey;
|
|
32
|
+
|
|
33
|
+
if (previousKey === undefined || previousKey === focusedKey) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const target = popToTopTarget(state, descriptors, previousKey);
|
|
38
|
+
|
|
39
|
+
if (target !== null) {
|
|
40
|
+
navigation.dispatch({ ...StackActions.popToTop(), target });
|
|
41
|
+
}
|
|
42
|
+
}, [focusedKey, descriptors, navigation, state]);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export { usePopToTopOnBlur };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigatorTypeBagBase,
|
|
3
|
+
ParamListBase,
|
|
4
|
+
StackActionHelpers,
|
|
5
|
+
StackNavigationState,
|
|
6
|
+
StackRouterOptions,
|
|
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 type {
|
|
14
|
+
SplitViewNavigationEventMap,
|
|
15
|
+
SplitViewNavigationOptions,
|
|
16
|
+
SplitViewNavigationProp,
|
|
17
|
+
SplitViewNavigatorProps,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import { splitViewRouter } from "./split-view-router.js";
|
|
20
|
+
import { SplitView } from "./split-view.js";
|
|
21
|
+
|
|
22
|
+
/** Type bag describing the split view navigator to React Navigation's typed APIs. */
|
|
23
|
+
type SplitViewTypeBag<
|
|
24
|
+
ParamList extends ParamListBase = ParamListBase,
|
|
25
|
+
NavigatorID extends string | undefined = string | undefined,
|
|
26
|
+
> = {
|
|
27
|
+
/** Route names and their params. */
|
|
28
|
+
ParamList: ParamList;
|
|
29
|
+
/** Optional navigator id. */
|
|
30
|
+
NavigatorID: NavigatorID;
|
|
31
|
+
/** Shape of the navigator state. */
|
|
32
|
+
State: StackNavigationState<ParamList>;
|
|
33
|
+
/** Per-screen options. */
|
|
34
|
+
ScreenOptions: SplitViewNavigationOptions;
|
|
35
|
+
/** Events the navigator emits. */
|
|
36
|
+
EventMap: SplitViewNavigationEventMap;
|
|
37
|
+
/** Navigation object per route. */
|
|
38
|
+
NavigationList: {
|
|
39
|
+
[RouteName in keyof ParamList]: SplitViewNavigationProp<ParamList, RouteName, NavigatorID>;
|
|
40
|
+
};
|
|
41
|
+
/** The navigator component. */
|
|
42
|
+
Navigator: typeof SplitViewNavigator;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Declares a typed screen of a split view navigator for the static configuration API. */
|
|
46
|
+
const createSplitViewScreen: StaticScreenFactory<SplitViewTypeBag> = createScreenFactory<SplitViewTypeBag>();
|
|
47
|
+
|
|
48
|
+
/** Renders the screens of a {@link createSplitViewNavigator} in an `AdwNavigationSplitView`. */
|
|
49
|
+
function SplitViewNavigator({
|
|
50
|
+
collapsed,
|
|
51
|
+
contentPlaceholder,
|
|
52
|
+
sidebarPosition,
|
|
53
|
+
minSidebarWidth,
|
|
54
|
+
maxSidebarWidth,
|
|
55
|
+
sidebarWidthFraction,
|
|
56
|
+
popOnEscape,
|
|
57
|
+
...options
|
|
58
|
+
}: SplitViewNavigatorProps): ReactNode {
|
|
59
|
+
const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder<
|
|
60
|
+
StackNavigationState<ParamListBase>,
|
|
61
|
+
StackRouterOptions,
|
|
62
|
+
StackActionHelpers<ParamListBase>,
|
|
63
|
+
SplitViewNavigationOptions,
|
|
64
|
+
SplitViewNavigationEventMap
|
|
65
|
+
>(splitViewRouter, options);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<NavigationContent>
|
|
69
|
+
<SplitView
|
|
70
|
+
state={state}
|
|
71
|
+
navigation={navigation}
|
|
72
|
+
descriptors={descriptors}
|
|
73
|
+
describe={describe}
|
|
74
|
+
collapsed={collapsed}
|
|
75
|
+
contentPlaceholder={contentPlaceholder}
|
|
76
|
+
sidebarPosition={sidebarPosition}
|
|
77
|
+
minSidebarWidth={minSidebarWidth}
|
|
78
|
+
maxSidebarWidth={maxSidebarWidth}
|
|
79
|
+
sidebarWidthFraction={sidebarWidthFraction}
|
|
80
|
+
popOnEscape={popOnEscape}
|
|
81
|
+
/>
|
|
82
|
+
</NavigationContent>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a master/detail navigator rendered with `AdwNavigationSplitView`: the first screen fills the sidebar
|
|
88
|
+
* pane, the rest are a stack in the content pane, and a narrow window collapses the two into one.
|
|
89
|
+
*/
|
|
90
|
+
function createSplitViewNavigator<
|
|
91
|
+
const ParamList extends ParamListBase,
|
|
92
|
+
const NavigatorID extends string | undefined = string | undefined,
|
|
93
|
+
const TypeBag extends NavigatorTypeBagBase = SplitViewTypeBag<ParamList, NavigatorID>,
|
|
94
|
+
const Config extends StaticConfig<TypeBag> | undefined = undefined,
|
|
95
|
+
>(config?: Config): TypedNavigator<TypeBag, Config> {
|
|
96
|
+
return createNavigatorFactory(SplitViewNavigator)(config) as TypedNavigator<TypeBag, Config>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { createSplitViewNavigator, createSplitViewScreen, SplitViewNavigator, type SplitViewTypeBag };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ParamListBase, RouterConfigOptions, StackNavigationState } from "@react-navigation/core";
|
|
2
|
+
import { StackRouter } from "@react-navigation/routers";
|
|
3
|
+
|
|
4
|
+
type StackState = StackNavigationState<ParamListBase>;
|
|
5
|
+
type StackRoute = StackState["routes"][number];
|
|
6
|
+
type StackRouterFactory = typeof StackRouter;
|
|
7
|
+
type StackRouterInstance = ReturnType<StackRouterFactory>;
|
|
8
|
+
type SidebarSlot = { route: StackRoute | undefined; serial: number };
|
|
9
|
+
type RouterAction = Parameters<StackRouterInstance["getStateForAction"]>[1];
|
|
10
|
+
|
|
11
|
+
const sidebarName = (config: RouterConfigOptions): string | undefined => config.routeNames[0];
|
|
12
|
+
|
|
13
|
+
const buildSidebarRoute = (slot: SidebarSlot, config: RouterConfigOptions, name: string): StackRoute => {
|
|
14
|
+
slot.serial += 1;
|
|
15
|
+
|
|
16
|
+
return { key: `${name}-sidebar-${String(slot.serial)}`, name, params: config.routeParamList[name] };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const resolveSidebarRoute = (slot: SidebarSlot, state: StackState, config: RouterConfigOptions): StackRoute => {
|
|
20
|
+
const name = sidebarName(config) ?? "";
|
|
21
|
+
const existing = state.routes.find((route) => route.name === name);
|
|
22
|
+
const cached = slot.route?.name === name ? slot.route : undefined;
|
|
23
|
+
|
|
24
|
+
return existing ?? cached ?? buildSidebarRoute(slot, config, name);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const pinSidebar = (slot: SidebarSlot, state: StackState, config: RouterConfigOptions): StackState => {
|
|
28
|
+
const name = sidebarName(config);
|
|
29
|
+
|
|
30
|
+
if (name === undefined || state.routes[0]?.name === name) {
|
|
31
|
+
slot.route = state.routes[0];
|
|
32
|
+
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sidebar = resolveSidebarRoute(slot, state, config);
|
|
37
|
+
const routes = [sidebar, ...state.routes.filter((route) => route.name !== name)];
|
|
38
|
+
const focusedKey = state.routes[state.index]?.key;
|
|
39
|
+
const index = routes.findIndex((route) => route.key === focusedKey);
|
|
40
|
+
slot.route = sidebar;
|
|
41
|
+
|
|
42
|
+
return { ...state, routes, index: index === -1 ? routes.length - 1 : index };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const asSelection = (action: RouterAction): RouterAction => {
|
|
46
|
+
if (action.type !== "NAVIGATE" || action.payload.pop !== undefined) {
|
|
47
|
+
return action;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { ...action, payload: { ...action.payload, pop: true } };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const pinActionResult = (
|
|
54
|
+
slot: SidebarSlot,
|
|
55
|
+
result: ReturnType<StackRouterInstance["getStateForAction"]>,
|
|
56
|
+
config: RouterConfigOptions,
|
|
57
|
+
): ReturnType<StackRouterInstance["getStateForAction"]> =>
|
|
58
|
+
result?.stale === false ? pinSidebar(slot, result, config) : result;
|
|
59
|
+
|
|
60
|
+
const withPinnedSidebar = (router: StackRouterInstance, slot: SidebarSlot): StackRouterInstance => ({
|
|
61
|
+
...router,
|
|
62
|
+
getInitialState: (config) => pinSidebar(slot, router.getInitialState(config), config),
|
|
63
|
+
getRehydratedState: (state, config) => pinSidebar(slot, router.getRehydratedState(state, config), config),
|
|
64
|
+
getStateForRouteNamesChange: (state, config) =>
|
|
65
|
+
pinSidebar(slot, router.getStateForRouteNamesChange(state, config), config),
|
|
66
|
+
getStateForAction: (state, action, config) =>
|
|
67
|
+
pinActionResult(slot, router.getStateForAction(state, asSelection(action), config), config),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const splitViewRouter: StackRouterFactory = (options) =>
|
|
71
|
+
withPinnedSidebar(StackRouter(options), { route: undefined, serial: 0 });
|
|
72
|
+
|
|
73
|
+
export { splitViewRouter };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type * as Adw from "@gtkx/gi/adw";
|
|
2
|
+
import type { ParamListBase, RouteProp, StackNavigationState } from "@react-navigation/core";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import * as Gdk from "@gtkx/gi/gdk";
|
|
5
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
6
|
+
import { AdwNavigationPage, AdwNavigationSplitView, AdwToolbarView } from "@gtkx/jsx/adw";
|
|
7
|
+
import { GtkEventControllerKey } from "@gtkx/jsx/gtk";
|
|
8
|
+
import { StackActions } from "@react-navigation/core";
|
|
9
|
+
import { useCallback, useRef } from "react";
|
|
10
|
+
import type {
|
|
11
|
+
SplitViewDescriptor,
|
|
12
|
+
SplitViewDescriptorMap,
|
|
13
|
+
SplitViewNavigationConfig,
|
|
14
|
+
SplitViewNavigationHelpers,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { HeaderBar } from "../shared/header-bar.js";
|
|
17
|
+
import { requireDescriptor } from "../shared/routes.js";
|
|
18
|
+
import { StackView } from "../stack/stack-view.js";
|
|
19
|
+
|
|
20
|
+
type ViewRef = { current: Adw.NavigationSplitView | null };
|
|
21
|
+
|
|
22
|
+
type SplitViewProps = SplitViewNavigationConfig & {
|
|
23
|
+
state: StackNavigationState<ParamListBase>;
|
|
24
|
+
navigation: SplitViewNavigationHelpers;
|
|
25
|
+
descriptors: SplitViewDescriptorMap;
|
|
26
|
+
describe: (route: RouteProp<ParamListBase>, isPlaceholder: boolean) => SplitViewDescriptor;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PACK_TYPES = { start: Gtk.PackType.START, end: Gtk.PackType.END } as const;
|
|
30
|
+
const CONTENT_TITLE = "Content";
|
|
31
|
+
const ESCAPE_KEYVAL = Gdk.KEY_Escape;
|
|
32
|
+
|
|
33
|
+
const hasContent = (state: { routes: readonly unknown[] }): boolean => state.routes.length > 1;
|
|
34
|
+
|
|
35
|
+
const contentTitle = (state: StackNavigationState<ParamListBase>, descriptors: SplitViewDescriptorMap): string => {
|
|
36
|
+
const route = state.routes[state.index];
|
|
37
|
+
|
|
38
|
+
if (route === undefined || state.index === 0) {
|
|
39
|
+
return state.routeNames[1] ?? CONTENT_TITLE;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return descriptors[route.key]?.options.title ?? route.name;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const popContent = (navigation: SplitViewNavigationHelpers): void => {
|
|
46
|
+
const state = navigation.getState();
|
|
47
|
+
navigation.dispatch({ ...StackActions.pop(state.routes.length - 1), target: state.key });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const settleShowContent = (viewRef: ViewRef, isShown: boolean, isSettled: boolean): void => {
|
|
51
|
+
if (isSettled !== isShown && viewRef.current !== null) {
|
|
52
|
+
viewRef.current.setShowContent(isSettled);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const useContentSync = (
|
|
57
|
+
viewRef: ViewRef,
|
|
58
|
+
navigation: SplitViewNavigationHelpers,
|
|
59
|
+
): ((isShown: boolean | null) => void) =>
|
|
60
|
+
useCallback((isShown: boolean | null) => {
|
|
61
|
+
if (isShown === null || isShown === hasContent(navigation.getState())) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isShown) {
|
|
66
|
+
popContent(navigation);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
settleShowContent(viewRef, isShown, hasContent(navigation.getState()));
|
|
70
|
+
}, [viewRef, navigation]);
|
|
71
|
+
|
|
72
|
+
const EscapeGuard = (): ReactNode => (
|
|
73
|
+
<GtkEventControllerKey
|
|
74
|
+
propagationPhase={Gtk.PropagationPhase.BUBBLE}
|
|
75
|
+
onKeyPressed={(keyval) => keyval === ESCAPE_KEYVAL}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const SidebarPage = ({ descriptor }: { descriptor: SplitViewDescriptor }): ReactNode => {
|
|
80
|
+
const { route, options, navigation } = descriptor;
|
|
81
|
+
|
|
82
|
+
if (options.headerShown === false) {
|
|
83
|
+
return <AdwNavigationPage title={options.title ?? route.name}>{descriptor.render()}</AdwNavigationPage>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const topBar = options.header === undefined
|
|
87
|
+
? <HeaderBar options={options} showBackButton={false} />
|
|
88
|
+
: <>{options.header({ route, navigation, options, back: undefined })}</>;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<AdwNavigationPage title={options.title ?? route.name}>
|
|
92
|
+
<AdwToolbarView topBar={topBar}>{descriptor.render()}</AdwToolbarView>
|
|
93
|
+
</AdwNavigationPage>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const SplitView = (props: SplitViewProps): ReactNode => {
|
|
98
|
+
const { state, navigation, descriptors, describe, contentPlaceholder } = props;
|
|
99
|
+
const { collapsed = false, sidebarPosition = "start", minSidebarWidth, maxSidebarWidth } = props;
|
|
100
|
+
const viewRef = useRef<Adw.NavigationSplitView | null>(null);
|
|
101
|
+
const onShowContentChanged = useContentSync(viewRef, navigation);
|
|
102
|
+
const sidebar = requireDescriptor(descriptors, state.routes[0]?.key ?? "");
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<AdwNavigationSplitView
|
|
106
|
+
ref={viewRef}
|
|
107
|
+
collapsed={collapsed}
|
|
108
|
+
showContent={hasContent(state)}
|
|
109
|
+
sidebarPosition={PACK_TYPES[sidebarPosition]}
|
|
110
|
+
minSidebarWidth={minSidebarWidth}
|
|
111
|
+
maxSidebarWidth={maxSidebarWidth}
|
|
112
|
+
sidebarWidthFraction={props.sidebarWidthFraction}
|
|
113
|
+
onNotifyShowContent={onShowContentChanged}
|
|
114
|
+
sidebar={<SidebarPage descriptor={sidebar} />}
|
|
115
|
+
>
|
|
116
|
+
<AdwNavigationPage
|
|
117
|
+
title={contentTitle(state, descriptors)}
|
|
118
|
+
controllers={collapsed && props.popOnEscape === false ? <EscapeGuard /> : null}
|
|
119
|
+
>
|
|
120
|
+
{hasContent(state)
|
|
121
|
+
? (
|
|
122
|
+
<StackView
|
|
123
|
+
offset={1}
|
|
124
|
+
popOnEscape={props.popOnEscape}
|
|
125
|
+
state={state}
|
|
126
|
+
navigation={navigation}
|
|
127
|
+
descriptors={descriptors}
|
|
128
|
+
describe={describe}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
: <>{contentPlaceholder}</>}
|
|
132
|
+
</AdwNavigationPage>
|
|
133
|
+
</AdwNavigationSplitView>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export { SplitView };
|