@gtkx/navigation 2.0.0-beta.10 → 2.0.0-beta.12

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 (89) hide show
  1. package/README.md +8 -168
  2. package/dist/drawer/create-drawer-navigator.d.ts.map +1 -1
  3. package/dist/drawer/create-drawer-navigator.js +2 -2
  4. package/dist/drawer/create-drawer-navigator.js.map +1 -1
  5. package/dist/drawer/drawer-item-list.d.ts.map +1 -1
  6. package/dist/drawer/drawer-item-list.js +5 -3
  7. package/dist/drawer/drawer-item-list.js.map +1 -1
  8. package/dist/drawer/drawer-router.d.ts.map +1 -1
  9. package/dist/drawer/drawer-router.js.map +1 -1
  10. package/dist/drawer/drawer-status.d.ts +2 -2
  11. package/dist/drawer/drawer-status.d.ts.map +1 -1
  12. package/dist/drawer/drawer-status.js +1 -9
  13. package/dist/drawer/drawer-status.js.map +1 -1
  14. package/dist/drawer/drawer-view.d.ts.map +1 -1
  15. package/dist/drawer/drawer-view.js +8 -12
  16. package/dist/drawer/drawer-view.js.map +1 -1
  17. package/dist/drawer/types.d.ts +2 -0
  18. package/dist/drawer/types.d.ts.map +1 -1
  19. package/dist/drawer/types.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/shared/header-bar.d.ts.map +1 -1
  25. package/dist/shared/header-bar.js +2 -2
  26. package/dist/shared/header-bar.js.map +1 -1
  27. package/dist/shared/routes.d.ts.map +1 -1
  28. package/dist/shared/scene-page.d.ts +3 -1
  29. package/dist/shared/scene-page.d.ts.map +1 -1
  30. package/dist/shared/scene-page.js +8 -1
  31. package/dist/shared/scene-page.js.map +1 -1
  32. package/dist/shared/use-pop-to-top-on-blur.d.ts.map +1 -1
  33. package/dist/split-view/create-split-view-navigator.d.ts.map +1 -1
  34. package/dist/split-view/create-split-view-navigator.js +2 -2
  35. package/dist/split-view/create-split-view-navigator.js.map +1 -1
  36. package/dist/split-view/split-view.d.ts.map +1 -1
  37. package/dist/split-view/split-view.js +3 -7
  38. package/dist/split-view/split-view.js.map +1 -1
  39. package/dist/split-view/types.d.ts.map +1 -1
  40. package/dist/split-view/types.js.map +1 -1
  41. package/dist/stack/create-stack-navigator.d.ts.map +1 -1
  42. package/dist/stack/create-stack-navigator.js +2 -2
  43. package/dist/stack/create-stack-navigator.js.map +1 -1
  44. package/dist/stack/stack-pages.d.ts.map +1 -1
  45. package/dist/stack/stack-pages.js +4 -14
  46. package/dist/stack/stack-pages.js.map +1 -1
  47. package/dist/stack/stack-view.d.ts.map +1 -1
  48. package/dist/stack/stack-view.js +5 -22
  49. package/dist/stack/stack-view.js.map +1 -1
  50. package/dist/stack/sync-navigation-stack.d.ts.map +1 -1
  51. package/dist/stack/types.d.ts.map +1 -1
  52. package/dist/stack/types.js.map +1 -1
  53. package/dist/tabs/create-tab-navigator.d.ts.map +1 -1
  54. package/dist/tabs/create-tab-navigator.js +2 -2
  55. package/dist/tabs/create-tab-navigator.js.map +1 -1
  56. package/dist/tabs/tab-view.d.ts.map +1 -1
  57. package/dist/tabs/tab-view.js +5 -9
  58. package/dist/tabs/tab-view.js.map +1 -1
  59. package/dist/tabs/types.d.ts +2 -0
  60. package/dist/tabs/types.d.ts.map +1 -1
  61. package/dist/tabs/types.js.map +1 -1
  62. package/dist/theme.d.ts.map +1 -1
  63. package/dist/theme.js.map +1 -1
  64. package/package.json +13 -21
  65. package/src/drawer/create-drawer-navigator.tsx +14 -16
  66. package/src/drawer/drawer-item-list.tsx +22 -15
  67. package/src/drawer/drawer-router.ts +4 -2
  68. package/src/drawer/drawer-status.ts +3 -16
  69. package/src/drawer/drawer-view.tsx +18 -23
  70. package/src/drawer/types.ts +6 -3
  71. package/src/index.ts +1 -5
  72. package/src/shared/header-bar.tsx +2 -2
  73. package/src/shared/scene-page.tsx +24 -12
  74. package/src/split-view/create-split-view-navigator.tsx +15 -17
  75. package/src/split-view/split-view.tsx +32 -26
  76. package/src/split-view/types.ts +4 -3
  77. package/src/stack/create-stack-navigator.tsx +9 -11
  78. package/src/stack/stack-pages.ts +14 -24
  79. package/src/stack/stack-view.tsx +9 -29
  80. package/src/stack/types.ts +4 -3
  81. package/src/tabs/create-tab-navigator.tsx +3 -5
  82. package/src/tabs/tab-view.tsx +28 -24
  83. package/src/tabs/types.ts +6 -3
  84. package/src/theme.ts +0 -1
  85. package/dist/shared/use-loaded-routes.d.ts +0 -3
  86. package/dist/shared/use-loaded-routes.d.ts.map +0 -1
  87. package/dist/shared/use-loaded-routes.js +0 -11
  88. package/dist/shared/use-loaded-routes.js.map +0 -1
  89. package/src/shared/use-loaded-routes.ts +0 -14
