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
package/src/ui/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# One Headless UI Components
|
|
2
|
+
|
|
3
|
+
Headless UI components for the One framework.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The headless tabs system provides complete control over tab navigation UI while maintaining full integration with One's file-based routing system. Unlike traditional tabs that use `@react-navigation/bottom-tabs` with opinionated styling, these components are completely unstyled.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
The UI components are available through the `one/ui` submodule:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Tabs, TabList, TabTrigger, TabSlot } from 'one/ui'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Core Components
|
|
18
|
+
|
|
19
|
+
| Component | Description |
|
|
20
|
+
|-----------|-------------|
|
|
21
|
+
| `Tabs` | Root container that wraps the entire tab structure and manages navigation state |
|
|
22
|
+
| `TabList` | Container for `TabTrigger` components, typically the tab bar |
|
|
23
|
+
| `TabTrigger` | Pressable element that switches between tabs |
|
|
24
|
+
| `TabSlot` | Renders the currently active tab's content |
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Tabs, TabList, TabTrigger, TabSlot } from 'one/ui'
|
|
30
|
+
|
|
31
|
+
export default function Layout() {
|
|
32
|
+
return (
|
|
33
|
+
<Tabs style={styles.root}>
|
|
34
|
+
<TabSlot />
|
|
35
|
+
<TabList style={styles.tabBar}>
|
|
36
|
+
<TabTrigger
|
|
37
|
+
name="home"
|
|
38
|
+
href="/"
|
|
39
|
+
asChild
|
|
40
|
+
resetOnFocus
|
|
41
|
+
>
|
|
42
|
+
<CustomButton icon="home">Home</CustomButton>
|
|
43
|
+
</TabTrigger>
|
|
44
|
+
<TabTrigger name="profile" href="/profile" asChild>
|
|
45
|
+
<CustomButton icon="user">Profile</CustomButton>
|
|
46
|
+
</TabTrigger>
|
|
47
|
+
</TabList>
|
|
48
|
+
</Tabs>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Advanced Usage with Hooks
|
|
54
|
+
|
|
55
|
+
### useTabsWithChildren()
|
|
56
|
+
|
|
57
|
+
Hook version of `<Tabs>` that allows custom wrapper components:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
export function MyTabs({ children }) {
|
|
61
|
+
const { NavigationContent } = useTabsWithChildren({ children })
|
|
62
|
+
return <NavigationContent />
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### useTabsWithTriggers()
|
|
67
|
+
|
|
68
|
+
Explicit trigger array version for advanced custom navigators:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
export function MyTabs({ children }) {
|
|
72
|
+
const { NavigationContent } = useTabsWithTriggers({
|
|
73
|
+
triggers: [
|
|
74
|
+
{ type: 'internal', name: 'home', href: '/' },
|
|
75
|
+
{ type: 'internal', name: 'profile', href: '/profile' },
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
return <NavigationContent />
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### useTabSlot()
|
|
83
|
+
|
|
84
|
+
Returns current tab element for custom slot rendering:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
function MyTabSlot() {
|
|
88
|
+
const slot = useTabSlot()
|
|
89
|
+
return slot
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### useTabTrigger()
|
|
94
|
+
|
|
95
|
+
Custom trigger logic for building custom tab buttons:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
function CustomTabBar() {
|
|
99
|
+
const home = useTabTrigger({ name: 'home' })
|
|
100
|
+
const profile = useTabTrigger({ name: 'profile' })
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View style={styles.customBar}>
|
|
104
|
+
<Pressable {...home.triggerProps}>
|
|
105
|
+
<Text style={home.trigger?.isFocused && styles.active}>
|
|
106
|
+
Home
|
|
107
|
+
</Text>
|
|
108
|
+
</Pressable>
|
|
109
|
+
<Pressable {...profile.triggerProps}>
|
|
110
|
+
<Text style={profile.trigger?.isFocused && styles.active}>
|
|
111
|
+
Profile
|
|
112
|
+
</Text>
|
|
113
|
+
</Pressable>
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Example
|
|
120
|
+
|
|
121
|
+
See `/examples/one-basic/app/tabs/` for a working example with custom-styled tabs.
|
package/src/ui/Slot.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Slot as RUISlot } from '@radix-ui/react-slot'
|
|
2
|
+
import {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useMemo,
|
|
5
|
+
type ForwardRefExoticComponent,
|
|
6
|
+
type Component,
|
|
7
|
+
type RefAttributes,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { StyleSheet, type ViewProps } from 'react-native'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* RadixUI has special logic to handle the merging of `style` and `className` props.
|
|
13
|
+
* On the web styles are not allowed so Radix does not handle this scenario.
|
|
14
|
+
* This could be fixed upstream (PR open), but it may not as RN is not their target
|
|
15
|
+
* platform.
|
|
16
|
+
*
|
|
17
|
+
* This shim calls `StyleSheet.flatten` on the styles before we render the <Slot />
|
|
18
|
+
*
|
|
19
|
+
* @see https://github.com/expo/expo/issues/31352
|
|
20
|
+
* @see https://github.com/radix-ui/primitives/issues/3107
|
|
21
|
+
* @param Component
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
function ShimSlotForReactNative(Component: typeof RUISlot): typeof RUISlot {
|
|
25
|
+
return forwardRef(function RNSlotHOC({ style, ...props }, ref) {
|
|
26
|
+
style = useMemo(() => StyleSheet.flatten(style), [style])
|
|
27
|
+
return <Component ref={ref} {...props} style={style} />
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Slot<Props = ViewProps, Ref = Component<ViewProps>>
|
|
32
|
+
extends ForwardRefExoticComponent<Props & RefAttributes<Ref>> {}
|
|
33
|
+
|
|
34
|
+
export const Slot: Slot = ShimSlotForReactNative(RUISlot) as Slot
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'
|
|
2
|
+
import type {
|
|
3
|
+
DefaultNavigatorOptions,
|
|
4
|
+
NavigationAction,
|
|
5
|
+
NavigationProp,
|
|
6
|
+
ParamListBase,
|
|
7
|
+
TabActionHelpers,
|
|
8
|
+
TabNavigationState,
|
|
9
|
+
TabRouterOptions,
|
|
10
|
+
useNavigationBuilder,
|
|
11
|
+
} from '@react-navigation/native'
|
|
12
|
+
import { createContext } from 'react'
|
|
13
|
+
|
|
14
|
+
import type { TriggerMap } from './common'
|
|
15
|
+
|
|
16
|
+
export type ExpoTabsProps = ExpoTabsNavigatorOptions
|
|
17
|
+
|
|
18
|
+
export type ExpoTabsNavigatorScreenOptions = {
|
|
19
|
+
detachInactiveScreens?: boolean
|
|
20
|
+
unmountOnBlur?: boolean
|
|
21
|
+
freezeOnBlur?: boolean
|
|
22
|
+
lazy?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ExpoTabsNavigatorOptions = DefaultNavigatorOptions<
|
|
26
|
+
ParamListBase,
|
|
27
|
+
string | undefined,
|
|
28
|
+
TabNavigationState<ParamListBase>,
|
|
29
|
+
ExpoTabsScreenOptions,
|
|
30
|
+
TabNavigationEventMap,
|
|
31
|
+
ExpoTabsNavigationProp<ParamListBase>
|
|
32
|
+
> &
|
|
33
|
+
// Should be set through `unstable_settings`
|
|
34
|
+
Omit<TabRouterOptions, 'initialRouteName'> &
|
|
35
|
+
ExpoTabsNavigatorScreenOptions
|
|
36
|
+
|
|
37
|
+
export type ExpoTabsNavigationProp<
|
|
38
|
+
ParamList extends ParamListBase,
|
|
39
|
+
RouteName extends keyof ParamList = keyof ParamList,
|
|
40
|
+
NavigatorID extends string | undefined = undefined,
|
|
41
|
+
> = NavigationProp<
|
|
42
|
+
ParamList,
|
|
43
|
+
RouteName,
|
|
44
|
+
NavigatorID,
|
|
45
|
+
TabNavigationState<ParamListBase>,
|
|
46
|
+
ExpoTabsScreenOptions,
|
|
47
|
+
TabNavigationEventMap
|
|
48
|
+
>
|
|
49
|
+
|
|
50
|
+
export type ExpoTabsScreenOptions = Pick<
|
|
51
|
+
BottomTabNavigationOptions,
|
|
52
|
+
'title' | 'lazy' | 'freezeOnBlur'
|
|
53
|
+
> & {
|
|
54
|
+
params?: object
|
|
55
|
+
title: string
|
|
56
|
+
action: NavigationAction
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type TabNavigationEventMap = {
|
|
60
|
+
/**
|
|
61
|
+
* Event which fires on tapping on the tab in the tab bar.
|
|
62
|
+
*/
|
|
63
|
+
tabPress: { data: undefined; canPreventDefault: true }
|
|
64
|
+
/**
|
|
65
|
+
* Event which fires on long press on the tab in the tab bar.
|
|
66
|
+
*/
|
|
67
|
+
tabLongPress: { data: undefined }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The React Navigation custom navigator.
|
|
72
|
+
*
|
|
73
|
+
* @see [`useNavigationBuilder`](https://reactnavigation.org/docs/custom-navigators/#usenavigationbuilder) hook from React Navigation for more information.
|
|
74
|
+
*/
|
|
75
|
+
export type TabsContextValue = ReturnType<
|
|
76
|
+
typeof useNavigationBuilder<
|
|
77
|
+
TabNavigationState<any>,
|
|
78
|
+
TabRouterOptions,
|
|
79
|
+
TabActionHelpers<ParamListBase>,
|
|
80
|
+
ExpoTabsNavigatorScreenOptions,
|
|
81
|
+
TabNavigationEventMap
|
|
82
|
+
>
|
|
83
|
+
>
|
|
84
|
+
|
|
85
|
+
export type TabContextValue = TabsDescriptor['options']
|
|
86
|
+
|
|
87
|
+
export const TabContext = createContext<TabContextValue>({})
|
|
88
|
+
/**
|
|
89
|
+
* @hidden
|
|
90
|
+
*/
|
|
91
|
+
export const TabTriggerMapContext = createContext<TriggerMap>({})
|
|
92
|
+
/**
|
|
93
|
+
* @hidden
|
|
94
|
+
*/
|
|
95
|
+
export const TabsDescriptorsContext = createContext<TabsContextValue['descriptors']>({})
|
|
96
|
+
/**
|
|
97
|
+
* @hidden
|
|
98
|
+
*/
|
|
99
|
+
export const TabsNavigatorContext = createContext<TabsContextValue['navigation'] | null>(null)
|
|
100
|
+
/**
|
|
101
|
+
* @hidden
|
|
102
|
+
*/
|
|
103
|
+
export const TabsStateContext = createContext<TabsContextValue['state']>({
|
|
104
|
+
type: 'tab',
|
|
105
|
+
preloadedRouteKeys: [],
|
|
106
|
+
history: [],
|
|
107
|
+
index: -1,
|
|
108
|
+
key: '',
|
|
109
|
+
stale: false,
|
|
110
|
+
routeNames: [],
|
|
111
|
+
routes: [],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export type Route = TabNavigationState<ParamListBase>['routes'][number]
|
|
115
|
+
export type TabsDescriptor = TabsContextValue['descriptors'][number]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactElement, ComponentProps } from 'react'
|
|
2
|
+
import { View, StyleSheet, type ViewProps } from 'react-native'
|
|
3
|
+
|
|
4
|
+
import { ViewSlot } from './common'
|
|
5
|
+
|
|
6
|
+
export type TabListProps = ViewProps & {
|
|
7
|
+
/** Forward props to child component and removes the extra `<View>`. Useful for custom wrappers. */
|
|
8
|
+
asChild?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrapper component for `TabTriggers`. `TabTriggers` within the `TabList` define the tabs.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Tabs>
|
|
17
|
+
* <TabSlot />
|
|
18
|
+
* <TabList>
|
|
19
|
+
* <TabTrigger name="home" href="/" />
|
|
20
|
+
* </TabList>
|
|
21
|
+
* </Tabs>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function TabList({ asChild, style, ...props }: TabListProps) {
|
|
25
|
+
const Comp = asChild ? ViewSlot : View
|
|
26
|
+
return <Comp style={[styles.tabList, style]} {...props} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @hidden
|
|
31
|
+
*/
|
|
32
|
+
export function isTabList(
|
|
33
|
+
child: ReactElement<any>
|
|
34
|
+
): child is ReactElement<ComponentProps<typeof TabList>> {
|
|
35
|
+
return child.type === TabList
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const styles = StyleSheet.create({
|
|
39
|
+
tabList: {
|
|
40
|
+
flexDirection: 'row',
|
|
41
|
+
justifyContent: 'space-between',
|
|
42
|
+
},
|
|
43
|
+
tabTrigger: {
|
|
44
|
+
flexDirection: 'row',
|
|
45
|
+
justifyContent: 'space-between',
|
|
46
|
+
},
|
|
47
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CommonNavigationAction,
|
|
3
|
+
type ParamListBase,
|
|
4
|
+
TabRouter as RNTabRouter,
|
|
5
|
+
type Router,
|
|
6
|
+
type TabActionType as RNTabActionType,
|
|
7
|
+
type TabNavigationState,
|
|
8
|
+
type TabRouterOptions as RNTabRouterOptions,
|
|
9
|
+
} from '@react-navigation/native'
|
|
10
|
+
|
|
11
|
+
import type { TriggerMap } from './common'
|
|
12
|
+
|
|
13
|
+
export type ExpoTabRouterOptions = RNTabRouterOptions & {
|
|
14
|
+
triggerMap: TriggerMap
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ExpoTabActionType =
|
|
18
|
+
| RNTabActionType
|
|
19
|
+
| CommonNavigationAction
|
|
20
|
+
| {
|
|
21
|
+
type: 'JUMP_TO'
|
|
22
|
+
source?: string
|
|
23
|
+
target?: string
|
|
24
|
+
payload: {
|
|
25
|
+
name: string
|
|
26
|
+
resetOnFocus?: boolean
|
|
27
|
+
params?: object
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ExpoTabRouter(options: ExpoTabRouterOptions) {
|
|
32
|
+
const rnTabRouter = RNTabRouter(options)
|
|
33
|
+
|
|
34
|
+
const router: Router<
|
|
35
|
+
TabNavigationState<ParamListBase>,
|
|
36
|
+
ExpoTabActionType | CommonNavigationAction
|
|
37
|
+
> = {
|
|
38
|
+
...rnTabRouter,
|
|
39
|
+
getStateForAction(state, action, options) {
|
|
40
|
+
if (action.type !== 'JUMP_TO') {
|
|
41
|
+
return rnTabRouter.getStateForAction(state, action, options)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const route = state.routes.find((route) => route.name === action.payload.name)
|
|
45
|
+
|
|
46
|
+
if (!route) {
|
|
47
|
+
// This shouldn't occur, but lets just hand it off to the next navigator in case.
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// We should reset if this is the first time visiting the route
|
|
52
|
+
let shouldReset = !state.history.some((item) => item.key === route?.key) && !route.state
|
|
53
|
+
|
|
54
|
+
if (!shouldReset && 'resetOnFocus' in action.payload && action.payload.resetOnFocus) {
|
|
55
|
+
shouldReset = state.routes[state.index].key !== route.key
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (shouldReset) {
|
|
59
|
+
options.routeParamList[route.name] = {
|
|
60
|
+
...options.routeParamList[route.name],
|
|
61
|
+
}
|
|
62
|
+
state = {
|
|
63
|
+
...state,
|
|
64
|
+
routes: state.routes.map((r) => {
|
|
65
|
+
if (r.key !== route.key) {
|
|
66
|
+
return r
|
|
67
|
+
}
|
|
68
|
+
return { ...r, state: undefined }
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
return rnTabRouter.getStateForAction(state, action, options)
|
|
72
|
+
} else {
|
|
73
|
+
return rnTabRouter.getStateForRouteFocus(state, route.key)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return router
|
|
79
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactElement, useState } from 'react'
|
|
2
|
+
import { Platform, StyleSheet } from 'react-native'
|
|
3
|
+
import { ScreenContainer, Screen } from 'react-native-screens'
|
|
4
|
+
|
|
5
|
+
import { TabContext, type TabsDescriptor } from './TabContext'
|
|
6
|
+
import type { TabListProps } from './TabList'
|
|
7
|
+
import { useNavigatorContext } from '../views/Navigator'
|
|
8
|
+
|
|
9
|
+
export type TabSlotProps = ComponentProps<typeof ScreenContainer> & {
|
|
10
|
+
/**
|
|
11
|
+
* Remove inactive screens.
|
|
12
|
+
*/
|
|
13
|
+
detachInactiveScreens?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Override how the `Screen` component is rendered.
|
|
16
|
+
*/
|
|
17
|
+
renderFn?: typeof defaultTabsSlotRender
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options provided to the `UseTabSlotOptions`.
|
|
22
|
+
*/
|
|
23
|
+
export type TabsSlotRenderOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Index of screen.
|
|
26
|
+
*/
|
|
27
|
+
index: number
|
|
28
|
+
/**
|
|
29
|
+
* Whether the screen is focused.
|
|
30
|
+
*/
|
|
31
|
+
isFocused: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Whether the screen has been loaded.
|
|
34
|
+
*/
|
|
35
|
+
loaded: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Should the screen be unloaded when inactive.
|
|
38
|
+
*/
|
|
39
|
+
detachInactiveScreens: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns a `ReactElement` of the current tab.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* function MyTabSlot() {
|
|
48
|
+
* const slot = useTabSlot();
|
|
49
|
+
*
|
|
50
|
+
* return slot;
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function useTabSlot({
|
|
55
|
+
detachInactiveScreens = ['android', 'ios', 'web'].includes(Platform.OS),
|
|
56
|
+
style,
|
|
57
|
+
renderFn = defaultTabsSlotRender,
|
|
58
|
+
}: TabSlotProps = {}) {
|
|
59
|
+
const { state, descriptors } = useNavigatorContext()
|
|
60
|
+
const focusedRouteKey = state.routes[state.index].key
|
|
61
|
+
const [loaded, setLoaded] = useState({ [focusedRouteKey]: true })
|
|
62
|
+
|
|
63
|
+
if (!loaded[focusedRouteKey]) {
|
|
64
|
+
setLoaded({ ...loaded, [focusedRouteKey]: true })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ScreenContainer
|
|
69
|
+
enabled={detachInactiveScreens}
|
|
70
|
+
hasTwoStates
|
|
71
|
+
style={[styles.screenContainer, style]}
|
|
72
|
+
>
|
|
73
|
+
{state.routes.map((route, index) => {
|
|
74
|
+
const descriptor = descriptors[route.key] as unknown as TabsDescriptor
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<TabContext.Provider key={descriptor.route.key} value={descriptor.options}>
|
|
78
|
+
{renderFn(descriptor, {
|
|
79
|
+
index,
|
|
80
|
+
isFocused: state.index === index,
|
|
81
|
+
loaded: loaded[route.key],
|
|
82
|
+
detachInactiveScreens,
|
|
83
|
+
})}
|
|
84
|
+
</TabContext.Provider>
|
|
85
|
+
)
|
|
86
|
+
})}
|
|
87
|
+
</ScreenContainer>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Renders the current tab.
|
|
93
|
+
*
|
|
94
|
+
* @see [`useTabSlot`](#usetabslot) for a hook version of this component.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```tsx
|
|
98
|
+
* <Tabs>
|
|
99
|
+
* <TabSlot />
|
|
100
|
+
* <TabList>
|
|
101
|
+
* <TabTrigger name="home" href="/" />
|
|
102
|
+
* </TabList>
|
|
103
|
+
* </Tabs>
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function TabSlot(props: TabSlotProps) {
|
|
107
|
+
return useTabSlot(props)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @hidden
|
|
112
|
+
*/
|
|
113
|
+
export function defaultTabsSlotRender(
|
|
114
|
+
descriptor: TabsDescriptor,
|
|
115
|
+
{ isFocused, loaded, detachInactiveScreens }: TabsSlotRenderOptions
|
|
116
|
+
) {
|
|
117
|
+
const { lazy = true, unmountOnBlur, freezeOnBlur } = descriptor.options
|
|
118
|
+
|
|
119
|
+
if (unmountOnBlur && !isFocused) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (lazy && !loaded && !isFocused) {
|
|
124
|
+
// Don't render a lazy screen if we've never navigated to it
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Screen
|
|
130
|
+
key={descriptor.route.key}
|
|
131
|
+
enabled={detachInactiveScreens}
|
|
132
|
+
activityState={isFocused ? 2 : 0}
|
|
133
|
+
freezeOnBlur={freezeOnBlur}
|
|
134
|
+
style={[styles.screen, isFocused ? styles.focused : styles.unfocused]}
|
|
135
|
+
>
|
|
136
|
+
{descriptor.render()}
|
|
137
|
+
</Screen>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @hidden
|
|
143
|
+
*/
|
|
144
|
+
export function isTabSlot(child: ReactElement<any>): child is ReactElement<TabListProps> {
|
|
145
|
+
return child.type === TabSlot
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
screen: {
|
|
150
|
+
flex: 1,
|
|
151
|
+
position: 'relative',
|
|
152
|
+
height: '100%',
|
|
153
|
+
},
|
|
154
|
+
screenContainer: {
|
|
155
|
+
flexShrink: 0,
|
|
156
|
+
flexGrow: 1,
|
|
157
|
+
},
|
|
158
|
+
focused: {
|
|
159
|
+
zIndex: 1,
|
|
160
|
+
display: 'flex',
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
flexGrow: 1,
|
|
163
|
+
},
|
|
164
|
+
unfocused: {
|
|
165
|
+
zIndex: -1,
|
|
166
|
+
display: 'none',
|
|
167
|
+
flexShrink: 1,
|
|
168
|
+
flexGrow: 0,
|
|
169
|
+
},
|
|
170
|
+
})
|