@gtkx/navigation 2.0.0-beta.11 → 2.0.0-beta.13
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 +3 -3
- package/dist/drawer/create-drawer-navigator.d.ts +3 -1
- package/dist/drawer/create-drawer-navigator.d.ts.map +1 -1
- package/dist/drawer/create-drawer-navigator.js +10 -10
- package/dist/drawer/create-drawer-navigator.js.map +1 -1
- package/dist/drawer/drawer-item-list.d.ts.map +1 -1
- package/dist/drawer/drawer-item-list.js +3 -2
- package/dist/drawer/drawer-item-list.js.map +1 -1
- package/dist/drawer/drawer-router.d.ts.map +1 -1
- package/dist/drawer/drawer-router.js.map +1 -1
- package/dist/drawer/drawer-status.d.ts.map +1 -1
- package/dist/drawer/drawer-view.d.ts.map +1 -1
- package/dist/drawer/drawer-view.js +6 -6
- package/dist/drawer/drawer-view.js.map +1 -1
- package/dist/drawer/types.d.ts +2 -0
- package/dist/drawer/types.d.ts.map +1 -1
- package/dist/drawer/types.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/shared/header-bar.d.ts.map +1 -1
- package/dist/shared/header-bar.js +2 -2
- package/dist/shared/header-bar.js.map +1 -1
- package/dist/shared/routes.d.ts.map +1 -1
- package/dist/shared/scene-page.d.ts +3 -1
- package/dist/shared/scene-page.d.ts.map +1 -1
- package/dist/shared/scene-page.js +4 -7
- package/dist/shared/scene-page.js.map +1 -1
- package/dist/shared/use-has-loaded.d.ts +3 -0
- package/dist/shared/use-has-loaded.d.ts.map +1 -0
- package/dist/shared/use-has-loaded.js +10 -0
- package/dist/shared/use-has-loaded.js.map +1 -0
- package/dist/shared/use-pop-to-top-on-blur.d.ts.map +1 -1
- package/dist/split-view/create-split-view-navigator.d.ts +7 -2
- package/dist/split-view/create-split-view-navigator.d.ts.map +1 -1
- package/dist/split-view/create-split-view-navigator.js +2 -6
- package/dist/split-view/create-split-view-navigator.js.map +1 -1
- package/dist/split-view/split-view-collapsed.d.ts +11 -0
- package/dist/split-view/split-view-collapsed.d.ts.map +1 -0
- package/dist/split-view/split-view-collapsed.js +17 -0
- package/dist/split-view/split-view-collapsed.js.map +1 -0
- package/dist/split-view/split-view.d.ts.map +1 -1
- package/dist/split-view/split-view.js +41 -20
- package/dist/split-view/split-view.js.map +1 -1
- package/dist/split-view/types.d.ts +9 -2
- package/dist/split-view/types.d.ts.map +1 -1
- package/dist/split-view/types.js.map +1 -1
- package/dist/stack/create-stack-navigator.d.ts +3 -1
- package/dist/stack/create-stack-navigator.d.ts.map +1 -1
- package/dist/stack/create-stack-navigator.js +0 -1
- package/dist/stack/create-stack-navigator.js.map +1 -1
- package/dist/stack/stack-pages.d.ts.map +1 -1
- package/dist/stack/stack-pages.js +3 -5
- package/dist/stack/stack-pages.js.map +1 -1
- package/dist/stack/stack-view.d.ts.map +1 -1
- package/dist/stack/stack-view.js +6 -14
- package/dist/stack/stack-view.js.map +1 -1
- package/dist/stack/sync-navigation-stack.d.ts.map +1 -1
- package/dist/stack/types.d.ts.map +1 -1
- package/dist/stack/types.js.map +1 -1
- package/dist/tab-view/create-tab-view-navigator.d.ts +32 -0
- package/dist/tab-view/create-tab-view-navigator.d.ts.map +1 -0
- package/dist/tab-view/create-tab-view-navigator.js +21 -0
- package/dist/tab-view/create-tab-view-navigator.js.map +1 -0
- package/dist/tab-view/page-registry.d.ts +34 -0
- package/dist/tab-view/page-registry.d.ts.map +1 -0
- package/dist/tab-view/page-registry.js +70 -0
- package/dist/tab-view/page-registry.js.map +1 -0
- package/dist/tab-view/tab-view-host.d.ts +25 -0
- package/dist/tab-view/tab-view-host.d.ts.map +1 -0
- package/dist/tab-view/tab-view-host.js +284 -0
- package/dist/tab-view/tab-view-host.js.map +1 -0
- package/dist/tab-view/tab-view-router.d.ts +34 -0
- package/dist/tab-view/tab-view-router.d.ts.map +1 -0
- package/dist/tab-view/tab-view-router.js +359 -0
- package/dist/tab-view/tab-view-router.js.map +1 -0
- package/dist/tab-view/types.d.ts +240 -0
- package/dist/tab-view/types.d.ts.map +1 -0
- package/dist/tab-view/types.js +2 -0
- package/dist/tab-view/types.js.map +1 -0
- package/dist/tabs/create-tab-navigator.d.ts +3 -1
- package/dist/tabs/create-tab-navigator.d.ts.map +1 -1
- package/dist/tabs/create-tab-navigator.js +0 -1
- package/dist/tabs/create-tab-navigator.js.map +1 -1
- package/dist/tabs/tab-view.d.ts.map +1 -1
- package/dist/tabs/tab-view.js +5 -7
- package/dist/tabs/tab-view.js.map +1 -1
- package/dist/tabs/types.d.ts +2 -0
- package/dist/tabs/types.d.ts.map +1 -1
- package/dist/tabs/types.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +2 -2
- package/dist/theme.js.map +1 -1
- package/package.json +9 -19
- package/src/drawer/create-drawer-navigator.tsx +23 -11
- package/src/drawer/drawer-item-list.tsx +16 -12
- package/src/drawer/drawer-router.ts +4 -2
- package/src/drawer/drawer-view.tsx +18 -18
- package/src/drawer/types.ts +6 -3
- package/src/index.ts +31 -5
- package/src/shared/header-bar.tsx +2 -2
- package/src/shared/scene-page.tsx +7 -8
- package/src/shared/use-has-loaded.ts +13 -0
- package/src/split-view/create-split-view-navigator.tsx +16 -1
- package/src/split-view/split-view-collapsed.ts +21 -0
- package/src/split-view/split-view.tsx +102 -49
- package/src/split-view/types.ts +13 -5
- package/src/stack/create-stack-navigator.tsx +11 -1
- package/src/stack/stack-pages.ts +14 -12
- package/src/stack/stack-view.tsx +10 -16
- package/src/stack/types.ts +4 -3
- package/src/tab-view/create-tab-view-navigator.tsx +122 -0
- package/src/tab-view/page-registry.ts +139 -0
- package/src/tab-view/tab-view-host.tsx +474 -0
- package/src/tab-view/tab-view-router.ts +528 -0
- package/src/tab-view/types.ts +291 -0
- package/src/tabs/create-tab-navigator.tsx +11 -1
- package/src/tabs/tab-view.tsx +28 -22
- package/src/tabs/types.ts +6 -3
- package/src/theme.ts +2 -3
|
@@ -47,17 +47,19 @@ type DrawerTypeBag<
|
|
|
47
47
|
/** Declares a typed screen of a drawer navigator for the static configuration API. */
|
|
48
48
|
const createDrawerScreen: StaticScreenFactory<DrawerTypeBag> = createScreenFactory<DrawerTypeBag>();
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const createFlag = (isInitial: boolean) => {
|
|
51
|
+
let isCurrent = isInitial;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
read: () => isCurrent,
|
|
55
|
+
update: (isNext: boolean) => {
|
|
56
|
+
isCurrent = isNext;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
update: (isNext: boolean) => {
|
|
57
|
-
isCurrent = isNext;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
+
const useDrawerRouter = (isCollapsed: boolean): DrawerRouterFactory => {
|
|
62
|
+
const [flag] = useState(() => createFlag(isCollapsed));
|
|
61
63
|
|
|
62
64
|
const [createRouter] = useState(() => createDrawerRouter(flag.read));
|
|
63
65
|
|
|
@@ -111,7 +113,17 @@ function createDrawerNavigator<
|
|
|
111
113
|
const ParamList extends ParamListBase,
|
|
112
114
|
const NavigatorID extends string | undefined = string | undefined,
|
|
113
115
|
const TypeBag extends NavigatorTypeBagBase = DrawerTypeBag<ParamList, NavigatorID>,
|
|
114
|
-
|
|
116
|
+
>(): TypedNavigator<TypeBag, undefined>;
|
|
117
|
+
/** Creates a drawer navigator rendered with `AdwOverlaySplitView`: a sidebar listing the screens beside the content. */
|
|
118
|
+
function createDrawerNavigator<
|
|
119
|
+
const ParamList extends ParamListBase,
|
|
120
|
+
const NavigatorID extends string | undefined = string | undefined,
|
|
121
|
+
const TypeBag extends NavigatorTypeBagBase = DrawerTypeBag<ParamList, NavigatorID>,
|
|
122
|
+
const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
|
|
123
|
+
>(config: Config): TypedNavigator<TypeBag, Config>;
|
|
124
|
+
function createDrawerNavigator<
|
|
125
|
+
const TypeBag extends NavigatorTypeBagBase,
|
|
126
|
+
const Config extends StaticConfig<TypeBag> | undefined,
|
|
115
127
|
>(config?: Config): TypedNavigator<TypeBag, Config> {
|
|
116
128
|
return createNavigatorFactory(DrawerNavigator)(config) as TypedNavigator<TypeBag, Config>;
|
|
117
129
|
}
|
|
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
4
|
import { GtkBox, GtkImage, GtkLabel, GtkListBox, GtkListBoxRow } from "@gtkx/jsx/gtk";
|
|
5
5
|
import { CommonActions, DrawerActions } from "@react-navigation/core";
|
|
6
|
-
import { useCallback, useContext } from "react";
|
|
6
|
+
import { useCallback, useContext, useState } from "react";
|
|
7
7
|
import type { DrawerContentProps, DrawerDescriptor, DrawerNavigationHelpers } from "./types.js";
|
|
8
8
|
import { requireDescriptor } from "../shared/routes.js";
|
|
9
9
|
import { DrawerCollapsedContext } from "./drawer-collapsed-context.js";
|
|
@@ -29,27 +29,31 @@ const didNavigateToRow = (navigation: DrawerNavigationHelpers, state: Navigation
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const useDrawerItemPress = (navigation: DrawerNavigationHelpers, isCollapsed: boolean): RowActivated =>
|
|
32
|
-
useCallback(
|
|
33
|
-
|
|
32
|
+
useCallback(
|
|
33
|
+
(row: Gtk.ListBoxRow) => {
|
|
34
|
+
const state = navigation.getState();
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if (!didNavigateToRow(navigation, state, row.getIndex())) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if (isCollapsed) {
|
|
41
|
+
navigation.dispatch({ ...DrawerActions.closeDrawer(), target: state.key });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[navigation, isCollapsed],
|
|
45
|
+
);
|
|
43
46
|
|
|
44
47
|
const DrawerItem = ({ descriptor }: { descriptor: DrawerDescriptor }): ReactNode => {
|
|
45
48
|
const { route, options } = descriptor;
|
|
46
49
|
const label = options.drawerLabel ?? options.title ?? route.name;
|
|
50
|
+
const [labelWidget, setLabelWidget] = useState<Gtk.Label | null>(null);
|
|
47
51
|
|
|
48
52
|
return (
|
|
49
|
-
<GtkListBoxRow
|
|
53
|
+
<GtkListBoxRow accessibleLabelledBy={labelWidget === null ? undefined : [labelWidget]}>
|
|
50
54
|
<GtkBox spacing={12}>
|
|
51
55
|
{options.drawerIcon === undefined ? null : <GtkImage iconName={options.drawerIcon} />}
|
|
52
|
-
<GtkLabel label={label} xalign={0} />
|
|
56
|
+
<GtkLabel ref={setLabelWidget} label={label} useUnderline={options.useUnderline ?? false} xalign={0} />
|
|
53
57
|
</GtkBox>
|
|
54
58
|
</GtkListBoxRow>
|
|
55
59
|
);
|
|
@@ -84,7 +84,9 @@ const withShownSidebar = (router: DrawerRouterInstance, isCollapsed: () => boole
|
|
|
84
84
|
keepSidebarShown(state, router.getStateForRouteFocus(state, key), isCollapsed()),
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
const createDrawerRouter =
|
|
88
|
-
|
|
87
|
+
const createDrawerRouter =
|
|
88
|
+
(isCollapsed: () => boolean): DrawerRouterFactory =>
|
|
89
|
+
(options) =>
|
|
90
|
+
withShownSidebar(DrawerRouter(options), isCollapsed);
|
|
89
91
|
|
|
90
92
|
export { createDrawerRouter, type DrawerRouterFactory };
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import type { DrawerNavigationState, ParamListBase } from "@react-navigation/core";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
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";
|
|
4
|
+
import { AdwHeaderBar, AdwOverlaySplitView, AdwToolbarView, AdwViewStack, AdwWindowTitle } from "@gtkx/jsx/adw";
|
|
11
5
|
import { GtkButton, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
12
6
|
import { DrawerActions } from "@react-navigation/core";
|
|
13
7
|
import { useCallback, useEffect, useRef } from "react";
|
|
@@ -39,20 +33,24 @@ type DrawerHeaderProps = {
|
|
|
39
33
|
|
|
40
34
|
type DrawerPageProps = {
|
|
41
35
|
descriptor: DrawerDescriptor;
|
|
36
|
+
isFocused: boolean;
|
|
42
37
|
isLoaded: boolean;
|
|
43
38
|
};
|
|
44
39
|
|
|
45
40
|
const PACK_TYPES = { start: Gtk.PackType.START, end: Gtk.PackType.END } as const;
|
|
46
41
|
|
|
47
42
|
const useSidebarSync = (navigation: DrawerNavigationHelpers): ((isShown: boolean | null) => void) =>
|
|
48
|
-
useCallback(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
useCallback(
|
|
44
|
+
(isShown: boolean | null) => {
|
|
45
|
+
if (isShown === null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const action = isShown ? DrawerActions.openDrawer() : DrawerActions.closeDrawer();
|
|
50
|
+
navigation.dispatch({ ...action, target: navigation.getState().key });
|
|
51
|
+
},
|
|
52
|
+
[navigation],
|
|
53
|
+
);
|
|
56
54
|
|
|
57
55
|
const useCloseOnNavigate = (
|
|
58
56
|
state: DrawerNavigationState<ParamListBase>,
|
|
@@ -73,7 +71,7 @@ const useCloseOnNavigate = (
|
|
|
73
71
|
};
|
|
74
72
|
|
|
75
73
|
const DrawerContent = (props: DrawerContentProps): ReactNode => (
|
|
76
|
-
<AdwToolbarView
|
|
74
|
+
<AdwToolbarView top={<AdwHeaderBar />}>
|
|
77
75
|
<GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER}>
|
|
78
76
|
<DrawerItemList {...props} />
|
|
79
77
|
</GtkScrolledWindow>
|
|
@@ -111,13 +109,14 @@ const DrawerHeader = ({ descriptor, navigation }: DrawerHeaderProps): ReactNode
|
|
|
111
109
|
);
|
|
112
110
|
};
|
|
113
111
|
|
|
114
|
-
const DrawerPage = ({ descriptor, isLoaded }: DrawerPageProps): ReactNode => {
|
|
112
|
+
const DrawerPage = ({ descriptor, isFocused, isLoaded }: DrawerPageProps): ReactNode => {
|
|
115
113
|
const { route, options } = descriptor;
|
|
116
114
|
|
|
117
115
|
return (
|
|
118
116
|
<ScenePage
|
|
119
117
|
name={route.key}
|
|
120
118
|
title={options.title ?? route.name}
|
|
119
|
+
isFocused={isFocused}
|
|
121
120
|
isLoaded={isLoaded || options.lazy === false}
|
|
122
121
|
render={() => descriptor.render()}
|
|
123
122
|
/>
|
|
@@ -147,12 +146,13 @@ const DrawerView = ({ state, navigation, descriptors, drawerContent, ...config }
|
|
|
147
146
|
onNotifyShowSidebar={onShowSidebarChanged}
|
|
148
147
|
sidebar={<>{sidebar}</>}
|
|
149
148
|
>
|
|
150
|
-
<AdwToolbarView
|
|
149
|
+
<AdwToolbarView top={<DrawerHeader descriptor={descriptor} navigation={navigation} />}>
|
|
151
150
|
<AdwViewStack visibleChildName={focused.key}>
|
|
152
151
|
{state.routes.map((route) => (
|
|
153
152
|
<DrawerPage
|
|
154
153
|
key={route.key}
|
|
155
154
|
descriptor={requireDescriptor(descriptors, route.key)}
|
|
155
|
+
isFocused={route.key === focused.key}
|
|
156
156
|
isLoaded={route.key === focused.key || state.preloadedRouteKeys.includes(route.key)}
|
|
157
157
|
/>
|
|
158
158
|
))}
|
package/src/drawer/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ type DrawerNavigationOptions = HeaderOptions & {
|
|
|
29
29
|
header?: (props: DrawerHeaderProps) => ReactNode;
|
|
30
30
|
/** Label shown in the sidebar; defaults to `title`, then to the route name. */
|
|
31
31
|
drawerLabel?: string;
|
|
32
|
+
/** Whether underscores in the sidebar label indicate mnemonics; defaults to `false`. */
|
|
33
|
+
useUnderline?: boolean;
|
|
32
34
|
/** Icon name shown next to the sidebar label. */
|
|
33
35
|
drawerIcon?: string;
|
|
34
36
|
/** Whether the screen mounts on first focus instead of at startup; defaults to `true`. */
|
|
@@ -49,7 +51,8 @@ type DrawerNavigationProp<
|
|
|
49
51
|
DrawerNavigationState<ParamList>,
|
|
50
52
|
DrawerNavigationOptions,
|
|
51
53
|
DrawerNavigationEventMap
|
|
52
|
-
> &
|
|
54
|
+
> &
|
|
55
|
+
DrawerActionHelpers<ParamList>;
|
|
53
56
|
|
|
54
57
|
/** Props a drawer screen component receives. */
|
|
55
58
|
type DrawerScreenProps<
|
|
@@ -124,8 +127,8 @@ type DrawerNavigatorProps = DefaultNavigatorOptions<
|
|
|
124
127
|
DrawerNavigationEventMap,
|
|
125
128
|
DrawerNavigationProp<ParamListBase>
|
|
126
129
|
> &
|
|
127
|
-
DrawerRouterOptions &
|
|
128
|
-
DrawerNavigationConfig;
|
|
130
|
+
DrawerRouterOptions &
|
|
131
|
+
DrawerNavigationConfig;
|
|
129
132
|
|
|
130
133
|
export type {
|
|
131
134
|
DrawerContentProps,
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
SplitViewNavigator,
|
|
31
31
|
type SplitViewTypeBag,
|
|
32
32
|
} from "./split-view/create-split-view-navigator.js";
|
|
33
|
+
export { useSplitViewCollapsed } from "./split-view/split-view-collapsed.js";
|
|
33
34
|
export type {
|
|
34
35
|
SplitViewDescriptor,
|
|
35
36
|
SplitViewDescriptorMap,
|
|
@@ -61,11 +62,7 @@ export type {
|
|
|
61
62
|
StackScreenProps,
|
|
62
63
|
StackTransitionData,
|
|
63
64
|
} from "./stack/types.js";
|
|
64
|
-
export {
|
|
65
|
-
createStaticNavigation,
|
|
66
|
-
type StaticNavigationProps,
|
|
67
|
-
type StaticNavigationTree,
|
|
68
|
-
} from "./static-navigation.js";
|
|
65
|
+
export { createStaticNavigation, type StaticNavigationProps, type StaticNavigationTree } from "./static-navigation.js";
|
|
69
66
|
export { createTabNavigator, createTabScreen, TabNavigator, type TabTypeBag } from "./tabs/create-tab-navigator.js";
|
|
70
67
|
export type {
|
|
71
68
|
TabDescriptor,
|
|
@@ -79,5 +76,34 @@ export type {
|
|
|
79
76
|
TabNavigatorProps,
|
|
80
77
|
TabScreenProps,
|
|
81
78
|
} from "./tabs/types.js";
|
|
79
|
+
export {
|
|
80
|
+
createTabViewNavigator,
|
|
81
|
+
createTabViewScreen,
|
|
82
|
+
TabViewNavigator,
|
|
83
|
+
type TabViewTypeBag,
|
|
84
|
+
} from "./tab-view/create-tab-view-navigator.js";
|
|
85
|
+
export { type TabViewHandle, useTabView } from "./tab-view/tab-view-host.js";
|
|
86
|
+
export { TabViewActions } from "./tab-view/tab-view-router.js";
|
|
87
|
+
export type {
|
|
88
|
+
TabViewActionHelpers,
|
|
89
|
+
TabViewActionType,
|
|
90
|
+
TabViewDescriptor,
|
|
91
|
+
TabViewDescriptorMap,
|
|
92
|
+
TabViewExtraDropTarget,
|
|
93
|
+
TabViewHeaderProps,
|
|
94
|
+
TabViewIcon,
|
|
95
|
+
TabViewNavigationConfig,
|
|
96
|
+
TabViewNavigationEventMap,
|
|
97
|
+
TabViewNavigationHelpers,
|
|
98
|
+
TabViewNavigationOptions,
|
|
99
|
+
TabViewNavigationProp,
|
|
100
|
+
TabViewNavigationState,
|
|
101
|
+
TabViewNavigatorProps,
|
|
102
|
+
TabViewOpenOptions,
|
|
103
|
+
TabViewOverviewConfig,
|
|
104
|
+
TabViewRouterOptions,
|
|
105
|
+
TabViewScreenProps,
|
|
106
|
+
TabViewTarget,
|
|
107
|
+
} from "./tab-view/types.js";
|
|
82
108
|
export { DarkTheme, DefaultTheme, type Theme } from "./theme.js";
|
|
83
109
|
export * from "@react-navigation/core";
|
|
@@ -21,12 +21,12 @@ const HeaderBar = ({ options, titleWidget, start, showBackButton }: HeaderBarPro
|
|
|
21
21
|
<AdwHeaderBar
|
|
22
22
|
showBackButton={showBackButton ?? true}
|
|
23
23
|
titleWidget={resolveTitleWidget(options, titleWidget)}
|
|
24
|
-
start={
|
|
24
|
+
start={
|
|
25
25
|
<>
|
|
26
26
|
{start}
|
|
27
27
|
{options.headerStart}
|
|
28
28
|
</>
|
|
29
|
-
|
|
29
|
+
}
|
|
30
30
|
end={options.headerEnd}
|
|
31
31
|
/>
|
|
32
32
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
import { AdwBin, AdwViewStackPage } from "@gtkx/jsx/adw";
|
|
3
|
-
import {
|
|
3
|
+
import { useHasLoaded } from "./use-has-loaded.js";
|
|
4
4
|
|
|
5
5
|
type ScenePageProps = {
|
|
6
6
|
name: string;
|
|
@@ -8,16 +8,14 @@ type ScenePageProps = {
|
|
|
8
8
|
iconName?: string;
|
|
9
9
|
badgeNumber?: number;
|
|
10
10
|
needsAttention?: boolean;
|
|
11
|
+
useUnderline?: boolean;
|
|
12
|
+
isFocused: boolean;
|
|
11
13
|
isLoaded: boolean;
|
|
12
14
|
render: () => ReactElement;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
const ScenePage = ({ name, title, isLoaded, render, ...page }: ScenePageProps): ReactNode => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (isLoaded && !hasLoaded) {
|
|
19
|
-
setHasLoaded(true);
|
|
20
|
-
}
|
|
17
|
+
const ScenePage = ({ name, title, isFocused, isLoaded, render, ...page }: ScenePageProps): ReactNode => {
|
|
18
|
+
const hasLoaded = useHasLoaded(isLoaded);
|
|
21
19
|
|
|
22
20
|
return (
|
|
23
21
|
<AdwViewStackPage
|
|
@@ -26,7 +24,8 @@ const ScenePage = ({ name, title, isLoaded, render, ...page }: ScenePageProps):
|
|
|
26
24
|
iconName={page.iconName}
|
|
27
25
|
badgeNumber={page.badgeNumber}
|
|
28
26
|
needsAttention={page.needsAttention}
|
|
29
|
-
|
|
27
|
+
accessibleHidden={!isFocused}
|
|
28
|
+
useUnderline={page.useUnderline ?? false}
|
|
30
29
|
>
|
|
31
30
|
<AdwBin>{hasLoaded ? render() : null}</AdwBin>
|
|
32
31
|
</AdwViewStackPage>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
const useHasLoaded = (isLoaded: boolean): boolean => {
|
|
4
|
+
const [hasLoaded, setHasLoaded] = useState(isLoaded);
|
|
5
|
+
|
|
6
|
+
if (isLoaded && !hasLoaded) {
|
|
7
|
+
setHasLoaded(true);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return isLoaded || hasLoaded;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { useHasLoaded };
|
|
@@ -48,6 +48,7 @@ const createSplitViewScreen: StaticScreenFactory<SplitViewTypeBag> = createScree
|
|
|
48
48
|
/** Renders the screens of a {@link createSplitViewNavigator} in an `AdwNavigationSplitView`. */
|
|
49
49
|
function SplitViewNavigator({
|
|
50
50
|
collapsed,
|
|
51
|
+
collapseCondition,
|
|
51
52
|
contentPlaceholder,
|
|
52
53
|
sidebarPosition,
|
|
53
54
|
minSidebarWidth,
|
|
@@ -71,6 +72,7 @@ function SplitViewNavigator({
|
|
|
71
72
|
descriptors={descriptors}
|
|
72
73
|
describe={describe}
|
|
73
74
|
collapsed={collapsed}
|
|
75
|
+
collapseCondition={collapseCondition}
|
|
74
76
|
contentPlaceholder={contentPlaceholder}
|
|
75
77
|
sidebarPosition={sidebarPosition}
|
|
76
78
|
minSidebarWidth={minSidebarWidth}
|
|
@@ -89,7 +91,20 @@ function createSplitViewNavigator<
|
|
|
89
91
|
const ParamList extends ParamListBase,
|
|
90
92
|
const NavigatorID extends string | undefined = string | undefined,
|
|
91
93
|
const TypeBag extends NavigatorTypeBagBase = SplitViewTypeBag<ParamList, NavigatorID>,
|
|
92
|
-
|
|
94
|
+
>(): TypedNavigator<TypeBag, undefined>;
|
|
95
|
+
/**
|
|
96
|
+
* Creates a master/detail navigator rendered with `AdwNavigationSplitView`: the first screen fills the sidebar
|
|
97
|
+
* pane, the rest are a stack in the content pane, and a narrow window collapses the two into one.
|
|
98
|
+
*/
|
|
99
|
+
function createSplitViewNavigator<
|
|
100
|
+
const ParamList extends ParamListBase,
|
|
101
|
+
const NavigatorID extends string | undefined = string | undefined,
|
|
102
|
+
const TypeBag extends NavigatorTypeBagBase = SplitViewTypeBag<ParamList, NavigatorID>,
|
|
103
|
+
const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
|
|
104
|
+
>(config: Config): TypedNavigator<TypeBag, Config>;
|
|
105
|
+
function createSplitViewNavigator<
|
|
106
|
+
const TypeBag extends NavigatorTypeBagBase,
|
|
107
|
+
const Config extends StaticConfig<TypeBag> | undefined,
|
|
93
108
|
>(config?: Config): TypedNavigator<TypeBag, Config> {
|
|
94
109
|
return createNavigatorFactory(SplitViewNavigator)(config) as TypedNavigator<TypeBag, Config>;
|
|
95
110
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Context, createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
const SplitViewCollapsedContext: Context<boolean | null> = createContext<boolean | null>(null);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns whether the surrounding split view navigator shows one pane at a time, for its sidebar and content
|
|
7
|
+
* screens. It follows the split view's `collapsed` property, so it holds for both the `collapsed` prop and
|
|
8
|
+
* `collapseCondition`, and changes after libadwaita has laid out the collapsed or widened panes. Use it to
|
|
9
|
+
* adapt a screen's presentation, such as switching an `AdwSidebar` to `Adw.SidebarMode.PAGE`.
|
|
10
|
+
*/
|
|
11
|
+
const useSplitViewCollapsed = (): boolean => {
|
|
12
|
+
const isCollapsed = useContext(SplitViewCollapsedContext);
|
|
13
|
+
|
|
14
|
+
if (isCollapsed === null) {
|
|
15
|
+
throw new Error("useSplitViewCollapsed must be called inside a split view navigator.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return isCollapsed;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { SplitViewCollapsedContext, useSplitViewCollapsed };
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type * as Adw from "@gtkx/gi/adw";
|
|
2
1
|
import type { ParamListBase, RouteProp, StackNavigationState } from "@react-navigation/core";
|
|
3
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import * as Adw from "@gtkx/gi/adw";
|
|
4
4
|
import * as Gdk from "@gtkx/gi/gdk";
|
|
5
|
+
import * as GObject from "@gtkx/gi/gobject";
|
|
5
6
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AdwBreakpoint,
|
|
9
|
+
AdwBreakpointBin,
|
|
10
|
+
AdwHeaderBar,
|
|
11
|
+
AdwNavigationPage,
|
|
12
|
+
AdwNavigationSplitView,
|
|
13
|
+
AdwToolbarView,
|
|
14
|
+
} from "@gtkx/jsx/adw";
|
|
7
15
|
import { GtkEventControllerKey } from "@gtkx/jsx/gtk";
|
|
16
|
+
import { useProperty } from "@gtkx/react";
|
|
8
17
|
import { StackActions } from "@react-navigation/core";
|
|
9
|
-
import { useCallback,
|
|
18
|
+
import { useCallback, useLayoutEffect, useMemo, useState } from "react";
|
|
10
19
|
import type {
|
|
11
20
|
SplitViewDescriptor,
|
|
12
21
|
SplitViewDescriptorMap,
|
|
@@ -16,8 +25,7 @@ import type {
|
|
|
16
25
|
import { HeaderBar } from "../shared/header-bar.js";
|
|
17
26
|
import { requireDescriptor } from "../shared/routes.js";
|
|
18
27
|
import { StackView } from "../stack/stack-view.js";
|
|
19
|
-
|
|
20
|
-
type ViewRef = { current: Adw.NavigationSplitView | null };
|
|
28
|
+
import { SplitViewCollapsedContext } from "./split-view-collapsed.js";
|
|
21
29
|
|
|
22
30
|
type SplitViewProps = SplitViewNavigationConfig & {
|
|
23
31
|
state: StackNavigationState<ParamListBase>;
|
|
@@ -47,27 +55,59 @@ const popContent = (navigation: SplitViewNavigationHelpers): void => {
|
|
|
47
55
|
navigation.dispatch({ ...StackActions.pop(state.routes.length - 1), target: state.key });
|
|
48
56
|
};
|
|
49
57
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const collapsedValue = (): GObject.Value => {
|
|
59
|
+
const value = new GObject.Value();
|
|
60
|
+
value.init(GObject.TYPE_BOOLEAN);
|
|
61
|
+
value.setBoolean(true);
|
|
62
|
+
|
|
63
|
+
return value;
|
|
54
64
|
};
|
|
55
65
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
):
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const useCollapseBreakpoint = (
|
|
67
|
+
view: Adw.NavigationSplitView | null,
|
|
68
|
+
collapseCondition: string | undefined,
|
|
69
|
+
): ReactNode => {
|
|
70
|
+
const [breakpoint, setBreakpoint] = useState<Adw.Breakpoint | null>(null);
|
|
71
|
+
const condition = useMemo(
|
|
72
|
+
() => (collapseCondition === undefined ? null : Adw.BreakpointCondition.parse(collapseCondition)),
|
|
73
|
+
[collapseCondition],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
if (breakpoint === null || view === null) {
|
|
62
78
|
return;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
81
|
+
breakpoint.addSetter(view, "collapsed", collapsedValue());
|
|
82
|
+
}, [view, breakpoint]);
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
return condition === null ? null : <AdwBreakpoint ref={setBreakpoint} condition={condition} />;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const settleShowContent = (view: Adw.NavigationSplitView | null, isShown: boolean, isSettled: boolean): void => {
|
|
88
|
+
if (isSettled !== isShown && view !== null) {
|
|
89
|
+
view.setShowContent(isSettled);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const useContentSync = (
|
|
94
|
+
view: Adw.NavigationSplitView | null,
|
|
95
|
+
navigation: SplitViewNavigationHelpers,
|
|
96
|
+
): ((isShown: boolean | null) => void) =>
|
|
97
|
+
useCallback(
|
|
98
|
+
(isShown: boolean | null) => {
|
|
99
|
+
if (isShown === null || isShown === hasContent(navigation.getState())) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isShown) {
|
|
104
|
+
popContent(navigation);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
settleShowContent(view, isShown, hasContent(navigation.getState()));
|
|
108
|
+
},
|
|
109
|
+
[view, navigation],
|
|
110
|
+
);
|
|
71
111
|
|
|
72
112
|
const EscapeGuard = (): ReactNode => (
|
|
73
113
|
<GtkEventControllerKey
|
|
@@ -83,42 +123,50 @@ const SidebarPage = ({ descriptor }: { descriptor: SplitViewDescriptor }): React
|
|
|
83
123
|
return <AdwNavigationPage title={options.title ?? route.name}>{descriptor.render()}</AdwNavigationPage>;
|
|
84
124
|
}
|
|
85
125
|
|
|
86
|
-
const topBar =
|
|
87
|
-
|
|
88
|
-
|
|
126
|
+
const topBar =
|
|
127
|
+
options.header === undefined ? (
|
|
128
|
+
<HeaderBar options={options} showBackButton={false} />
|
|
129
|
+
) : (
|
|
130
|
+
<>{options.header({ route, navigation, options, back: undefined })}</>
|
|
131
|
+
);
|
|
89
132
|
|
|
90
133
|
return (
|
|
91
134
|
<AdwNavigationPage title={options.title ?? route.name}>
|
|
92
|
-
<AdwToolbarView
|
|
135
|
+
<AdwToolbarView top={topBar}>{descriptor.render()}</AdwToolbarView>
|
|
93
136
|
</AdwNavigationPage>
|
|
94
137
|
);
|
|
95
138
|
};
|
|
96
139
|
|
|
97
140
|
const SplitView = (props: SplitViewProps): ReactNode => {
|
|
98
|
-
const { state, navigation, descriptors, describe, contentPlaceholder } = props;
|
|
99
|
-
const {
|
|
100
|
-
const
|
|
101
|
-
const onShowContentChanged = useContentSync(
|
|
141
|
+
const { state, navigation, descriptors, describe, contentPlaceholder, collapseCondition } = props;
|
|
142
|
+
const { sidebarPosition = "start", minSidebarWidth, maxSidebarWidth } = props;
|
|
143
|
+
const [view, setView] = useState<Adw.NavigationSplitView | null>(null);
|
|
144
|
+
const onShowContentChanged = useContentSync(view, navigation);
|
|
145
|
+
const collapseBreakpoint = useCollapseBreakpoint(view, collapseCondition);
|
|
102
146
|
const sidebar = requireDescriptor(descriptors, state.routes[0]?.key ?? "");
|
|
147
|
+
const collapsed = collapseCondition === undefined ? (props.collapsed ?? false) : undefined;
|
|
148
|
+
const isCollapsed = useProperty(view, "collapsed") ?? collapsed ?? false;
|
|
103
149
|
|
|
150
|
+
// AdwBreakpointBin warns without a size request; the window's size request sets the smallest supported size.
|
|
104
151
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
<SplitViewCollapsedContext value={isCollapsed}>
|
|
153
|
+
<AdwBreakpointBin widthRequest={1} heightRequest={1} breakpoints={collapseBreakpoint}>
|
|
154
|
+
<AdwNavigationSplitView
|
|
155
|
+
ref={setView}
|
|
156
|
+
collapsed={collapsed}
|
|
157
|
+
showContent={hasContent(state)}
|
|
158
|
+
sidebarPosition={PACK_TYPES[sidebarPosition]}
|
|
159
|
+
minSidebarWidth={minSidebarWidth}
|
|
160
|
+
maxSidebarWidth={maxSidebarWidth}
|
|
161
|
+
sidebarWidthFraction={props.sidebarWidthFraction}
|
|
162
|
+
onNotifyShowContent={onShowContentChanged}
|
|
163
|
+
sidebar={<SidebarPage descriptor={sidebar} />}
|
|
164
|
+
>
|
|
165
|
+
<AdwNavigationPage
|
|
166
|
+
title={contentTitle(state, descriptors)}
|
|
167
|
+
controllers={isCollapsed && props.popOnEscape === false ? <EscapeGuard /> : null}
|
|
168
|
+
>
|
|
169
|
+
{hasContent(state) ? (
|
|
122
170
|
<StackView
|
|
123
171
|
offset={1}
|
|
124
172
|
popOnEscape={props.popOnEscape}
|
|
@@ -127,10 +175,15 @@ const SplitView = (props: SplitViewProps): ReactNode => {
|
|
|
127
175
|
descriptors={descriptors}
|
|
128
176
|
describe={describe}
|
|
129
177
|
/>
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
) : (
|
|
179
|
+
<AdwToolbarView top={<AdwHeaderBar showTitle={false} />}>
|
|
180
|
+
{contentPlaceholder}
|
|
181
|
+
</AdwToolbarView>
|
|
182
|
+
)}
|
|
183
|
+
</AdwNavigationPage>
|
|
184
|
+
</AdwNavigationSplitView>
|
|
185
|
+
</AdwBreakpointBin>
|
|
186
|
+
</SplitViewCollapsedContext>
|
|
134
187
|
);
|
|
135
188
|
};
|
|
136
189
|
|
package/src/split-view/types.ts
CHANGED
|
@@ -29,7 +29,8 @@ type SplitViewNavigationProp<
|
|
|
29
29
|
StackNavigationState<ParamList>,
|
|
30
30
|
SplitViewNavigationOptions,
|
|
31
31
|
SplitViewNavigationEventMap
|
|
32
|
-
> &
|
|
32
|
+
> &
|
|
33
|
+
StackActionHelpers<ParamList>;
|
|
33
34
|
|
|
34
35
|
/** Props a split view screen component receives. */
|
|
35
36
|
type SplitViewScreenProps<
|
|
@@ -59,9 +60,16 @@ type SplitViewDescriptorMap = Record<string, SplitViewDescriptor>;
|
|
|
59
60
|
|
|
60
61
|
/** Props of the split view navigator beyond the options every navigator shares. */
|
|
61
62
|
type SplitViewNavigationConfig = {
|
|
62
|
-
/** Whether the panes stack into one instead of sitting side by side; defaults to `false`. */
|
|
63
|
+
/** Whether the panes stack into one instead of sitting side by side; defaults to `false`. Ignored while `collapseCondition` is set. */
|
|
63
64
|
collapsed?: boolean;
|
|
64
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* An `Adw.BreakpointCondition` string, such as `"max-width: 500sp"`, measured against the navigator's size.
|
|
67
|
+
* The panes collapse while it holds. libadwaita applies it during layout, before GTK moves focus, so
|
|
68
|
+
* widening the window does not reselect the focused sidebar row the way a `collapsed` prop set from a
|
|
69
|
+
* breakpoint handler can. Give the window a `widthRequest` and `heightRequest`, as for window breakpoints.
|
|
70
|
+
*/
|
|
71
|
+
collapseCondition?: string;
|
|
72
|
+
/** Fills the content pane, below an untitled header bar, while no content route is open; defaults to nothing. */
|
|
65
73
|
contentPlaceholder?: ReactNode;
|
|
66
74
|
/** Which side the sidebar sits on; defaults to `"start"`. */
|
|
67
75
|
sidebarPosition?: "start" | "end";
|
|
@@ -84,8 +92,8 @@ type SplitViewNavigatorProps = DefaultNavigatorOptions<
|
|
|
84
92
|
SplitViewNavigationEventMap,
|
|
85
93
|
SplitViewNavigationProp<ParamListBase>
|
|
86
94
|
> &
|
|
87
|
-
StackRouterOptions &
|
|
88
|
-
SplitViewNavigationConfig;
|
|
95
|
+
StackRouterOptions &
|
|
96
|
+
SplitViewNavigationConfig;
|
|
89
97
|
|
|
90
98
|
export type {
|
|
91
99
|
SplitViewDescriptor,
|