@@ -1,5 +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
4
 
4
5
  type ScenePageProps = {
5
6
  name: string;
@@ -7,21 +8,32 @@ type ScenePageProps = {
7
8
  iconName?: string;
8
9
  badgeNumber?: number;
9
10
  needsAttention?: boolean;
11
+ useUnderline?: boolean;
12
+ isFocused: boolean;
10
13
  isLoaded: boolean;
11
14
  render: () => ReactElement;
12
15
  };
13
16
 
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
- );
17
+ const ScenePage = ({ name, title, isFocused, isLoaded, render, ...page }: ScenePageProps): ReactNode => {
18
+ const [hasLoaded, setHasLoaded] = useState(isLoaded);
19
+
20
+ if (isLoaded && !hasLoaded) {
21
+ setHasLoaded(true);
22
+ }
23
+
24
+ return (
25
+ <AdwViewStackPage
26
+ name={name}
27
+ title={title}
28
+ iconName={page.iconName}
29
+ badgeNumber={page.badgeNumber}
30
+ needsAttention={page.needsAttention}
31
+ accessibleHidden={!isFocused}
32
+ useUnderline={page.useUnderline ?? false}
33
+ >
34
+ <AdwBin>{hasLoaded ? render() : null}</AdwBin>
35
+ </AdwViewStackPage>
36
+ );
37
+ };
26
38
 
27
39
  export { ScenePage };
