@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.
Files changed (121) hide show
  1. package/README.md +3 -3
  2. package/dist/drawer/create-drawer-navigator.d.ts +3 -1
  3. package/dist/drawer/create-drawer-navigator.d.ts.map +1 -1
  4. package/dist/drawer/create-drawer-navigator.js +10 -10
  5. package/dist/drawer/create-drawer-navigator.js.map +1 -1
  6. package/dist/drawer/drawer-item-list.d.ts.map +1 -1
  7. package/dist/drawer/drawer-item-list.js +3 -2
  8. package/dist/drawer/drawer-item-list.js.map +1 -1
  9. package/dist/drawer/drawer-router.d.ts.map +1 -1
  10. package/dist/drawer/drawer-router.js.map +1 -1
  11. package/dist/drawer/drawer-status.d.ts.map +1 -1
  12. package/dist/drawer/drawer-view.d.ts.map +1 -1
  13. package/dist/drawer/drawer-view.js +6 -6
  14. package/dist/drawer/drawer-view.js.map +1 -1
  15. package/dist/drawer/types.d.ts +2 -0
  16. package/dist/drawer/types.d.ts.map +1 -1
  17. package/dist/drawer/types.js.map +1 -1
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/shared/header-bar.d.ts.map +1 -1
  23. package/dist/shared/header-bar.js +2 -2
  24. package/dist/shared/header-bar.js.map +1 -1
  25. package/dist/shared/routes.d.ts.map +1 -1
  26. package/dist/shared/scene-page.d.ts +3 -1
  27. package/dist/shared/scene-page.d.ts.map +1 -1
  28. package/dist/shared/scene-page.js +4 -7
  29. package/dist/shared/scene-page.js.map +1 -1
  30. package/dist/shared/use-has-loaded.d.ts +3 -0
  31. package/dist/shared/use-has-loaded.d.ts.map +1 -0
  32. package/dist/shared/use-has-loaded.js +10 -0
  33. package/dist/shared/use-has-loaded.js.map +1 -0
  34. package/dist/shared/use-pop-to-top-on-blur.d.ts.map +1 -1
  35. package/dist/split-view/create-split-view-navigator.d.ts +7 -2
  36. package/dist/split-view/create-split-view-navigator.d.ts.map +1 -1
  37. package/dist/split-view/create-split-view-navigator.js +2 -6
  38. package/dist/split-view/create-split-view-navigator.js.map +1 -1
  39. package/dist/split-view/split-view-collapsed.d.ts +11 -0
  40. package/dist/split-view/split-view-collapsed.d.ts.map +1 -0
  41. package/dist/split-view/split-view-collapsed.js +17 -0
  42. package/dist/split-view/split-view-collapsed.js.map +1 -0
  43. package/dist/split-view/split-view.d.ts.map +1 -1
  44. package/dist/split-view/split-view.js +41 -20
  45. package/dist/split-view/split-view.js.map +1 -1
  46. package/dist/split-view/types.d.ts +9 -2
  47. package/dist/split-view/types.d.ts.map +1 -1
  48. package/dist/split-view/types.js.map +1 -1
  49. package/dist/stack/create-stack-navigator.d.ts +3 -1
  50. package/dist/stack/create-stack-navigator.d.ts.map +1 -1
  51. package/dist/stack/create-stack-navigator.js +0 -1
  52. package/dist/stack/create-stack-navigator.js.map +1 -1
  53. package/dist/stack/stack-pages.d.ts.map +1 -1
  54. package/dist/stack/stack-pages.js +3 -5
  55. package/dist/stack/stack-pages.js.map +1 -1
  56. package/dist/stack/stack-view.d.ts.map +1 -1
  57. package/dist/stack/stack-view.js +6 -14
  58. package/dist/stack/stack-view.js.map +1 -1
  59. package/dist/stack/sync-navigation-stack.d.ts.map +1 -1
  60. package/dist/stack/types.d.ts.map +1 -1
  61. package/dist/stack/types.js.map +1 -1
  62. package/dist/tab-view/create-tab-view-navigator.d.ts +32 -0
  63. package/dist/tab-view/create-tab-view-navigator.d.ts.map +1 -0
  64. package/dist/tab-view/create-tab-view-navigator.js +21 -0
  65. package/dist/tab-view/create-tab-view-navigator.js.map +1 -0
  66. package/dist/tab-view/page-registry.d.ts +34 -0
  67. package/dist/tab-view/page-registry.d.ts.map +1 -0
  68. package/dist/tab-view/page-registry.js +70 -0
  69. package/dist/tab-view/page-registry.js.map +1 -0
  70. package/dist/tab-view/tab-view-host.d.ts +25 -0
  71. package/dist/tab-view/tab-view-host.d.ts.map +1 -0
  72. package/dist/tab-view/tab-view-host.js +284 -0
  73. package/dist/tab-view/tab-view-host.js.map +1 -0
  74. package/dist/tab-view/tab-view-router.d.ts +34 -0
  75. package/dist/tab-view/tab-view-router.d.ts.map +1 -0
  76. package/dist/tab-view/tab-view-router.js +359 -0
  77. package/dist/tab-view/tab-view-router.js.map +1 -0
  78. package/dist/tab-view/types.d.ts +240 -0
  79. package/dist/tab-view/types.d.ts.map +1 -0
  80. package/dist/tab-view/types.js +2 -0
  81. package/dist/tab-view/types.js.map +1 -0
  82. package/dist/tabs/create-tab-navigator.d.ts +3 -1
  83. package/dist/tabs/create-tab-navigator.d.ts.map +1 -1
  84. package/dist/tabs/create-tab-navigator.js +0 -1
  85. package/dist/tabs/create-tab-navigator.js.map +1 -1
  86. package/dist/tabs/tab-view.d.ts.map +1 -1
  87. package/dist/tabs/tab-view.js +5 -7
  88. package/dist/tabs/tab-view.js.map +1 -1
  89. package/dist/tabs/types.d.ts +2 -0
  90. package/dist/tabs/types.d.ts.map +1 -1
  91. package/dist/tabs/types.js.map +1 -1
  92. package/dist/theme.d.ts.map +1 -1
  93. package/dist/theme.js +2 -2
  94. package/dist/theme.js.map +1 -1
  95. package/package.json +9 -19
  96. package/src/drawer/create-drawer-navigator.tsx +23 -11
  97. package/src/drawer/drawer-item-list.tsx +16 -12
  98. package/src/drawer/drawer-router.ts +4 -2
  99. package/src/drawer/drawer-view.tsx +18 -18
  100. package/src/drawer/types.ts +6 -3
  101. package/src/index.ts +31 -5
  102. package/src/shared/header-bar.tsx +2 -2
  103. package/src/shared/scene-page.tsx +7 -8
  104. package/src/shared/use-has-loaded.ts +13 -0
  105. package/src/split-view/create-split-view-navigator.tsx +16 -1
  106. package/src/split-view/split-view-collapsed.ts +21 -0
  107. package/src/split-view/split-view.tsx +102 -49
  108. package/src/split-view/types.ts +13 -5
  109. package/src/stack/create-stack-navigator.tsx +11 -1
  110. package/src/stack/stack-pages.ts +14 -12
  111. package/src/stack/stack-view.tsx +10 -16
  112. package/src/stack/types.ts +4 -3
  113. package/src/tab-view/create-tab-view-navigator.tsx +122 -0
  114. package/src/tab-view/page-registry.ts +139 -0
  115. package/src/tab-view/tab-view-host.tsx +474 -0
  116. package/src/tab-view/tab-view-router.ts +528 -0
  117. package/src/tab-view/types.ts +291 -0
  118. package/src/tabs/create-tab-navigator.tsx +11 -1
  119. package/src/tabs/tab-view.tsx +28 -22
  120. package/src/tabs/types.ts +6 -3
  121. 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 useDrawerRouter = (isCollapsed: boolean): DrawerRouterFactory => {
51
- const [flag] = useState(() => {
52
- let isCurrent = isCollapsed;
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
- return {
55
- read: () => isCurrent,
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
- const Config extends StaticConfig<TypeBag> | undefined = undefined,
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((row: Gtk.ListBoxRow) => {
33
- const state = navigation.getState();
32
+ useCallback(
33
+ (row: Gtk.ListBoxRow) => {
34
+ const state = navigation.getState();
34
35
 
35
- if (!didNavigateToRow(navigation, state, row.getIndex())) {
36
- return;
37
- }
36
+ if (!didNavigateToRow(navigation, state, row.getIndex())) {
37
+ return;
38
+ }
38
39
 
39
- if (isCollapsed) {
40
- navigation.dispatch({ ...DrawerActions.closeDrawer(), target: state.key });
41
- }
42
- }, [navigation, isCollapsed]);
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 accessibleLabel={label}>
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 = (isCollapsed: () => boolean): DrawerRouterFactory => (options) =>
88
- withShownSidebar(DrawerRouter(options), isCollapsed);
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((isShown: boolean | null) => {
49
- if (isShown === null) {
50
- return;
51
- }
52
-
53
- const action = isShown ? DrawerActions.openDrawer() : DrawerActions.closeDrawer();
54
- navigation.dispatch({ ...action, target: navigation.getState().key });
55
- }, [navigation]);
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 topBar={<AdwHeaderBar />}>
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 topBar={<DrawerHeader descriptor={descriptor} navigation={navigation} />}>
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
  ))}
@@ -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
- > & DrawerActionHelpers<ParamList>;
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 { useState } from "react";
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 [hasLoaded, setHasLoaded] = useState(isLoaded);
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
- useUnderline={false}
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
- const Config extends StaticConfig<TypeBag> | undefined = undefined,
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 { AdwNavigationPage, AdwNavigationSplitView, AdwToolbarView } from "@gtkx/jsx/adw";
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, useRef } from "react";
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 settleShowContent = (viewRef: ViewRef, isShown: boolean, isSettled: boolean): void => {
51
- if (isSettled !== isShown && viewRef.current !== null) {
52
- viewRef.current.setShowContent(isSettled);
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 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())) {
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
- if (!isShown) {
66
- popContent(navigation);
67
- }
81
+ breakpoint.addSetter(view, "collapsed", collapsedValue());
82
+ }, [view, breakpoint]);
68
83
 
69
- settleShowContent(viewRef, isShown, hasContent(navigation.getState()));
70
- }, [viewRef, navigation]);
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 = options.header === undefined
87
- ? <HeaderBar options={options} showBackButton={false} />
88
- : <>{options.header({ route, navigation, options, back: undefined })}</>;
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 topBar={topBar}>{descriptor.render()}</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 { collapsed = false, sidebarPosition = "start", minSidebarWidth, maxSidebarWidth } = props;
100
- const viewRef = useRef<Adw.NavigationSplitView | null>(null);
101
- const onShowContentChanged = useContentSync(viewRef, navigation);
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
- <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
- ? (
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
- : <>{contentPlaceholder}</>}
132
- </AdwNavigationPage>
133
- </AdwNavigationSplitView>
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
 
@@ -29,7 +29,8 @@ type SplitViewNavigationProp<
29
29
  StackNavigationState<ParamList>,
30
30
  SplitViewNavigationOptions,
31
31
  SplitViewNavigationEventMap
32
- > & StackActionHelpers<ParamList>;
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
- /** Fills the content pane while no content route is open; defaults to nothing. */
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,