one 1.2.6 → 1.2.8
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/dist/cjs/ui/Slot.cjs +42 -0
- package/dist/cjs/ui/Slot.js +27 -0
- package/dist/cjs/ui/Slot.js.map +6 -0
- package/dist/cjs/ui/Slot.native.js +48 -0
- package/dist/cjs/ui/Slot.native.js.map +1 -0
- package/dist/cjs/ui/TabContext.cjs +44 -0
- package/dist/cjs/ui/TabContext.js +35 -0
- package/dist/cjs/ui/TabContext.js.map +6 -0
- package/dist/cjs/ui/TabContext.native.js +47 -0
- package/dist/cjs/ui/TabContext.native.js.map +1 -0
- package/dist/cjs/ui/TabList.cjs +52 -0
- package/dist/cjs/ui/TabList.js +38 -0
- package/dist/cjs/ui/TabList.js.map +6 -0
- package/dist/cjs/ui/TabList.native.js +57 -0
- package/dist/cjs/ui/TabList.native.js.map +1 -0
- package/dist/cjs/ui/TabRouter.cjs +47 -0
- package/dist/cjs/ui/TabRouter.js +41 -0
- package/dist/cjs/ui/TabRouter.js.map +6 -0
- package/dist/cjs/ui/TabRouter.native.js +57 -0
- package/dist/cjs/ui/TabRouter.native.js.map +1 -0
- package/dist/cjs/ui/TabSlot.cjs +115 -0
- package/dist/cjs/ui/TabSlot.js +91 -0
- package/dist/cjs/ui/TabSlot.js.map +6 -0
- package/dist/cjs/ui/TabSlot.native.js +120 -0
- package/dist/cjs/ui/TabSlot.native.js.map +1 -0
- package/dist/cjs/ui/TabTrigger.cjs +151 -0
- package/dist/cjs/ui/TabTrigger.js +120 -0
- package/dist/cjs/ui/TabTrigger.js.map +6 -0
- package/dist/cjs/ui/TabTrigger.native.js +153 -0
- package/dist/cjs/ui/TabTrigger.native.js.map +1 -0
- package/dist/cjs/ui/Tabs.cjs +175 -0
- package/dist/cjs/ui/Tabs.js +121 -0
- package/dist/cjs/ui/Tabs.js.map +6 -0
- package/dist/cjs/ui/Tabs.native.js +191 -0
- package/dist/cjs/ui/Tabs.native.js.map +1 -0
- package/dist/cjs/ui/common.cjs +160 -0
- package/dist/cjs/ui/common.js +146 -0
- package/dist/cjs/ui/common.js.map +6 -0
- package/dist/cjs/ui/common.native.js +223 -0
- package/dist/cjs/ui/common.native.js.map +1 -0
- package/dist/cjs/ui/index.cjs +18 -0
- package/dist/cjs/ui/index.js +15 -0
- package/dist/cjs/ui/index.js.map +6 -0
- package/dist/cjs/ui/index.native.js +21 -0
- package/dist/cjs/ui/index.native.js.map +1 -0
- package/dist/cjs/ui/useComponent.cjs +46 -0
- package/dist/cjs/ui/useComponent.js +37 -0
- package/dist/cjs/ui/useComponent.js.map +6 -0
- package/dist/cjs/ui/useComponent.native.js +53 -0
- package/dist/cjs/ui/useComponent.native.js.map +1 -0
- package/dist/esm/ui/Slot.js +17 -0
- package/dist/esm/ui/Slot.js.map +6 -0
- package/dist/esm/ui/Slot.mjs +19 -0
- package/dist/esm/ui/Slot.mjs.map +1 -0
- package/dist/esm/ui/Slot.native.js +22 -0
- package/dist/esm/ui/Slot.native.js.map +1 -0
- package/dist/esm/ui/TabContext.js +19 -0
- package/dist/esm/ui/TabContext.js.map +6 -0
- package/dist/esm/ui/TabContext.mjs +17 -0
- package/dist/esm/ui/TabContext.mjs.map +1 -0
- package/dist/esm/ui/TabContext.native.js +17 -0
- package/dist/esm/ui/TabContext.native.js.map +1 -0
- package/dist/esm/ui/TabList.js +24 -0
- package/dist/esm/ui/TabList.js.map +6 -0
- package/dist/esm/ui/TabList.mjs +28 -0
- package/dist/esm/ui/TabList.mjs.map +1 -0
- package/dist/esm/ui/TabList.native.js +30 -0
- package/dist/esm/ui/TabList.native.js.map +1 -0
- package/dist/esm/ui/TabRouter.js +27 -0
- package/dist/esm/ui/TabRouter.js.map +6 -0
- package/dist/esm/ui/TabRouter.mjs +24 -0
- package/dist/esm/ui/TabRouter.mjs.map +1 -0
- package/dist/esm/ui/TabRouter.native.js +31 -0
- package/dist/esm/ui/TabRouter.native.js.map +1 -0
- package/dist/esm/ui/TabSlot.js +80 -0
- package/dist/esm/ui/TabSlot.js.map +6 -0
- package/dist/esm/ui/TabSlot.mjs +89 -0
- package/dist/esm/ui/TabSlot.mjs.map +1 -0
- package/dist/esm/ui/TabSlot.native.js +91 -0
- package/dist/esm/ui/TabSlot.native.js.map +1 -0
- package/dist/esm/ui/TabTrigger.js +115 -0
- package/dist/esm/ui/TabTrigger.js.map +6 -0
- package/dist/esm/ui/TabTrigger.mjs +126 -0
- package/dist/esm/ui/TabTrigger.mjs.map +1 -0
- package/dist/esm/ui/TabTrigger.native.js +125 -0
- package/dist/esm/ui/TabTrigger.native.js.map +1 -0
- package/dist/esm/ui/Tabs.js +130 -0
- package/dist/esm/ui/Tabs.js.map +6 -0
- package/dist/esm/ui/Tabs.mjs +149 -0
- package/dist/esm/ui/Tabs.mjs.map +1 -0
- package/dist/esm/ui/Tabs.native.js +162 -0
- package/dist/esm/ui/Tabs.native.js.map +1 -0
- package/dist/esm/ui/common.js +133 -0
- package/dist/esm/ui/common.js.map +6 -0
- package/dist/esm/ui/common.mjs +135 -0
- package/dist/esm/ui/common.mjs.map +1 -0
- package/dist/esm/ui/common.native.js +195 -0
- package/dist/esm/ui/common.native.js.map +1 -0
- package/dist/esm/ui/index.js +2 -0
- package/dist/esm/ui/index.js.map +6 -0
- package/dist/esm/ui/index.mjs +2 -0
- package/dist/esm/ui/index.mjs.map +1 -0
- package/dist/esm/ui/index.native.js +2 -0
- package/dist/esm/ui/index.native.js.map +1 -0
- package/dist/esm/ui/useComponent.js +22 -0
- package/dist/esm/ui/useComponent.js.map +6 -0
- package/dist/esm/ui/useComponent.mjs +23 -0
- package/dist/esm/ui/useComponent.mjs.map +1 -0
- package/dist/esm/ui/useComponent.native.js +27 -0
- package/dist/esm/ui/useComponent.native.js.map +1 -0
- package/package.json +18 -9
- package/src/ui/README.md +121 -0
- package/src/ui/Slot.tsx +34 -0
- package/src/ui/TabContext.tsx +115 -0
- package/src/ui/TabList.tsx +47 -0
- package/src/ui/TabRouter.tsx +79 -0
- package/src/ui/TabSlot.tsx +170 -0
- package/src/ui/TabTrigger.tsx +282 -0
- package/src/ui/Tabs.tsx +313 -0
- package/src/ui/common.tsx +277 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/useComponent.tsx +42 -0
- package/types/ui/Slot.d.ts +6 -0
- package/types/ui/Slot.d.ts.map +1 -0
- package/types/ui/TabContext.d.ts +190 -0
- package/types/ui/TabContext.d.ts.map +1 -0
- package/types/ui/TabList.d.ts +25 -0
- package/types/ui/TabList.d.ts.map +1 -0
- package/types/ui/TabRouter.d.ts +103 -0
- package/types/ui/TabRouter.d.ts.map +1 -0
- package/types/ui/TabSlot.d.ts +73 -0
- package/types/ui/TabSlot.d.ts.map +1 -0
- package/types/ui/TabTrigger.d.ts +88 -0
- package/types/ui/TabTrigger.d.ts.map +1 -0
- package/types/ui/Tabs.d.ts +255 -0
- package/types/ui/Tabs.d.ts.map +1 -0
- package/types/ui/common.d.ts +40 -0
- package/types/ui/common.d.ts.map +1 -0
- package/types/ui/index.d.ts +2 -0
- package/types/ui/index.d.ts.map +1 -0
- package/types/ui/useComponent.d.ts +10 -0
- package/types/ui/useComponent.d.ts.map +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
2
|
+
import type { TabNavigationState } from '@react-navigation/native'
|
|
3
|
+
import { type ReactNode, use, type ReactElement, type ComponentProps, useCallback } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
type View,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Pressable,
|
|
8
|
+
type PressableProps,
|
|
9
|
+
type GestureResponderEvent,
|
|
10
|
+
} from 'react-native'
|
|
11
|
+
|
|
12
|
+
import { TabTriggerMapContext } from './TabContext'
|
|
13
|
+
import type { TriggerMap } from './common'
|
|
14
|
+
import { appendBaseUrl } from '../fork/getPathFromState-mods'
|
|
15
|
+
import { router } from '../router/imperative-api'
|
|
16
|
+
import { stripGroupSegmentsFromPath } from '../router/matchers'
|
|
17
|
+
import type { OneRouter } from '../interfaces/router'
|
|
18
|
+
import { useNavigatorContext } from '../views/Navigator'
|
|
19
|
+
|
|
20
|
+
type PressablePropsWithoutFunctionChildren = Omit<PressableProps, 'children'> & {
|
|
21
|
+
children?: ReactNode | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TabTriggerProps = PressablePropsWithoutFunctionChildren & {
|
|
25
|
+
/**
|
|
26
|
+
* Name of tab. When used within a `TabList` this sets the name of the tab.
|
|
27
|
+
* Otherwise, this references the name.
|
|
28
|
+
*/
|
|
29
|
+
name: string
|
|
30
|
+
/**
|
|
31
|
+
* Name of tab. Required when used within a `TabList`.
|
|
32
|
+
*/
|
|
33
|
+
href?: OneRouter.Href
|
|
34
|
+
/**
|
|
35
|
+
* Forward props to child component. Useful for custom wrappers.
|
|
36
|
+
*/
|
|
37
|
+
asChild?: boolean
|
|
38
|
+
/**
|
|
39
|
+
* Resets the route when switching to a tab.
|
|
40
|
+
*/
|
|
41
|
+
resetOnFocus?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TabTriggerOptions = {
|
|
45
|
+
name: string
|
|
46
|
+
href: OneRouter.Href
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type TabTriggerSlotProps = PressablePropsWithoutFunctionChildren &
|
|
50
|
+
React.RefAttributes<View> & {
|
|
51
|
+
isFocused?: boolean
|
|
52
|
+
href?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const TabTriggerSlot = Slot as React.ForwardRefExoticComponent<TabTriggerSlotProps>
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Helper function to determine if a mouse event should be handled
|
|
59
|
+
*/
|
|
60
|
+
function shouldHandleMouseEvent(
|
|
61
|
+
e?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
|
|
62
|
+
): boolean {
|
|
63
|
+
if (!e) return true
|
|
64
|
+
|
|
65
|
+
// Check if it's a MouseEvent
|
|
66
|
+
if ('button' in e) {
|
|
67
|
+
// Only handle left clicks without modifier keys
|
|
68
|
+
return (
|
|
69
|
+
!e.metaKey &&
|
|
70
|
+
!e.altKey &&
|
|
71
|
+
!e.ctrlKey &&
|
|
72
|
+
!e.shiftKey &&
|
|
73
|
+
(e.button == null || e.button === 0) &&
|
|
74
|
+
[undefined, null, '', 'self'].includes((e.currentTarget as any).target)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a trigger to navigate to a tab. When used as child of `TabList`, its
|
|
83
|
+
* functionality slightly changes since the `href` prop is required,
|
|
84
|
+
* and the trigger also defines what routes are present in the `Tabs`.
|
|
85
|
+
*
|
|
86
|
+
* When used outside of `TabList`, this component no longer requires an `href`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* <Tabs>
|
|
91
|
+
* <TabSlot />
|
|
92
|
+
* <TabList>
|
|
93
|
+
* <TabTrigger name="home" href="/" />
|
|
94
|
+
* </TabList>
|
|
95
|
+
* </Tabs>
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function TabTrigger({ asChild, name, href, resetOnFocus, ...props }: TabTriggerProps) {
|
|
99
|
+
const { trigger, triggerProps } = useTabTrigger({
|
|
100
|
+
name,
|
|
101
|
+
resetOnFocus,
|
|
102
|
+
...props,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Pressable doesn't accept the extra props, so only pass them if we are using asChild
|
|
106
|
+
if (asChild) {
|
|
107
|
+
return (
|
|
108
|
+
<TabTriggerSlot
|
|
109
|
+
style={styles.tabTrigger}
|
|
110
|
+
{...props}
|
|
111
|
+
{...triggerProps}
|
|
112
|
+
href={trigger?.resolvedHref}
|
|
113
|
+
>
|
|
114
|
+
{props.children}
|
|
115
|
+
</TabTriggerSlot>
|
|
116
|
+
)
|
|
117
|
+
} else {
|
|
118
|
+
// These props are not typed, but are allowed by React Native Web
|
|
119
|
+
const reactNativeWebProps = { href: trigger?.resolvedHref }
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Pressable style={styles.tabTrigger} {...reactNativeWebProps} {...props} {...triggerProps}>
|
|
123
|
+
{props.children}
|
|
124
|
+
</Pressable>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @hidden
|
|
131
|
+
*/
|
|
132
|
+
export function isTabTrigger(
|
|
133
|
+
child: ReactElement<any>
|
|
134
|
+
): child is ReactElement<ComponentProps<typeof TabTrigger>> {
|
|
135
|
+
return child.type === TabTrigger
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Options for `switchTab` function.
|
|
140
|
+
*/
|
|
141
|
+
export type SwitchToOptions = {
|
|
142
|
+
/**
|
|
143
|
+
* Navigate and reset the history on route focus.
|
|
144
|
+
*/
|
|
145
|
+
resetOnFocus?: boolean
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type Trigger = TriggerMap[string] & {
|
|
149
|
+
isFocused: boolean
|
|
150
|
+
resolvedHref: string
|
|
151
|
+
route: TabNavigationState<any>['routes'][number]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type UseTabTriggerResult = {
|
|
155
|
+
switchTab: (name: string, options: SwitchToOptions) => void
|
|
156
|
+
getTrigger: (name: string) => Trigger | undefined
|
|
157
|
+
trigger?: Trigger
|
|
158
|
+
triggerProps: TriggerProps
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type TriggerProps = {
|
|
162
|
+
isFocused: boolean
|
|
163
|
+
onPress: PressableProps['onPress']
|
|
164
|
+
onLongPress: PressableProps['onLongPress']
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Utility hook creating custom `TabTrigger`.
|
|
169
|
+
*/
|
|
170
|
+
export function useTabTrigger(options: TabTriggerProps): UseTabTriggerResult {
|
|
171
|
+
const { state, navigation } = useNavigatorContext()
|
|
172
|
+
const { name, resetOnFocus, onPress, onLongPress } = options
|
|
173
|
+
const triggerMap = use(TabTriggerMapContext)
|
|
174
|
+
|
|
175
|
+
const getTrigger = useCallback(
|
|
176
|
+
(name: string) => {
|
|
177
|
+
const config = triggerMap[name]
|
|
178
|
+
|
|
179
|
+
if (!config) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
isFocused: state.index === config.index,
|
|
185
|
+
route: state.routes[config.index],
|
|
186
|
+
resolvedHref: stripGroupSegmentsFromPath(appendBaseUrl(config.href)),
|
|
187
|
+
...config,
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
[triggerMap, state]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const trigger = name !== undefined ? getTrigger(name) : undefined
|
|
194
|
+
|
|
195
|
+
const switchTab = useCallback(
|
|
196
|
+
(name: string, options?: SwitchToOptions) => {
|
|
197
|
+
const config = triggerMap[name]
|
|
198
|
+
|
|
199
|
+
if (config) {
|
|
200
|
+
if (config.type === 'external') {
|
|
201
|
+
return router.navigate(config.href)
|
|
202
|
+
} else {
|
|
203
|
+
return navigation?.dispatch({
|
|
204
|
+
...config.action,
|
|
205
|
+
type: 'JUMP_TO',
|
|
206
|
+
payload: {
|
|
207
|
+
...config.action.payload,
|
|
208
|
+
...options,
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
return navigation?.dispatch({
|
|
214
|
+
type: 'JUMP_TO',
|
|
215
|
+
payload: {
|
|
216
|
+
name,
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
[navigation, triggerMap]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const handleOnPress = useCallback<NonNullable<PressableProps['onPress']>>(
|
|
225
|
+
(event) => {
|
|
226
|
+
onPress?.(event)
|
|
227
|
+
if (!trigger) return
|
|
228
|
+
if (event?.isDefaultPrevented()) return
|
|
229
|
+
|
|
230
|
+
navigation?.emit({
|
|
231
|
+
type: 'tabPress',
|
|
232
|
+
target: trigger.type === 'internal' ? trigger.route.key : trigger?.href,
|
|
233
|
+
canPreventDefault: true,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
if (!shouldHandleMouseEvent(event)) return
|
|
237
|
+
|
|
238
|
+
switchTab(name, { resetOnFocus })
|
|
239
|
+
},
|
|
240
|
+
[onPress, name, resetOnFocus, trigger, navigation, switchTab]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const handleOnLongPress = useCallback<NonNullable<PressableProps['onLongPress']>>(
|
|
244
|
+
(event) => {
|
|
245
|
+
onLongPress?.(event)
|
|
246
|
+
if (!trigger) return
|
|
247
|
+
if (event?.isDefaultPrevented()) return
|
|
248
|
+
|
|
249
|
+
navigation?.emit({
|
|
250
|
+
type: 'tabLongPress',
|
|
251
|
+
target: trigger.type === 'internal' ? trigger.route.key : trigger?.href,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
if (!shouldHandleMouseEvent(event)) return
|
|
255
|
+
|
|
256
|
+
switchTab(name, {
|
|
257
|
+
resetOnFocus,
|
|
258
|
+
})
|
|
259
|
+
},
|
|
260
|
+
[onLongPress, name, resetOnFocus, trigger, navigation, switchTab]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const triggerProps = {
|
|
264
|
+
isFocused: Boolean(trigger?.isFocused),
|
|
265
|
+
onPress: handleOnPress,
|
|
266
|
+
onLongPress: handleOnLongPress,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
switchTab,
|
|
271
|
+
getTrigger,
|
|
272
|
+
trigger,
|
|
273
|
+
triggerProps,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const styles = StyleSheet.create({
|
|
278
|
+
tabTrigger: {
|
|
279
|
+
flexDirection: 'row',
|
|
280
|
+
justifyContent: 'space-between',
|
|
281
|
+
},
|
|
282
|
+
})
|
package/src/ui/Tabs.tsx
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DefaultNavigatorOptions,
|
|
3
|
+
LinkingContext,
|
|
4
|
+
type ParamListBase,
|
|
5
|
+
type TabActionHelpers,
|
|
6
|
+
type TabNavigationState,
|
|
7
|
+
type TabRouterOptions,
|
|
8
|
+
useNavigationBuilder,
|
|
9
|
+
} from '@react-navigation/native'
|
|
10
|
+
import {
|
|
11
|
+
Children,
|
|
12
|
+
type ComponentProps,
|
|
13
|
+
Fragment,
|
|
14
|
+
type ReactElement,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
isValidElement,
|
|
17
|
+
use,
|
|
18
|
+
useMemo,
|
|
19
|
+
type PropsWithChildren,
|
|
20
|
+
} from 'react'
|
|
21
|
+
import { StyleSheet, type ViewProps, View } from 'react-native'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
type ExpoTabsScreenOptions,
|
|
25
|
+
type TabNavigationEventMap,
|
|
26
|
+
TabTriggerMapContext,
|
|
27
|
+
type TabsContextValue,
|
|
28
|
+
} from './TabContext'
|
|
29
|
+
import { isTabList } from './TabList'
|
|
30
|
+
import { ExpoTabRouter, type ExpoTabRouterOptions } from './TabRouter'
|
|
31
|
+
import { isTabSlot } from './TabSlot'
|
|
32
|
+
import { isTabTrigger } from './TabTrigger'
|
|
33
|
+
import { ViewSlot, type ScreenTrigger, triggersToScreens } from './common'
|
|
34
|
+
import { useComponent } from './useComponent'
|
|
35
|
+
import { useRouteNode, useContextKey } from '../router/Route'
|
|
36
|
+
import { useRouteInfo } from '../hooks'
|
|
37
|
+
import { resolveHref } from '../link/href'
|
|
38
|
+
import { shouldLinkExternally } from '../utils/url'
|
|
39
|
+
import { NavigatorContext } from '../views/Navigator'
|
|
40
|
+
import type { RouterFactory } from '@react-navigation/native'
|
|
41
|
+
|
|
42
|
+
type NavigatorContextValue = {
|
|
43
|
+
contextKey: string
|
|
44
|
+
state: ReturnType<typeof useNavigationBuilder>['state']
|
|
45
|
+
navigation: ReturnType<typeof useNavigationBuilder>['navigation']
|
|
46
|
+
descriptors: ReturnType<typeof useNavigationBuilder>['descriptors']
|
|
47
|
+
router: RouterFactory<any, any, any>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export * from './TabContext'
|
|
51
|
+
export * from './TabList'
|
|
52
|
+
export * from './TabSlot'
|
|
53
|
+
export * from './TabTrigger'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Options to provide to the Tab Router.
|
|
57
|
+
*/
|
|
58
|
+
export type UseTabsOptions = Omit<
|
|
59
|
+
DefaultNavigatorOptions<
|
|
60
|
+
ParamListBase,
|
|
61
|
+
any,
|
|
62
|
+
TabNavigationState<any>,
|
|
63
|
+
ExpoTabsScreenOptions,
|
|
64
|
+
TabNavigationEventMap,
|
|
65
|
+
any
|
|
66
|
+
>,
|
|
67
|
+
'children'
|
|
68
|
+
> & {
|
|
69
|
+
backBehavior?: TabRouterOptions['backBehavior']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type TabsProps = ViewProps & {
|
|
73
|
+
/** Forward props to child component and removes the extra `<View>`. Useful for custom wrappers. */
|
|
74
|
+
asChild?: boolean
|
|
75
|
+
options?: UseTabsOptions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Root component for the headless tabs.
|
|
80
|
+
*
|
|
81
|
+
* @see [`useTabsWithChildren`](#usetabswithchildrenoptions) for a hook version of this component.
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* <Tabs>
|
|
85
|
+
* <TabSlot />
|
|
86
|
+
* <TabList>
|
|
87
|
+
* <TabTrigger name="home" href="/" />
|
|
88
|
+
* </TabList>
|
|
89
|
+
* </Tabs>
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function Tabs(props: TabsProps) {
|
|
93
|
+
const { children, asChild, options, ...rest } = props
|
|
94
|
+
const Comp = asChild ? ViewSlot : View
|
|
95
|
+
|
|
96
|
+
const { NavigationContent } = useTabsWithChildren({
|
|
97
|
+
// asChild adds an extra layer, so we need to process the child's children
|
|
98
|
+
children:
|
|
99
|
+
asChild &&
|
|
100
|
+
isValidElement(children) &&
|
|
101
|
+
children.props &&
|
|
102
|
+
typeof children.props === 'object' &&
|
|
103
|
+
'children' in children.props
|
|
104
|
+
? (children.props.children as ReactNode)
|
|
105
|
+
: children,
|
|
106
|
+
...options,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Comp style={styles.tabsRoot} {...rest}>
|
|
111
|
+
<NavigationContent>{children}</NavigationContent>
|
|
112
|
+
</Comp>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// @docsMissing
|
|
117
|
+
export type UseTabsWithChildrenOptions = PropsWithChildren<UseTabsOptions>
|
|
118
|
+
|
|
119
|
+
// @docsMissing
|
|
120
|
+
export type UseTabsWithTriggersOptions = UseTabsOptions & {
|
|
121
|
+
triggers: ScreenTrigger[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Hook version of `Tabs`. The returned NavigationContent component
|
|
126
|
+
* should be rendered. Using the hook requires using the `<TabList />`
|
|
127
|
+
* and `<TabTrigger />` components exported from One.
|
|
128
|
+
*
|
|
129
|
+
* The `useTabsWithTriggers()` hook can be used for custom components.
|
|
130
|
+
*
|
|
131
|
+
* @see [`Tabs`](#tabs) for the component version of this hook.
|
|
132
|
+
* @example
|
|
133
|
+
* ```tsx
|
|
134
|
+
* export function MyTabs({ children }) {
|
|
135
|
+
* const { NavigationContent } = useTabsWithChildren({ children })
|
|
136
|
+
*
|
|
137
|
+
* return <NavigationContent />
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function useTabsWithChildren(options: UseTabsWithChildrenOptions) {
|
|
142
|
+
const { children, ...rest } = options
|
|
143
|
+
return useTabsWithTriggers({ triggers: parseTriggersFromChildren(children), ...rest })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Alternative hook version of `Tabs` that uses explicit triggers
|
|
148
|
+
* instead of `children`.
|
|
149
|
+
*
|
|
150
|
+
* @see [`Tabs`](#tabs) for the component version of this hook.
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* export function MyTabs({ children }) {
|
|
154
|
+
* const { NavigationContent } = useTabsWithChildren({ triggers: [] })
|
|
155
|
+
*
|
|
156
|
+
* return <NavigationContent />
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function useTabsWithTriggers(options: UseTabsWithTriggersOptions): TabsContextValue {
|
|
161
|
+
const { triggers, ...rest } = options
|
|
162
|
+
// Ensure we extend the parent triggers, so we can trigger them as well
|
|
163
|
+
const parentTriggerMap = use(TabTriggerMapContext)
|
|
164
|
+
const routeNode = useRouteNode()
|
|
165
|
+
const contextKey = useContextKey()
|
|
166
|
+
const linking = use(LinkingContext).options
|
|
167
|
+
const routeInfo = useRouteInfo()
|
|
168
|
+
|
|
169
|
+
if (!routeNode || !linking) {
|
|
170
|
+
throw new Error('No RouteNode. This is likely a bug in one router.')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const initialRouteName = routeNode.initialRouteName
|
|
174
|
+
|
|
175
|
+
const { children, triggerMap } = triggersToScreens(
|
|
176
|
+
triggers,
|
|
177
|
+
routeNode,
|
|
178
|
+
linking,
|
|
179
|
+
initialRouteName,
|
|
180
|
+
parentTriggerMap,
|
|
181
|
+
routeInfo,
|
|
182
|
+
contextKey
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const navigatorContext = useNavigationBuilder<
|
|
186
|
+
TabNavigationState<any>,
|
|
187
|
+
ExpoTabRouterOptions,
|
|
188
|
+
TabActionHelpers<ParamListBase>,
|
|
189
|
+
ExpoTabsScreenOptions,
|
|
190
|
+
TabNavigationEventMap
|
|
191
|
+
>(ExpoTabRouter, {
|
|
192
|
+
children,
|
|
193
|
+
...rest,
|
|
194
|
+
triggerMap,
|
|
195
|
+
id: contextKey,
|
|
196
|
+
initialRouteName,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const {
|
|
200
|
+
state,
|
|
201
|
+
descriptors,
|
|
202
|
+
navigation,
|
|
203
|
+
describe,
|
|
204
|
+
NavigationContent: RNNavigationContent,
|
|
205
|
+
} = navigatorContext
|
|
206
|
+
|
|
207
|
+
const navigatorContextValue = useMemo<NavigatorContextValue>(
|
|
208
|
+
() => ({
|
|
209
|
+
...(navigatorContext as unknown as ReturnType<typeof useNavigationBuilder>),
|
|
210
|
+
contextKey,
|
|
211
|
+
router: ExpoTabRouter,
|
|
212
|
+
}),
|
|
213
|
+
[navigatorContext, contextKey, ExpoTabRouter]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const NavigationContent = useComponent((children: React.ReactNode) => (
|
|
217
|
+
<TabTriggerMapContext.Provider value={triggerMap}>
|
|
218
|
+
<NavigatorContext.Provider value={navigatorContextValue}>
|
|
219
|
+
<RNNavigationContent>{children}</RNNavigationContent>
|
|
220
|
+
</NavigatorContext.Provider>
|
|
221
|
+
</TabTriggerMapContext.Provider>
|
|
222
|
+
)) as TabsContextValue['NavigationContent']
|
|
223
|
+
|
|
224
|
+
return { state, descriptors, navigation, NavigationContent, describe }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseTriggersFromChildren(
|
|
228
|
+
children: ReactNode,
|
|
229
|
+
screenTriggers: ScreenTrigger[] = [],
|
|
230
|
+
isInTabList = false
|
|
231
|
+
) {
|
|
232
|
+
Children.forEach(children, (child) => {
|
|
233
|
+
if (!child || !isValidElement(child) || isTabSlot(child)) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (isFragment(child) && typeof child.props.children !== 'function') {
|
|
238
|
+
return parseTriggersFromChildren(
|
|
239
|
+
child.props.children,
|
|
240
|
+
screenTriggers,
|
|
241
|
+
isInTabList || isTabList(child)
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isTabList(child) && typeof child.props.children !== 'function') {
|
|
246
|
+
let children = child.props.children
|
|
247
|
+
|
|
248
|
+
// <TabList asChild /> adds an extra layer. We need to parse the child's children
|
|
249
|
+
if (
|
|
250
|
+
child.props.asChild &&
|
|
251
|
+
isValidElement(children) &&
|
|
252
|
+
children.props &&
|
|
253
|
+
typeof children.props === 'object' &&
|
|
254
|
+
'children' in children.props
|
|
255
|
+
) {
|
|
256
|
+
children = children.props.children as ReactNode
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return parseTriggersFromChildren(children, screenTriggers, isInTabList || isTabList(child))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// We should only process TabTriggers within the TabList. All other components will be ignored
|
|
263
|
+
if (!isInTabList || !isTabTrigger(child)) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { href, name } = child.props
|
|
268
|
+
|
|
269
|
+
if (!href) {
|
|
270
|
+
if (process.env.NODE_ENV === 'development') {
|
|
271
|
+
console.warn(
|
|
272
|
+
`<TabTrigger name={${name}}> does not have a 'href' prop. TabTriggers within a <TabList /> are required to have an href.`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const resolvedHref = resolveHref(href)
|
|
279
|
+
|
|
280
|
+
if (shouldLinkExternally(resolvedHref)) {
|
|
281
|
+
return screenTriggers.push({
|
|
282
|
+
type: 'external',
|
|
283
|
+
name,
|
|
284
|
+
href: resolvedHref,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!name) {
|
|
289
|
+
if (process.env.NODE_ENV === 'development') {
|
|
290
|
+
console.warn(
|
|
291
|
+
`<TabTrigger> does not have a 'name' prop. TabTriggers within a <TabList /> are required to have a name.`
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return screenTriggers.push({ type: 'internal', href: resolvedHref, name })
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
return screenTriggers
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isFragment(
|
|
304
|
+
child: ReactElement<any>
|
|
305
|
+
): child is ReactElement<ComponentProps<typeof Fragment>> {
|
|
306
|
+
return child.type === Fragment
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const styles = StyleSheet.create({
|
|
310
|
+
tabsRoot: {
|
|
311
|
+
flex: 1,
|
|
312
|
+
},
|
|
313
|
+
})
|