@@ -56,7 +56,7 @@ function SplitViewNavigator({
56
56
  popOnEscape,
57
57
  ...options
58
58
  }: SplitViewNavigatorProps): ReactNode {
59
- const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder<
59
+ const { state, describe, descriptors, navigation, render } = useNavigationBuilder<
60
60
  StackNavigationState<ParamListBase>,
61
61
  StackRouterOptions,
62
62
  StackActionHelpers<ParamListBase>,
@@ -64,22 +64,20 @@ function SplitViewNavigator({
64
64
  SplitViewNavigationEventMap
65
65
  >(splitViewRouter, options);
66
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>
67
+ return render(
68
+ <SplitView
69
+ state={state}
70
+ navigation={navigation}
71
+ descriptors={descriptors}
72
+ describe={describe}
73
+ collapsed={collapsed}
74
+ contentPlaceholder={contentPlaceholder}
75
+ sidebarPosition={sidebarPosition}
76
+ minSidebarWidth={minSidebarWidth}
77
+ maxSidebarWidth={maxSidebarWidth}
78
+ sidebarWidthFraction={sidebarWidthFraction}
79
+ popOnEscape={popOnEscape}
80
+ />,
83
81
  );
84
82
  }
85
83
 
@@ -57,17 +57,20 @@ const useContentSync = (
57
57
  viewRef: ViewRef,
58
58
  navigation: SplitViewNavigationHelpers,
59
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]);
60
+ useCallback(
61
+ (isShown: boolean | null) => {
62
+ if (isShown === null || isShown === hasContent(navigation.getState())) {
63
+ return;
64
+ }
65
+
66
+ if (!isShown) {
67
+ popContent(navigation);
68
+ }
69
+
70
+ settleShowContent(viewRef, isShown, hasContent(navigation.getState()));
71
+ },
72
+ [viewRef, navigation],
73
+ );
71
74
 
72
75
  const EscapeGuard = (): ReactNode => (
73
76
  <GtkEventControllerKey
@@ -83,9 +86,12 @@ const SidebarPage = ({ descriptor }: { descriptor: SplitViewDescriptor }): React
83
86
  return <AdwNavigationPage title={options.title ?? route.name}>{descriptor.render()}</AdwNavigationPage>;
84
87
  }
85
88
 
86
- const topBar = options.header === undefined
87
- ? <HeaderBar options={options} showBackButton={false} />
88
- : <>{options.header({ route, navigation, options, back: undefined })}</>;
89
+ const topBar =
90
+ options.header === undefined ? (
91
+ <HeaderBar options={options} showBackButton={false} />
92
+ ) : (
93
+ <>{options.header({ route, navigation, options, back: undefined })}</>
94
+ );
89
95
 
90
96
  return (
91
97
  <AdwNavigationPage title={options.title ?? route.name}>
@@ -117,18 +123,18 @@ const SplitView = (props: SplitViewProps): ReactNode => {
117
123
  title={contentTitle(state, descriptors)}
118
124
  controllers={collapsed && props.popOnEscape === false ? <EscapeGuard /> : null}
119
125
  >
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}</>}
126
+ {hasContent(state) ? (
127
+ <StackView
128
+ offset={1}
129
+ popOnEscape={props.popOnEscape}
130
+ state={state}
131
+ navigation={navigation}
132
+ descriptors={descriptors}
133
+ describe={describe}
134
+ />
135
+ ) : (
136
+ <>{contentPlaceholder}</>
137
+ )}
132
138
  </AdwNavigationPage>
133
139
  </AdwNavigationSplitView>
134
140
  );
@@ -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<
@@ -84,8 +85,8 @@ type SplitViewNavigatorProps = DefaultNavigatorOptions<
84
85
  SplitViewNavigationEventMap,
85
86
  SplitViewNavigationProp<ParamListBase>
86
87
  > &
87
- StackRouterOptions &
88
- SplitViewNavigationConfig;
88
+ StackRouterOptions &
89
+ SplitViewNavigationConfig;
89
90
 
