@natsuneko-laboratory/react-native-desktop-navigation 0.1.0-alpha.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.
- package/DesktopNavigation.podspec +18 -0
- package/LICENSE +21 -0
- package/NATIVE.md +300 -0
- package/README.md +127 -0
- package/dist/core/NavigationContainer.d.ts +23 -0
- package/dist/core/NavigationContainer.js +68 -0
- package/dist/core/NavigationItem.d.ts +27 -0
- package/dist/core/NavigationItem.js +71 -0
- package/dist/core/Scene.d.ts +19 -0
- package/dist/core/Scene.js +124 -0
- package/dist/core/SelectionNavigator.d.ts +38 -0
- package/dist/core/SelectionNavigator.js +194 -0
- package/dist/core/builder.d.ts +40 -0
- package/dist/core/builder.js +81 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/context.js +36 -0
- package/dist/core/focus.d.ts +14 -0
- package/dist/core/focus.js +39 -0
- package/dist/core/hooks.d.ts +8 -0
- package/dist/core/hooks.js +48 -0
- package/dist/core/navigation.d.ts +6 -0
- package/dist/core/navigation.js +57 -0
- package/dist/core/store.d.ts +39 -0
- package/dist/core/store.js +391 -0
- package/dist/core/types.d.ts +155 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +46 -0
- package/dist/native/host.d.ts +100 -0
- package/dist/native/host.js +190 -0
- package/dist/native/icons.d.ts +56 -0
- package/dist/native/icons.js +55 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.js +44 -0
- package/dist/native/sidebar.d.ts +39 -0
- package/dist/native/sidebar.js +126 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.d.ts +10 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.js +4 -0
- package/dist/native/split.d.ts +27 -0
- package/dist/native/split.js +105 -0
- package/dist/native/stack.d.ts +26 -0
- package/dist/native/stack.js +66 -0
- package/dist/platform/index.d.ts +52 -0
- package/dist/platform/index.js +28 -0
- package/dist/platform/macos.d.ts +2 -0
- package/dist/platform/macos.js +18 -0
- package/dist/platform/windows.d.ts +2 -0
- package/dist/platform/windows.js +18 -0
- package/dist/routers/index.d.ts +23 -0
- package/dist/routers/index.js +393 -0
- package/dist/routers/types.d.ts +116 -0
- package/dist/routers/types.js +2 -0
- package/dist/sidebar/index.d.ts +17 -0
- package/dist/sidebar/index.js +18 -0
- package/dist/split/index.d.ts +42 -0
- package/dist/split/index.js +199 -0
- package/dist/stack/index.d.ts +23 -0
- package/dist/stack/index.js +88 -0
- package/dist/tabs/index.d.ts +13 -0
- package/dist/tabs/index.js +15 -0
- package/docs/GUIDE.md +384 -0
- package/macos/DDNNavigationComponentView.h +4 -0
- package/macos/DDNNavigationComponentView.mm +40 -0
- package/macos/DDNNavigationView.swift +398 -0
- package/macos/DDNNavigationViewManager.mm +11 -0
- package/package.json +82 -0
- package/react-native.config.js +19 -0
- package/src/core/NavigationContainer.tsx +137 -0
- package/src/core/NavigationItem.tsx +149 -0
- package/src/core/Scene.tsx +194 -0
- package/src/core/SelectionNavigator.tsx +371 -0
- package/src/core/builder.tsx +147 -0
- package/src/core/context.tsx +43 -0
- package/src/core/focus.tsx +50 -0
- package/src/core/hooks.ts +42 -0
- package/src/core/navigation.ts +77 -0
- package/src/core/store.ts +471 -0
- package/src/core/types.ts +176 -0
- package/src/index.ts +36 -0
- package/src/native/host.tsx +377 -0
- package/src/native/icons.ts +110 -0
- package/src/native/index.ts +56 -0
- package/src/native/sidebar.tsx +230 -0
- package/src/native/specs/DesktopNavigationHostNativeComponent.ts +12 -0
- package/src/native/split.tsx +160 -0
- package/src/native/stack.tsx +157 -0
- package/src/platform/index.tsx +76 -0
- package/src/platform/macos.ts +15 -0
- package/src/platform/windows.ts +15 -0
- package/src/routers/index.ts +442 -0
- package/src/routers/types.ts +117 -0
- package/src/sidebar/index.tsx +42 -0
- package/src/split/index.tsx +350 -0
- package/src/stack/index.tsx +213 -0
- package/src/tabs/index.tsx +40 -0
- package/windows/DesktopNavigation/DesktopNavigation.def +3 -0
- package/windows/DesktopNavigation/DesktopNavigation.vcxproj +125 -0
- package/windows/DesktopNavigation/NavigationHost.cpp +432 -0
- package/windows/DesktopNavigation/ReactPackageProvider.cpp +9 -0
- package/windows/DesktopNavigation/ReactPackageProvider.h +10 -0
- package/windows/DesktopNavigation/ReactPackageProvider.idl +6 -0
- package/windows/DesktopNavigation/pch.cpp +1 -0
- package/windows/DesktopNavigation/pch.h +27 -0
package/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Guide
|
|
2
|
+
|
|
3
|
+
This guide covers the full JS API exported from `@natsuneko-laboratory/react-native-desktop-navigation` (the `/` entry point): Stack, Sidebar, Tabs, Split, hooks, keyboard handling, persistence, and styling.
|
|
4
|
+
|
|
5
|
+
For the experimental SwiftUI / WinUI renderer exported from `/native`, see [NATIVE.md](../NATIVE.md). For a full running example, see [examples/desktop](../examples/desktop).
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {
|
|
11
|
+
NavigationContainer,
|
|
12
|
+
createStackNavigator,
|
|
13
|
+
type StackScreenProps,
|
|
14
|
+
} from '@natsuneko-laboratory/react-native-desktop-navigation';
|
|
15
|
+
import { Button, Text } from 'react-native';
|
|
16
|
+
|
|
17
|
+
type Params = {
|
|
18
|
+
Home: undefined;
|
|
19
|
+
Profile: { userId: string };
|
|
20
|
+
};
|
|
21
|
+
const Stack = createStackNavigator<Params>();
|
|
22
|
+
|
|
23
|
+
function Home({ navigation }: StackScreenProps<Params, 'Home'>) {
|
|
24
|
+
return (
|
|
25
|
+
<Button
|
|
26
|
+
title="Profile"
|
|
27
|
+
onPress={() => navigation.push('Profile', { userId: '123' })}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function Profile({ route }: StackScreenProps<Params, 'Profile'>) {
|
|
32
|
+
return <Text>{route.params.userId}</Text>;
|
|
33
|
+
}
|
|
34
|
+
export default function App() {
|
|
35
|
+
return (
|
|
36
|
+
<NavigationContainer>
|
|
37
|
+
<Stack.Navigator
|
|
38
|
+
initialRouteName="Home"
|
|
39
|
+
screenOptions={{ animation: 'fade' }}
|
|
40
|
+
>
|
|
41
|
+
<Stack.Screen name="Home" component={Home} />
|
|
42
|
+
<Stack.Screen
|
|
43
|
+
name="Profile"
|
|
44
|
+
component={Profile}
|
|
45
|
+
options={({ route }) => ({ title: route.params.userId })}
|
|
46
|
+
/>
|
|
47
|
+
</Stack.Navigator>
|
|
48
|
+
</NavigationContainer>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Calling `navigate('Profile')` with a wrong route name or params produces a type error. If a screen requires params at launch, provide `initialParams` on that Screen. Keep screen declarations stable while mounted; when adding or removing declarations, or changing a Navigator's `id`, change the React `key` to force a remount.
|
|
54
|
+
|
|
55
|
+
| API | Behavior |
|
|
56
|
+
| -------------------------------- | ------------------------------------------------------------------------------------- |
|
|
57
|
+
| `navigate(name, params?)` | Goes back to the closest route with the same name, or pushes a new one if none exists |
|
|
58
|
+
| `push(name, params?)` | Adds a new instance with a unique key |
|
|
59
|
+
| `replace(name, params?)` | Replaces the active route |
|
|
60
|
+
| `pop(count = 1)` | Removes the given number of routes, keeping the root |
|
|
61
|
+
| `popTo(name, params?)` | Goes back to the given route; warns and no-ops if it doesn't exist |
|
|
62
|
+
| `popToRoot()` | Returns to the first route |
|
|
63
|
+
| `goBack()` / `canGoBack()` | Handles going back from the current Navigator to its parent |
|
|
64
|
+
| `goForward()` / `canGoForward()` | Moves forward to the next state in desktop history |
|
|
65
|
+
|
|
66
|
+
`historyBehavior="stack"` is the default. With `"desktop"`, the route array before each transition is kept as Back / Forward history, and the Forward history is discarded when a new transition happens after going Back. There is no cap on history size.
|
|
67
|
+
|
|
68
|
+
Screen options support `title`, `headerShown`, `header`, `headerLeft`, `headerRight`, `contentStyle`, `animation`, `presentation`, `focusBehavior`, and `inactiveBehavior`. `header` and similar options accept a ReactNode or a function that receives props.
|
|
69
|
+
|
|
70
|
+
- `inactiveBehavior`: `keep` (default) keeps the local state of hidden screens without unmounting them. `unmount` destroys the inactive component. The navigation state of child Navigators is preserved either way.
|
|
71
|
+
- `animation`: `default` / `none` / `fade` / `slide-horizontal` / `slide-vertical`. Enter animations are implemented with React Native's `Animated`.
|
|
72
|
+
- `presentation`: `card` / `modal` / `dialog`. `modal` and `dialog` are overlays built with React Native `View`; they are not OS-native dialogs and do not provide a complete keyboard focus trap.
|
|
73
|
+
|
|
74
|
+
## Sidebar / Tabs
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const Sidebar = createSidebarNavigator<{
|
|
78
|
+
Timeline: undefined;
|
|
79
|
+
Albums: undefined;
|
|
80
|
+
Settings: undefined;
|
|
81
|
+
}>();
|
|
82
|
+
|
|
83
|
+
<Sidebar.Navigator width={240} collapsible>
|
|
84
|
+
<Sidebar.Section title="Workspace">
|
|
85
|
+
<Sidebar.Screen name="Timeline" component={Timeline} />
|
|
86
|
+
</Sidebar.Section>
|
|
87
|
+
<Sidebar.Section title="Library">
|
|
88
|
+
<Sidebar.Screen name="Albums" component={Albums} options={{ badge: 3 }} />
|
|
89
|
+
<Sidebar.Screen
|
|
90
|
+
name="Settings"
|
|
91
|
+
component={Settings}
|
|
92
|
+
options={{ disabled: false }}
|
|
93
|
+
/>
|
|
94
|
+
</Sidebar.Section>
|
|
95
|
+
</Sidebar.Navigator>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Sidebar supports `label`, `icon` (a ReactNode, or a function that receives `{ focused, disabled }`), `badge`, `hidden`, and `disabled`. Use `select(name)` or `navigate(name, params?)` to select an item, and `collapseSidebar()` / `expandSidebar()` / `toggleSidebar()` to open and close it. It also supports `position="right"`, `width` / `minWidth` / `maxWidth`, `collapsedWidth`, and `defaultCollapsed`.
|
|
99
|
+
|
|
100
|
+
Arrow Up / Down, Home, and End move focus among visible, enabled items, and Enter selects the focused item. Avoid configurations where every item is hidden or disabled.
|
|
101
|
+
|
|
102
|
+
`createTabNavigator<Params>()` also returns `Navigator` and `Screen`. Tabs render at the top of the screen and support `select` / `navigate` / `nextTab` / `previousTab`. Switch tabs with Ctrl+Tab / Ctrl+Shift+Tab, and use ← / → / Home / End / Enter within the tab bar.
|
|
103
|
+
|
|
104
|
+
## Split and nesting
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
const Split = createSplitNavigator();
|
|
108
|
+
|
|
109
|
+
<Split.Navigator
|
|
110
|
+
layout={({ width }) => (width < 700 ? ['content'] : ['sidebar', 'content'])}
|
|
111
|
+
onColumnResize={(id, width) => console.log(id, width)}
|
|
112
|
+
>
|
|
113
|
+
<Split.Column
|
|
114
|
+
id="sidebar"
|
|
115
|
+
component={SidebarNavigation}
|
|
116
|
+
minWidth={180}
|
|
117
|
+
maxWidth={320}
|
|
118
|
+
/>
|
|
119
|
+
<Split.Column id="content" component={ContentNavigation} minWidth={320} />
|
|
120
|
+
</Split.Navigator>;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Split supports 2 or 3 columns. Dragging a divider with the mouse, pressing ← / →, or using accessibility increase/decrease actions updates the widths of the two adjacent columns together, respects the min/max on both sides, and preserves the position of the other divider when there are three columns. The displayed width is computed from the measured width of the Split itself and of each Divider, and the last visible column absorbs the remaining width. When the window shrinks, each column shrinks down to its `minWidth`. If the total falls below the sum of all columns' `minWidth`, hide a column via `layout`. `layout` must return one or more unique, known column IDs. Hidden columns keep their component and navigation state. Whether a column is shown or hidden is determined entirely by the return value of `layout`.
|
|
124
|
+
|
|
125
|
+
### Making the Sidebar bar follow the Divider too
|
|
126
|
+
|
|
127
|
+
If the Sidebar's bar has a fixed numeric width, changing the Split column's width won't change the bar's own width. When you place the item list in a dedicated Split column, specify `width="fill"`. Dragging the column then makes the items, selection background, and footer all follow the column's width. The bar never draws beyond the column's width.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
function SidebarNavigation() {
|
|
131
|
+
return (
|
|
132
|
+
<Sidebar.Navigator width="fill" collapsedWidth={56}>
|
|
133
|
+
{/* Sidebar.Screen for the item list; screen content lives in another Split column */}
|
|
134
|
+
</Sidebar.Navigator>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
<Split.Navigator onColumnResize={(id, width) => saveWidth(id, width)}>
|
|
139
|
+
<Split.Column
|
|
140
|
+
id="sidebar"
|
|
141
|
+
component={SidebarNavigation}
|
|
142
|
+
defaultWidth={240}
|
|
143
|
+
minWidth={180}
|
|
144
|
+
maxWidth={400}
|
|
145
|
+
/>
|
|
146
|
+
<Split.Column id="content" component={ContentNavigation} minWidth={320} />
|
|
147
|
+
</Split.Navigator>;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`width="fill"` makes the Sidebar's bar use the entire width of its parent. If you display a screen next to the bar within the same Sidebar Navigator, keep using a numeric `width` as before. Collapsing a Sidebar placed directly in a `Split.Column` also shrinks the column width and the Divider down to `collapsedWidth` (default 56). Expanding it restores the last drag-adjusted column width, recalculated to fit the current window width and each column's min/max. This also works for a right-side Sidebar, `defaultCollapsed`, programmatic open/close, and restoring from saved state.
|
|
151
|
+
|
|
152
|
+
While collapsed, `collapsedWidth` takes priority over the column's `minWidth` and pins the column at that width. To keep the column width fixed and only open/close the Sidebar's bar, set `collapsible={false}` on the `Split.Column`. A Sidebar nested further inside a Stack or Tabs screen does not affect the outer Split column's width.
|
|
153
|
+
|
|
154
|
+
Column widths and divider positions read the same navigation state. `onColumnResize` is called for each column whose width changed, whether from a drag or a linked Sidebar open/close, so a single drag can fire two notifications (for the columns on either side). Width recalculation from a window resize is reflected via `onStateChange`. Neither resizing nor toggling divider visibility remounts a column's component.
|
|
155
|
+
|
|
156
|
+
A Navigator can be placed inside a Screen's or Column's component. Actions are handled by the current Navigator and, if unhandled, propagate to the parent. Between sibling Navigators, you can target one explicitly with `dispatch({ type, target: nodeId, ... })`. When placing multiple Navigators of the same kind under the same Screen, give each a unique `id`.
|
|
157
|
+
|
|
158
|
+
## Hooks / Events / Focus
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const navigation = useNavigation<StackNavigation<Params>>();
|
|
162
|
+
const route = useRoute<NavigationRoute<'Profile', Params['Profile']>>();
|
|
163
|
+
const activeKey = useNavigationState((state) => state.activeRouteKey);
|
|
164
|
+
|
|
165
|
+
useFocusEffect(
|
|
166
|
+
React.useCallback(() => {
|
|
167
|
+
const unsubscribe = subscribeToUpdates();
|
|
168
|
+
return unsubscribe;
|
|
169
|
+
}, []),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
React.useEffect(
|
|
173
|
+
() =>
|
|
174
|
+
navigation.addListener('beforeRemove', (event) => {
|
|
175
|
+
if (hasUnsavedChanges) event.preventDefault();
|
|
176
|
+
}),
|
|
177
|
+
[navigation, hasUnsavedChanges],
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`focus` / `blur` / `beforeRemove` / `state` events are dispatched, and `addListener` returns an unsubscribe function. When an action such as going back or replacing also removes child screens, their `beforeRemove` listeners are checked too; if any one of them is prevented, the whole action is not applied. On a focus change, `blur` is dispatched from child to parent, and `focus` from parent to child.
|
|
182
|
+
|
|
183
|
+
Because desktop focus APIs differ per platform, register native controls that need reliable focus restoration with `NavigationFocusable`.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<NavigationFocusable id="search">
|
|
187
|
+
<TextInput accessibilityLabel="Search" />
|
|
188
|
+
</NavigationFocusable>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The child should be a control that exposes `ref.focus()` and `onFocus`. `focusBehavior="restore"` (default) restores the most recently registered control, `"first"` restores the first registered control, and `"none"` leaves it to the app. There is no automatic restoration that scans unregistered native controls.
|
|
192
|
+
|
|
193
|
+
## Keyboard / Platform Adapter
|
|
194
|
+
|
|
195
|
+
The default Back / Forward shortcuts are Cmd+[ / Cmd+] on macOS and Alt+Left / Alt+Right on Windows. Key events that bubble within the Container are handled there. Events already handled by a child, and events during TextInput editing, are left alone. To implement your own shortcut scope, call `stopPropagation()` in the child's handler.
|
|
196
|
+
|
|
197
|
+
You can disable this with `keyboardShortcuts={false}`. Pass shortcut arrays via `{ back, forward, nextTab, previousTab }` to override the defaults. Use `platformAdapter` to replace transitions, default widths, TextInput detection, and key bindings.
|
|
198
|
+
|
|
199
|
+
Native event wiring is based on the official [macOS View events](https://microsoft.github.io/react-native-macos/api/view-events) and [Windows IKeyboardProps](https://microsoft.github.io/react-native-windows/docs/ikeyboardprops-api/) docs. Verify native key event / focus behavior against your app's RN desktop version.
|
|
200
|
+
|
|
201
|
+
## Persistence and Ref
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const navigationRef = createNavigationRef<Params>();
|
|
205
|
+
const initialState = savedJson ? restoreNavigationState(savedJson) : undefined;
|
|
206
|
+
|
|
207
|
+
<NavigationContainer
|
|
208
|
+
ref={navigationRef}
|
|
209
|
+
initialState={initialState}
|
|
210
|
+
onStateChange={(state) => save(serializeNavigationState(state))}
|
|
211
|
+
>
|
|
212
|
+
<AppNavigator />
|
|
213
|
+
</NavigationContainer>;
|
|
214
|
+
|
|
215
|
+
if (navigationRef.isReady()) {
|
|
216
|
+
navigationRef.navigate('Profile', { userId: '123' });
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Ref operations before the Container mounts or after it unmounts are no-ops. `initialState` is only read on the first mount. The snapshot is a version-1 node tree; putting non-JSON values such as functions, circular references, or `Date` objects into params will throw. Components, options, React local state, and the last-focused control are not persisted. You choose the storage backend.
|
|
221
|
+
|
|
222
|
+
`theme` accepts a `NavigationTheme`, and `DefaultTheme` / `useNavigationTheme` are exported.
|
|
223
|
+
|
|
224
|
+
## Customizing component design
|
|
225
|
+
|
|
226
|
+
Overall coloring comes from the Container's `theme`; shape, spacing, and text for each part are set through the following props / options. They accept `StyleSheet.create` values, arrays, and regular React Native `StyleProp`s.
|
|
227
|
+
|
|
228
|
+
| Target | Customization API |
|
|
229
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
230
|
+
| Container / whole Navigator | `style` (Container also has `theme`) |
|
|
231
|
+
| Screen | `sceneStyle` (outer frame) and `contentStyle` (screen content) in `screenOptions` / Screen `options` |
|
|
232
|
+
| Stack Header | `headerStyle`, `headerTitleStyle`, `headerTintColor` |
|
|
233
|
+
| Header left/right, Back | `headerLeftContainerStyle`, `headerRightContainerStyle`, `headerBackButtonStyle`, `headerBackTitleStyle`, `headerBackTitle` |
|
|
234
|
+
| Modal / Dialog | `overlayStyle` (background) and `dialogStyle` (dialog surface) in Stack options |
|
|
235
|
+
| Sidebar / Tabs bar | Navigator's `barStyle`, `barContentStyle` (inside the scroll area), `contentStyle` (outer frame of the screen area) |
|
|
236
|
+
| Sidebar / Tabs items | Screen options' `itemStyle`, `labelStyle`, `iconContainerStyle`, `badgeStyle`, `renderItemContent` |
|
|
237
|
+
| Sidebar Section heading | Navigator's `sectionStyle` / `sectionTitleStyle`, Section's `style` / `titleStyle` / `renderTitle` |
|
|
238
|
+
| Sidebar collapse button | Navigator's `collapseButtonShown`, `collapseButtonStyle`, `collapseLabelStyle`, `renderCollapseButtonContent` |
|
|
239
|
+
| Split columns | Navigator's `columnStyle`, Column's `style` / `contentStyle` |
|
|
240
|
+
| Split dividers | Navigator's / Column's `dividerShown`, `dividerStyle`, `renderDivider` |
|
|
241
|
+
|
|
242
|
+
`screenOptions` sets the default for every Screen. Specifying the same option on a Screen's `options` overrides it for that screen. Your styles are applied after the built-in default styles. However, navigation itself still controls `display: none` on hidden scenes, Split column widths / min-max / visibility, and `opacity` / `transform` during transitions. Set column widths via `defaultWidth` / `minWidth` / `maxWidth` and resize interactions.
|
|
243
|
+
|
|
244
|
+
### Selected, focused, hovered, and pressed states
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<Sidebar.Navigator
|
|
248
|
+
barStyle={{ backgroundColor: '#201a2b', borderRightWidth: 0 }}
|
|
249
|
+
barContentStyle={{ padding: 8 }}
|
|
250
|
+
screenOptions={{
|
|
251
|
+
itemStyle: ({ selected, focused, hovered, pressed, disabled }) => ({
|
|
252
|
+
backgroundColor: pressed
|
|
253
|
+
? '#68458c'
|
|
254
|
+
: selected
|
|
255
|
+
? '#493265'
|
|
256
|
+
: hovered
|
|
257
|
+
? '#30253e'
|
|
258
|
+
: 'transparent',
|
|
259
|
+
borderColor: focused ? '#c4a0ff' : 'transparent',
|
|
260
|
+
opacity: disabled ? 0.4 : 1,
|
|
261
|
+
borderRadius: 10,
|
|
262
|
+
}),
|
|
263
|
+
labelStyle: ({ selected }) => ({
|
|
264
|
+
color: '#fff',
|
|
265
|
+
fontWeight: selected ? '700' : '400',
|
|
266
|
+
}),
|
|
267
|
+
badgeStyle: { color: '#ddc9ff', paddingHorizontal: 6 },
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{/* Sidebar.Screen / Sidebar.Section */}
|
|
271
|
+
</Sidebar.Navigator>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Each of the four item style options accepts either a static style or a function that receives `NavigationItemState`. The state includes `selected`, `focused`, `hovered`, `pressed`, `disabled`, and `collapsed`. `selected` reflects the currently displayed screen, and `focused` reflects native / keyboard focus on the item. The existing `icon({ focused })` still uses `focused` to mean the selected state, as before. `badgeStyle` styles a string/number badge; for a ReactNode badge, style the node itself.
|
|
275
|
+
|
|
276
|
+
### Hiding the Divider / replacing the Sidebar footer
|
|
277
|
+
|
|
278
|
+
To fully hide a Split divider, set `dividerShown={false}`. This removes not just the line, but also the divider's hit area, the mouse-drag region, and its keyboard / accessibility actions. The column's component and state are preserved.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
<Split.Navigator dividerShown={false}>
|
|
282
|
+
<Split.Column id="sidebar" component={SidebarNavigation} minWidth={180} />
|
|
283
|
+
<Split.Column id="content" component={ContentNavigation} minWidth={320} />
|
|
284
|
+
</Split.Navigator>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Setting `dividerShown` on a Column overrides the Navigator's setting for the divider immediately after that column. The last visible column never shows a divider. `renderDivider={() => null}` is the existing API for hiding only the divider's contents; use `dividerShown` when you want to remove the whole hit area. To keep resize interactions but make only the line invisible, use `dividerStyle={{ backgroundColor: 'transparent' }}`.
|
|
288
|
+
|
|
289
|
+
The Sidebar's own edge border is separate from the Split divider. To remove both, also set `barStyle={{ borderLeftWidth: 0, borderRightWidth: 0 }}` on the Sidebar.
|
|
290
|
+
|
|
291
|
+
The `«` shown at the bottom of the Sidebar is its collapse button. To hide only the button, set `collapseButtonShown={false}`. To replace the entire footer with custom UI, use `renderSidebarFooter`.
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
<Sidebar.Navigator
|
|
295
|
+
sidebarFooterStyle={{ padding: 8 }}
|
|
296
|
+
renderSidebarFooter={({ collapsed, toggleSidebar }) => (
|
|
297
|
+
<Pressable
|
|
298
|
+
accessibilityRole="button"
|
|
299
|
+
accessibilityLabel={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
300
|
+
focusable
|
|
301
|
+
onPress={toggleSidebar}
|
|
302
|
+
style={{ padding: 10, borderWidth: 0 }}
|
|
303
|
+
>
|
|
304
|
+
<Text>{collapsed ? 'Expand' : 'Collapse'}</Text>
|
|
305
|
+
</Pressable>
|
|
306
|
+
)}
|
|
307
|
+
>
|
|
308
|
+
{/* Sidebar.Screen */}
|
|
309
|
+
</Sidebar.Navigator>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
This renderer entirely replaces the default Pressable. `renderSidebarFooter={() => null}` also removes the footer's outer frame and padding. The existing `renderCollapseButtonContent` remains available as the API for replacing only the contents of the default button.
|
|
313
|
+
|
|
314
|
+
`SidebarFooterProps` provides `collapsed`, `toggleSidebar` / `collapseSidebar` / `expandSidebar`, and `children`, the default button's contents. Rendering `children` lets you include the default button inside your own footer. When `collapseButtonShown={false}` or `collapsible={false}`, `children` is `null`. A fully custom footer can still be shown independent of these settings. Labels, roles, and key handling for a fully replaced UI are your app's responsibility.
|
|
315
|
+
|
|
316
|
+
### Replacing item and divider contents
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
<Sidebar.Screen
|
|
320
|
+
name="Albums"
|
|
321
|
+
component={Albums}
|
|
322
|
+
options={{
|
|
323
|
+
renderItemContent: ({ label, selected, collapsed, children }) => (
|
|
324
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
325
|
+
<View style={{ width: 3, height: 20, backgroundColor: selected ? '#c4a0ff' : 'transparent' }} />
|
|
326
|
+
{children}
|
|
327
|
+
{!collapsed && <Text accessibilityElementsHidden>{label.length}</Text>}
|
|
328
|
+
</View>
|
|
329
|
+
),
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
<Split.Navigator
|
|
334
|
+
dividerStyle={({ dragging }) => ({ width: 12, backgroundColor: dragging ? '#493265' : '#222' })}
|
|
335
|
+
renderDivider={({ columnId, width, dragging }) => (
|
|
336
|
+
<View style={{ width: 3, height: 40, backgroundColor: dragging ? '#c4a0ff' : '#555' }} />
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
339
|
+
{/* Split.Column */}
|
|
340
|
+
</Split.Navigator>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`renderItemContent`'s `children` is the standard icon / label / badge with your styles already applied. You can also replace the whole thing. The library still manages the outer Pressable, so click handling, keyboard selection, `disabled`, the accessibility label, and selection state all keep working. Return only display nodes; do not nest independent interactive elements such as buttons inside the return value.
|
|
344
|
+
|
|
345
|
+
`renderCollapseButtonContent` also receives the standard `children` and `collapsed`. `renderDivider` receives `columnId`, the column's current `width`, and `dragging`, while the outer resize / keyboard / accessibility handling is preserved. A renderer set on a Column takes priority over the Navigator's renderer, and divider styles are applied Navigator first, then Column.
|
|
346
|
+
|
|
347
|
+
The whole header can also be replaced with the existing `header`, and its left/right contents with `headerLeft` / `headerRight`. When you replace these entirely, implement your own rendering, interaction, and accessibility attributes in the renderer. The default Dialog background color is `theme.colors.surface`.
|
|
348
|
+
|
|
349
|
+
Styles and renderers you configure are not saved into the navigation state JSON. See [Customization.tsx](../examples/desktop/Customization.tsx) for a runnable example.
|
|
350
|
+
|
|
351
|
+
## Verification and compatibility
|
|
352
|
+
|
|
353
|
+
The peerDependencies are React `>=18` / React Native `>=0.78`. This is the installable range, not a guarantee that every combination has been verified natively.
|
|
354
|
+
|
|
355
|
+
| Library | React / RN types | RN macOS native | RN Windows native |
|
|
356
|
+
| ------- | ----------------------------------------------------- | --------------- | ----------------- |
|
|
357
|
+
| 0.1.0 | Locally automated tests with React 19.1.0 / RN 0.81.0 | Unverified | Unverified |
|
|
358
|
+
|
|
359
|
+
CI runs type checking, Router / Store tests, React UI tests with native primitives replaced, and package builds on Linux / macOS / Windows. As of adding this CI configuration, its results on remote CI runners have not yet been confirmed. Only versions that have completed real-device verification will be added to this table.
|
|
360
|
+
|
|
361
|
+
```sh
|
|
362
|
+
npm ci
|
|
363
|
+
npm run check
|
|
364
|
+
npm run format:check
|
|
365
|
+
npm pack --dry-run
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
An [Example App](../examples/desktop/App.tsx) and [run / acceptance instructions](../examples/desktop/README.md) are provided. Automated tests verify screen retention, nesting, focus restoration, per-OS key bindings, Tabs, Split dragging, responsive layout, and serialize / restore.
|
|
369
|
+
|
|
370
|
+
## Internal structure
|
|
371
|
+
|
|
372
|
+
```text
|
|
373
|
+
src/core Store / Container / hooks / events / focus / scene
|
|
374
|
+
src/routers Platform- and React-independent state transitions
|
|
375
|
+
src/stack Stack / Header
|
|
376
|
+
src/sidebar Sidebar / Section
|
|
377
|
+
src/tabs Desktop Tabs
|
|
378
|
+
src/split Columns / resizing / responsive layout
|
|
379
|
+
src/platform macOS / Windows adapters and the type boundary for native props
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The Router alone is available from `@natsuneko-laboratory/react-native-desktop-navigation/routers`. A Navigator dispatches actions to the Store; it never mutates the Router or State directly.
|
|
383
|
+
|
|
384
|
+
Multi-window support, detachable tabs, native title bars, Group, deep linking, and custom transition drivers are out of scope for this version.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#import "DDNNavigationComponentView.h"
|
|
2
|
+
#if __has_include(<DesktopNavigation/DesktopNavigation-Swift.h>)
|
|
3
|
+
#import <DesktopNavigation/DesktopNavigation-Swift.h>
|
|
4
|
+
#else
|
|
5
|
+
#import "DesktopNavigation-Swift.h"
|
|
6
|
+
#endif
|
|
7
|
+
#import <react/renderer/components/DesktopNavigationSpec/ComponentDescriptors.h>
|
|
8
|
+
#import <react/renderer/components/DesktopNavigationSpec/EventEmitters.h>
|
|
9
|
+
#import <react/renderer/components/DesktopNavigationSpec/Props.h>
|
|
10
|
+
|
|
11
|
+
using namespace facebook::react;
|
|
12
|
+
|
|
13
|
+
@implementation DDNNavigationComponentView {
|
|
14
|
+
DDNNavigationView *_navigationView;
|
|
15
|
+
}
|
|
16
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
17
|
+
return concreteComponentDescriptorProvider<DesktopNavigationHostComponentDescriptor>();
|
|
18
|
+
}
|
|
19
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
20
|
+
if (self = [super initWithFrame:frame]) {
|
|
21
|
+
_props = std::make_shared<const DesktopNavigationHostProps>();
|
|
22
|
+
_navigationView = [[DDNNavigationView alloc] initWithFrame:frame];
|
|
23
|
+
__weak DDNNavigationComponentView *weakSelf = self;
|
|
24
|
+
_navigationView.onEvent = ^(NSString *payload) {
|
|
25
|
+
DDNNavigationComponentView *strongSelf = weakSelf;
|
|
26
|
+
if (!strongSelf || !strongSelf->_eventEmitter) return;
|
|
27
|
+
auto emitter = std::static_pointer_cast<const DesktopNavigationHostEventEmitter>(strongSelf->_eventEmitter);
|
|
28
|
+
emitter->onNavigationEvent({std::string(payload.UTF8String)});
|
|
29
|
+
};
|
|
30
|
+
self.contentView = _navigationView;
|
|
31
|
+
}
|
|
32
|
+
return self;
|
|
33
|
+
}
|
|
34
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
|
|
35
|
+
const auto &next = *std::static_pointer_cast<const DesktopNavigationHostProps>(props);
|
|
36
|
+
[_navigationView setConfiguration:[NSString stringWithUTF8String:next.configuration.c_str()]];
|
|
37
|
+
[super updateProps:props oldProps:oldProps];
|
|
38
|
+
}
|
|
39
|
+
+ (BOOL)shouldBeRecycled { return NO; }
|
|
40
|
+
@end
|