90
91
  export type {
91
92
  SplitViewDescriptor,
@@ -46,7 +46,7 @@ const createStackScreen: StaticScreenFactory<StackTypeBag> = createScreenFactory
46
46
 
47
47
  /** Renders the screens of a {@link createStackNavigator} as pages of an `AdwNavigationView`. */
48
48
  function StackNavigator({ popOnEscape, ...options }: StackNavigatorProps): ReactNode {
49
- const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder<
49
+ const { state, describe, descriptors, navigation, render } = useNavigationBuilder<
50
50
  StackNavigationState<ParamListBase>,
51
51
  StackRouterOptions,
52
52
  StackActionHelpers<ParamListBase>,
@@ -54,16 +54,14 @@ function StackNavigator({ popOnEscape, ...options }: StackNavigatorProps): React
54
54
  StackNavigationEventMap
55
55
  >(StackRouter, options);
56
56
 
57
- return (
58
- <NavigationContent>
59
- <StackView
60
- popOnEscape={popOnEscape}
61
- state={state}
62
- navigation={navigation}
63
- descriptors={descriptors}
64
- describe={describe}
65
- />
66
- </NavigationContent>
57
+ return render(
58
+ <StackView
59
+ popOnEscape={popOnEscape}
60
+ state={state}
61
+ navigation={navigation}
62
+ descriptors={descriptors}
63
+ describe={describe}
64
+ />,
67
65
  );
68
66
  }
69
67
 
@@ -33,18 +33,6 @@ const neededKeys = (state: StackState, closing: StackDescriptorMap, offset: numb
33
33
  const hasSameMembers = (left: readonly string[], right: readonly string[]): boolean =>
34
34
  left.length === right.length && left.every((key) => right.includes(key));
35
35
 
36
- const hasRefreshedFocus = (tracking: PageTracking, descriptors: StackDescriptorMap): boolean => {
37
- const { focusedKey } = tracking;
38
-
39
- if (focusedKey === undefined) {
40
- return false;
41
- }
42
-
43
- const next = descriptors[focusedKey];
44
-
45
- return next !== undefined && tracking.descriptors[focusedKey]?.route !== next.route;
46
- };
47
-
48
36
  const appendOrder = (previous: readonly string[], needed: readonly string[]): string[] => [
49
37
  ...previous.filter((key) => needed.includes(key)),
50
38
  ...needed.filter((key) => !previous.includes(key)),
@@ -112,27 +100,29 @@ const useStackPages = (
112
100
  ): StackPages => {
113
101
  const [tracking, setTracking] = useState<PageTracking>(() => initialTracking(state, descriptors, offset));
114
102
 
115
- if (!hasSameMembers(tracking.order, neededKeys(state, tracking.closing, offset)) ||
103
+ if (
104
+ !hasSameMembers(tracking.order, neededKeys(state, tracking.closing, offset)) ||
116
105
  tracking.focusedKey !== getFocusedKey(state, offset) ||
117
- hasRefreshedFocus(tracking, descriptors)) {
106
+ tracking.descriptors !== descriptors
107
+ ) {
118
108
  setTracking(advance(tracking, state, descriptors, offset));
119
109
  }
120
110
 
121
111
  const release = useCallback((key: string) => {
122
- setTracking((current) => (current.closing[key] === undefined
123
- ? current
124
- : {
125
- ...current,
126
- order: current.order.filter((entry) => entry !== key),
127
- closing: withoutKeys(current.closing, [key]),
128
- }));
112
+ setTracking((current) =>
113
+ current.closing[key] === undefined
114
+ ? current
115
+ : {
116
+ ...current,
117
+ order: current.order.filter((entry) => entry !== key),
118
+ closing: withoutKeys(current.closing, [key]),
119
+ },
120
+ );
129
121
  }, []);
130
122
 
131
123
  const sources = { state, descriptors, closing: tracking.closing };
132
124
 
133
- const pages = tracking.order
134
- .map((key) => resolvePage(key, sources, describe))
135
- .filter((page) => page !== undefined);
125
+ const pages = tracking.order.map((key) => resolvePage(key, sources, describe)).filter((page) => page !== undefined);
136
126
 
137
127
  return { pages, release };
138
128
  };
@@ -2,6 +2,7 @@ import type * as Adw from "@gtkx/gi/adw";
2
2
  import type { ParamListBase, StackNavigationState } from "@react-navigation/core";
3
3
  import type { ReactNode, RefObject } from "react";
4
4
  import { AdwNavigationPage, AdwNavigationView, AdwToolbarView } from "@gtkx/jsx/adw";
5
+ import { useLatestRef } from "@gtkx/react/internal";
5
6
  import { useCallback, useEffect, useRef } from "react";
6
7
  import type { Describe } from "./stack-pages.js";
7
8
  import type { OptionsByKey } from "./sync-navigation-stack.js";
@@ -11,14 +12,12 @@ import type {
11
12
  StackNavigationConfig,
12
13
  StackNavigationEventMap,
13
14
  StackNavigationHelpers,
14
- StackNavigationOptions,
15
15
  } from "./types.js";
16
16
  import { HeaderBar } from "../shared/header-bar.js";
17
17
  import { useStackPages } from "./stack-pages.js";
18
18
  import { reconcilePoppedStack, syncNavigationStack } from "./sync-navigation-stack.js";
19
19
 
20
20
  type ViewRef = RefObject<Adw.NavigationView | null>;
21
- type MutableOptions = Record<string, StackNavigationOptions>;
22
21
 
23
22
  type StackViewProps = StackNavigationConfig & {
24
23
  state: StackNavigationState<ParamListBase>;
@@ -49,28 +48,6 @@ const previousDescriptor = (
49
48
  return previousKey === undefined ? undefined : descriptors[previousKey];
50
49
  };
51
50
 
52
- const useLatest = <T,>(value: T): RefObject<T> => {
53
- const ref = useRef(value);
54
-
55
- useEffect(() => {
56
- ref.current = value;
57
- }, [value]);
58
-
59
- return ref;
60
- };
61
-
62
- const useTrackedOptions = (descriptors: StackDescriptorMap): RefObject<MutableOptions> => {
63
- const optionsRef = useRef<MutableOptions>({});
64
-
65
- useEffect(() => {
66
- for (const [key, descriptor] of Object.entries(descriptors)) {
67
- optionsRef.current[key] = descriptor.options;
68
- }
69
- }, [descriptors]);
70
-
71
- return optionsRef;
72
- };
73
-
74
51
  const useStackSync = (
75
52
  viewRef: ViewRef,
76
53
  state: StackNavigationState<ParamListBase>,
@@ -98,7 +75,7 @@ const usePoppedHandler = (
98
75
  options: RefObject<OptionsByKey>,
99
76
  offset: number,
100
77
  ): (() => void) => {
101
- const navigationRef = useLatest(navigation);
78
+ const navigationRef = useLatestRef(navigation);
102
79
  const isScheduledRef = useRef(false);
103
80
 
104
81
  return useCallback(() => {
@@ -128,9 +105,12 @@ const StackPageContent = ({ descriptor, previous }: Omit<StackPageProps, "naviga
128
105
 
129
106
  const back = previous === undefined ? undefined : { title: previous.options.title ?? previous.route.name };
130
107
 
131
- const topBar = options.header === undefined
132
- ? <HeaderBar options={options} showBackButton={options.headerBackVisible ?? true} />
133
- : <>{options.header({ route, navigation, options, back })}</>;
108
+ const topBar =
109
+ options.header === undefined ? (
110
+ <HeaderBar options={options} showBackButton={options.headerBackVisible ?? true} />
111
+ ) : (
112
+ <>{options.header({ route, navigation, options, back })}</>
113
+ );
134
114
 
135
115
  return <AdwToolbarView topBar={topBar}>{descriptor.render()}</AdwToolbarView>;
136
116
  };
@@ -179,8 +159,8 @@ const StackPage = ({ descriptor, previous, navigation, onHidden }: StackPageProp
179
159
  const StackView = (props: StackViewProps): ReactNode => {
180
160
  const { state, navigation, descriptors, describe, popOnEscape, offset = 0 } = props;
181
161
  const viewRef = useRef<Adw.NavigationView | null>(null);
182
- const options = useTrackedOptions(descriptors);
183
162
  const { pages, release } = useStackPages(state, descriptors, describe, offset);
163
+ const options = useLatestRef<OptionsByKey>(Object.fromEntries(pages.map((page) => [page.route.key, page.options])));
184
164
  const onPopped = usePoppedHandler(viewRef, navigation, options, offset);
185
165
  useStackSync(viewRef, state, options, offset);
186
166
 
@@ -68,7 +68,8 @@ type StackNavigationProp<
68
68
  StackNavigationState<ParamList>,
69
69
  StackNavigationOptions,
70
70
  StackNavigationEventMap
71
- > & StackActionHelpers<ParamList>;
71
+ > &
72
+ StackActionHelpers<ParamList>;
72
73
 
73
74
  /** Props a stack screen component receives. */
74
75
  type StackScreenProps<
@@ -112,8 +113,8 @@ type StackNavigatorProps = DefaultNavigatorOptions<
112
113
  StackNavigationEventMap,
113
114
  StackNavigationProp<ParamListBase>
114
115
  > &
115
- StackRouterOptions &
116
- StackNavigationConfig;
116
+ StackRouterOptions &
117
+ StackNavigationConfig;
117
118
 
118
119
  export type {
119
120
  StackDescriptor,
@@ -41,7 +41,7 @@ const createTabScreen: StaticScreenFactory<TabTypeBag> = createScreenFactory<Tab
41
41
 
42
42
  /** Renders the screens of a {@link createTabNavigator} as pages of an `AdwViewStack` with a view switcher. */
43
43
  function TabNavigator({ tabBarPosition, ...options }: TabNavigatorProps): ReactNode {
44
- const { state, descriptors, navigation, NavigationContent } = useNavigationBuilder<
44
+ const { state, descriptors, navigation, render } = useNavigationBuilder<
45
45
  TabNavigationState<ParamListBase>,
46
46
  TabRouterOptions,
47
47
  TabActionHelpers<ParamListBase>,
@@ -49,10 +49,8 @@ function TabNavigator({ tabBarPosition, ...options }: TabNavigatorProps): ReactN
49
49
  TabNavigationEventMap
50
50
  >(TabRouter, options);
51
51
 
52
- return (
53
- <NavigationContent>
54
- <TabView tabBarPosition={tabBarPosition} state={state} navigation={navigation} descriptors={descriptors} />
55
- </NavigationContent>
52
+ return render(
53
+ <TabView tabBarPosition={tabBarPosition} state={state} navigation={navigation} descriptors={descriptors} />,
56
54
  );
57
55
  }
58
56
 
@@ -8,7 +8,6 @@ import type { TabDescriptor, TabDescriptorMap, TabNavigationConfig, TabNavigatio
8
8
  import { HeaderBar } from "../shared/header-bar.js";
9
9
  import { getFocusedRoute, requireDescriptor } from "../shared/routes.js";
10
10
  import { ScenePage } from "../shared/scene-page.js";
11
- import { useLoadedRoutes } from "../shared/use-loaded-routes.js";
12
11
  import { usePopToTopOnBlur } from "../shared/use-pop-to-top-on-blur.js";
13
12
 
14
13
  type TabViewProps = TabNavigationConfig & {
@@ -24,25 +23,29 @@ type TabHeaderProps = {
24
23
 
25
24
  type TabPageProps = {
26
25
  descriptor: TabDescriptor;
26
+ isFocused: boolean;
27
27
  isLoaded: boolean;
28
28
  };
29
29
 
30
30
  const useTabSelection = (navigation: TabNavigationHelpers): ((name: string | null) => void) =>
31
- useCallback((name: string | null) => {
32
- const current = navigation.getState();
33
- const focusedKey = getFocusedRoute(current).key;
34
- const route = current.routes.find((candidate) => candidate.key === name);
35
-
36
- if (route === undefined || route.key === focusedKey) {
37
- return;
38
- }
39
-
40
- const event = navigation.emit({ type: "tabPress", target: route.key, canPreventDefault: true });
41
-
42
- if (!event.defaultPrevented) {
43
- navigation.dispatch({ ...CommonActions.navigate(route.name, route.params), target: current.key });
44
- }
45
- }, [navigation]);
31
+ useCallback(
32
+ (name: string | null) => {
33
+ const current = navigation.getState();
34
+ const focusedKey = getFocusedRoute(current).key;
35
+ const route = current.routes.find((candidate) => candidate.key === name);
36
+
37
+ if (route === undefined || route.key === focusedKey) {
38
+ return;
39
+ }
40
+
41
+ const event = navigation.emit({ type: "tabPress", target: route.key, canPreventDefault: true });
42
+
43
+ if (!event.defaultPrevented) {
44
+ navigation.dispatch({ ...CommonActions.navigate(route.name, route.params), target: current.key });
45
+ }
46
+ },
47
+ [navigation],
48
+ );
46
49
 
47
50
  const defaultTabHeader = ({ descriptor, viewSwitcher }: TabHeaderProps): ReactElement => (
48
51
  <HeaderBar
@@ -68,16 +71,18 @@ const TabHeader = (props: TabHeaderProps): ReactNode => {
68
71
  );
69
72
  };
70
73
 
71
- const TabPage = ({ descriptor, isLoaded }: TabPageProps): ReactNode => {
74
+ const TabPage = ({ descriptor, isFocused, isLoaded }: TabPageProps): ReactNode => {
72
75
  const { route, options } = descriptor;
73
76
 
74
77
  return (
75
78
  <ScenePage
76
79
  name={route.key}
77
80
  title={options.tabBarLabel ?? options.title ?? route.name}
81
+ useUnderline={options.useUnderline}
78
82
  iconName={options.tabBarIcon}
79
83
  badgeNumber={options.tabBarBadge}
80
84
  needsAttention={options.needsAttention}
85
+ isFocused={isFocused}
81
86
  isLoaded={isLoaded || options.lazy === false}
82
87
  render={() => descriptor.render()}
83
88
  />
@@ -88,21 +93,19 @@ const TabView = ({ state, navigation, descriptors, tabBarPosition = "top" }: Tab
88
93
  const [stack, setStack] = useState<Adw.ViewStack | null>(null);
89
94
  const focused = getFocusedRoute(state);
90
95
  const descriptor = requireDescriptor(descriptors, focused.key);
91
- const loaded = useLoadedRoutes(focused.key, state.preloadedRouteKeys);
92
96
  const onVisibleChildChanged = useTabSelection(navigation);
93
97
 
94
- const viewSwitcher = stack === null
95
- ? undefined
96
- : <AdwViewSwitcher stack={stack} policy={Adw.ViewSwitcherPolicy.WIDE} />;
98
+ const viewSwitcher =
99
+ stack === null ? undefined : <AdwViewSwitcher stack={stack} policy={Adw.ViewSwitcherPolicy.WIDE} />;
97
100
 
98
101
  const hasSwitcherBar = tabBarPosition === "bottom" && stack !== null;
99
102
  usePopToTopOnBlur(state, descriptors, navigation);
100
103
 
101
104
  return (
102
105
  <AdwToolbarView
103
- topBar={(
106
+ topBar={
104
107
  <TabHeader descriptor={descriptor} viewSwitcher={tabBarPosition === "top" ? viewSwitcher : undefined} />
105
- )}
108
+ }
106
109
  bottomBar={hasSwitcherBar ? <AdwViewSwitcherBar stack={stack} reveal /> : undefined}
107
110
  >
108
111
  <AdwViewStack
@@ -115,7 +118,8 @@ const TabView = ({ state, navigation, descriptors, tabBarPosition = "top" }: Tab
115
118
  <TabPage
116
119
  key={route.key}
117
120
  descriptor={requireDescriptor(descriptors, route.key)}
118
- isLoaded={loaded.has(route.key)}
121
+ isFocused={route.key === focused.key}
122
+ isLoaded={route.key === focused.key || state.preloadedRouteKeys.includes(route.key)}
119
123
  />
120
124
  ))}
121
125
  </AdwViewStack>
package/src/tabs/types.ts CHANGED
@@ -29,6 +29,8 @@ type TabNavigationOptions = HeaderOptions & {
29
29
  header?: (props: TabHeaderProps) => ReactNode;
30
30
  /** Label shown in the view switcher; defaults to `title`, then to the route name. */
31
31
  tabBarLabel?: string;
32
+ /** Whether underscores in the tab label indicate mnemonics; defaults to `false`. */
33
+ useUnderline?: boolean;
32
34
  /** Icon name shown in the view switcher. */
33
35
  tabBarIcon?: string;
34
36
  /** Badge number shown in the view switcher; `0` hides the badge. */
@@ -61,7 +63,8 @@ type TabNavigationProp<
61
63
  TabNavigationState<ParamList>,
62
64
  TabNavigationOptions,
63
65
  TabNavigationEventMap
64
- > & TabActionHelpers<ParamList>;
66
+ > &
67
+ TabActionHelpers<ParamList>;
65
68
 
66
69
  /** Props a tab screen component receives. */
67
70
  type TabScreenProps<
@@ -103,8 +106,8 @@ type TabNavigatorProps = DefaultNavigatorOptions<
103
106
  TabNavigationEventMap,
104
107
  TabNavigationProp<ParamListBase>
105
108
  > &
106
- TabRouterOptions &
107
- TabNavigationConfig;
109
+ TabRouterOptions &
110
+ TabNavigationConfig;
108
111
 
109
112
  export type {
110
113
  TabDescriptor,
package/src/theme.ts CHANGED
@@ -11,7 +11,6 @@ type AdwaitaTheme = {
11
11
  };
12
12
 
13
13
  declare module "@react-navigation/core" {
14
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
15
14
  interface Theme extends AdwaitaTheme {}
16
15
  }
17
16
 
@@ -1,3 +0,0 @@
1
- declare const useLoadedRoutes: (focusedKey: string, preloadedKeys: readonly string[]) => ReadonlySet<string>;
2
- export { useLoadedRoutes };
3
- //# sourceMappingURL=use-loaded-routes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-loaded-routes.d.ts","sourceRoot":"","sources":["../../src/shared/use-loaded-routes.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,eAAe,GAAI,YAAY,MAAM,EAAE,eAAe,SAAS,MAAM,EAAE,KAAG,WAAW,CAAC,MAAM,CASjG,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -1,11 +0,0 @@
1
- import { useState } from "react";
2
- const useLoadedRoutes = (focusedKey, preloadedKeys) => {
3
- const [loaded, setLoaded] = useState(() => new Set([focusedKey]));
4
- const pending = [focusedKey, ...preloadedKeys].filter((key) => !loaded.has(key));
5
- if (pending.length > 0) {
6
- setLoaded(new Set([...loaded, ...pending]));
7
- }
8
- return loaded;
9
- };
10
- export { useLoadedRoutes };
11
- //# sourceMappingURL=use-loaded-routes.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-loaded-routes.js","sourceRoot":"","sources":["../../src/shared/use-loaded-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,MAAM,eAAe,GAAG,CAAC,UAAkB,EAAE,aAAgC,EAAuB,EAAE;IAClG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAsB,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACvF,MAAM,OAAO,GAAG,CAAC,UAAU,EAAE,GAAG,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAEjF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC","sourcesContent":["import { useState } from \"react\";\n\nconst useLoadedRoutes = (focusedKey: string, preloadedKeys: readonly string[]): ReadonlySet<string> => {\n const [loaded, setLoaded] = useState<ReadonlySet<string>>(() => new Set([focusedKey]));\n const pending = [focusedKey, ...preloadedKeys].filter((key) => !loaded.has(key));\n\n if (pending.length > 0) {\n setLoaded(new Set([...loaded, ...pending]));\n }\n\n return loaded;\n};\n\nexport { useLoadedRoutes };\n"]}
@@ -1,14 +0,0 @@
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 };