@tamagui/tabs 2.7.7 → 3.0.0-beta.1091.1

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 (67) hide show
  1. package/dist/cjs/StyledContext.cjs +15 -20
  2. package/dist/cjs/StyledContext.native.cjs +30 -0
  3. package/dist/cjs/StyledContext.native.js +16 -20
  4. package/dist/cjs/StyledContext.native.js.map +1 -1
  5. package/dist/cjs/Tabs.cjs +230 -68
  6. package/dist/cjs/Tabs.native.cjs +253 -0
  7. package/dist/cjs/Tabs.native.js +236 -67
  8. package/dist/cjs/Tabs.native.js.map +1 -1
  9. package/dist/cjs/index.cjs +9 -27
  10. package/dist/cjs/index.native.cjs +21 -0
  11. package/dist/cjs/index.native.js +10 -27
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/esm/StyledContext.mjs +3 -4
  14. package/dist/esm/StyledContext.mjs.map +1 -1
  15. package/dist/esm/StyledContext.native.js +3 -4
  16. package/dist/esm/StyledContext.native.js.map +1 -1
  17. package/dist/esm/Tabs.mjs +210 -55
  18. package/dist/esm/Tabs.mjs.map +1 -1
  19. package/dist/esm/Tabs.native.js +216 -55
  20. package/dist/esm/Tabs.native.js.map +1 -1
  21. package/dist/esm/index.js +3 -11
  22. package/dist/esm/index.mjs +3 -11
  23. package/dist/esm/index.native.js +3 -11
  24. package/dist/jsx/StyledContext.mjs +3 -4
  25. package/dist/jsx/StyledContext.mjs.map +1 -1
  26. package/dist/jsx/StyledContext.native.cjs +30 -0
  27. package/dist/jsx/StyledContext.native.js +16 -20
  28. package/dist/jsx/StyledContext.native.js.map +1 -1
  29. package/dist/jsx/Tabs.mjs +210 -55
  30. package/dist/jsx/Tabs.mjs.map +1 -1
  31. package/dist/jsx/Tabs.native.cjs +253 -0
  32. package/dist/jsx/Tabs.native.js +236 -67
  33. package/dist/jsx/Tabs.native.js.map +1 -1
  34. package/dist/jsx/index.js +3 -11
  35. package/dist/jsx/index.mjs +3 -11
  36. package/dist/jsx/index.native.cjs +21 -0
  37. package/dist/jsx/index.native.js +10 -27
  38. package/dist/jsx/index.native.js.map +1 -1
  39. package/package.json +14 -18
  40. package/src/StyledContext.tsx +3 -3
  41. package/src/Tabs.tsx +379 -55
  42. package/src/index.ts +1 -9
  43. package/types/StyledContext.d.ts +3 -3
  44. package/types/StyledContext.d.ts.map +1 -1
  45. package/types/Tabs.d.ts +398 -35
  46. package/types/Tabs.d.ts.map +1 -1
  47. package/types/index.d.ts +1 -537
  48. package/types/index.d.ts.map +1 -1
  49. package/dist/cjs/createTabs.cjs +0 -293
  50. package/dist/cjs/createTabs.native.js +0 -308
  51. package/dist/cjs/createTabs.native.js.map +0 -1
  52. package/dist/esm/createTabs.mjs +0 -257
  53. package/dist/esm/createTabs.mjs.map +0 -1
  54. package/dist/esm/createTabs.native.js +0 -269
  55. package/dist/esm/createTabs.native.js.map +0 -1
  56. package/dist/esm/index.js.map +0 -1
  57. package/dist/esm/index.mjs.map +0 -1
  58. package/dist/esm/index.native.js.map +0 -1
  59. package/dist/jsx/createTabs.mjs +0 -257
  60. package/dist/jsx/createTabs.mjs.map +0 -1
  61. package/dist/jsx/createTabs.native.js +0 -308
  62. package/dist/jsx/createTabs.native.js.map +0 -1
  63. package/dist/jsx/index.js.map +0 -1
  64. package/dist/jsx/index.mjs.map +0 -1
  65. package/src/createTabs.tsx +0 -438
  66. package/types/createTabs.d.ts +0 -527
  67. package/types/createTabs.d.ts.map +0 -1
package/src/Tabs.tsx CHANGED
@@ -1,69 +1,393 @@
1
- import { styled, View } from '@tamagui/core'
2
- import { getButtonSized } from '@tamagui/get-button-sized'
3
- import { SizableStack, ThemeableStack } from '@tamagui/stacks'
1
+ import { composeRefs } from '@tamagui/compose-refs'
2
+ import { isWeb } from '@tamagui/constants'
3
+ import { createStyledHOC, styled, View } from '@tamagui/core'
4
+ import { composeEventHandlers, withStaticProperties } from '@tamagui/helpers'
5
+ import { RovingFocusGroup, type RovingFocusGroupProps } from '@tamagui/roving-focus'
6
+ import { SizeContext, type TokenSize } from '@tamagui/size'
7
+ import { useTab, useTabContent, useTabs, useTabsList } from '@tamagui/tabs-headless'
8
+ import type { GetProps, StylePiece, TamaguiElement } from '@tamagui/web'
9
+ import { useEvent } from '@tamagui/web'
10
+ import * as React from 'react'
11
+ import type { LayoutRectangle } from '@tamagui/react-native-types'
12
+ import { TabsProvider, useTabsContext } from './StyledContext'
4
13
 
5
- const TABS_NAME = 'Tabs'
6
- export const DefaultTabsFrame = styled(SizableStack, {
7
- name: TABS_NAME,
14
+ const TABS_CONTEXT = 'TabsContext'
15
+
16
+ export const TabsFrame = styled(View, {
17
+ displayName: 'Tabs',
18
+ context: SizeContext,
8
19
  })
9
20
 
10
- const TRIGGER_NAME = 'TabsTrigger'
21
+ export const TabsListFrame = styled(View, {
22
+ displayName: 'TabsList',
23
+ context: SizeContext,
24
+ role: 'tablist',
25
+ })
11
26
 
12
- export const DefaultTabsTabFrame = styled(
13
- View,
14
- {
15
- name: TRIGGER_NAME,
16
- role: 'tab',
27
+ export const TabsTabFrame = styled(View, {
28
+ displayName: 'TabsTrigger',
29
+ context: SizeContext,
30
+ role: 'tab',
17
31
 
18
- variants: {
19
- size: {
20
- '...size': getButtonSized,
32
+ variants: {
33
+ disabled: {
34
+ true: {
35
+ pointerEvents: 'none',
21
36
  },
37
+ },
38
+ } as const,
39
+ })
22
40
 
23
- disabled: {
24
- true: {
25
- pointerEvents: 'none',
26
- },
27
- },
41
+ export const TabsContentFrame = styled(View, {
42
+ displayName: 'TabsContent',
43
+ context: SizeContext,
44
+ })
28
45
 
29
- unstyled: {
30
- false: {
31
- borderWidth: 0,
32
- backgroundColor: '$background',
33
- userSelect: 'none',
34
- justifyContent: 'center',
35
- alignItems: 'center',
36
- flexWrap: 'nowrap',
37
- flexDirection: 'row',
38
- cursor: 'pointer',
39
-
40
- pressStyle: {
41
- backgroundColor: '$backgroundPress',
42
- },
43
-
44
- focusVisibleStyle: {
45
- outlineColor: '$outlineColor',
46
- outlineWidth: 2,
47
- outlineStyle: 'solid',
48
- zIndex: 10,
49
- },
50
- },
51
- },
52
- } as const,
46
+ type TabsScopeProps = {
47
+ __scopeTabs?: string
48
+ }
53
49
 
54
- defaultVariants: {
55
- unstyled: process.env.TAMAGUI_HEADLESS === '1',
56
- },
57
- },
58
- {
59
- accept: {
60
- activeStyle: 'style',
61
- } as const,
50
+ type TabsFrameProps = GetProps<typeof TabsFrame>
51
+
52
+ type TabsExtraProps<Tab = string> = TabsScopeProps & {
53
+ /** The value for the selected tab, if controlled */
54
+ value?: string
55
+ /** The value of the tab to select by default, if uncontrolled */
56
+ defaultValue?: Tab
57
+ /** A function called when a new tab is selected */
58
+ onValueChange?: (value: Tab) => void
59
+ /** Coordinates a size value with styled descendants. */
60
+ size?: TokenSize
61
+ /**
62
+ * The orientation the tabs are laid out.
63
+ * Mainly so arrow navigation is done accordingly (left & right vs. up & down).
64
+ * @defaultValue horizontal
65
+ */
66
+ orientation?: RovingFocusGroupProps['orientation']
67
+ /** The direction of navigation between tab triggers. */
68
+ dir?: RovingFocusGroupProps['dir']
69
+ /**
70
+ * Whether a tab is activated automatically or manually. Automatic activation is only
71
+ * supported on web; native tabs always activate manually.
72
+ * @defaultValue automatic
73
+ */
74
+ activationMode?: 'automatic' | 'manual'
75
+ }
76
+
77
+ export type TabsProps<Tab = string> = TabsFrameProps & TabsExtraProps<Tab>
78
+
79
+ type TabsListExtraProps = TabsScopeProps & {
80
+ /** Whether keyboard navigation loops after the first or last trigger. */
81
+ loop?: boolean
82
+ /** Disables every trigger in the list. */
83
+ disabled?: boolean
84
+ }
85
+
86
+ export type TabsListProps = GetProps<typeof TabsListFrame> & TabsListExtraProps
87
+
88
+ export type InteractionType = 'select' | 'focus' | 'hover'
89
+ export type TabLayout = LayoutRectangle
90
+ export type TabsTriggerLayout = LayoutRectangle
91
+
92
+ type TabsTabExtraProps = TabsScopeProps & {
93
+ /** The value selected when this trigger is activated. */
94
+ value: string
95
+ /** Reports the measured trigger rectangle for custom indicators. */
96
+ onInteraction?: (type: InteractionType, layout: TabLayout | null) => void
97
+ /** Custom styles to apply while this trigger is selected. */
98
+ activeStyle?: StylePiece
99
+ /** Theme to apply while this trigger is selected. */
100
+ activeTheme?: string | null
101
+ }
102
+
103
+ export type TabsTabProps = GetProps<typeof TabsTabFrame> & TabsTabExtraProps
104
+
105
+ type TabsContentExtraProps = TabsScopeProps & {
106
+ /** The value that selects this content. */
107
+ value: string
108
+ /** Mounts the content even when its value is not selected. */
109
+ forceMount?: boolean
110
+ }
111
+
112
+ export type TabsContentProps = GetProps<typeof TabsContentFrame> & TabsContentExtraProps
113
+
114
+ const TabsListDisabledContext = React.createContext(false)
115
+
116
+ export const TabsList = createStyledHOC(
117
+ TabsListFrame,
118
+ function TabsList(props: TabsListProps, forwardedRef) {
119
+ const { __scopeTabs, loop = true, disabled = false, children, ...listProps } = props
120
+ const context = useTabsContext(__scopeTabs)
121
+ const { listProps: headlessListProps } = useTabsList({
122
+ orientation: context.orientation,
123
+ disabled,
124
+ })
125
+
126
+ return (
127
+ <TabsListDisabledContext.Provider value={disabled}>
128
+ <RovingFocusGroup
129
+ __scopeRovingFocusGroup={__scopeTabs || TABS_CONTEXT}
130
+ orientation={context.orientation}
131
+ dir={context.dir}
132
+ loop={loop}
133
+ asChild
134
+ >
135
+ <TabsListFrame
136
+ {...headlessListProps}
137
+ flexDirection={context.orientation === 'vertical' ? 'column' : 'row'}
138
+ ref={forwardedRef}
139
+ {...listProps}
140
+ >
141
+ {children}
142
+ </TabsListFrame>
143
+ </RovingFocusGroup>
144
+ </TabsListDisabledContext.Provider>
145
+ )
62
146
  }
63
147
  )
64
148
 
65
- const CONTENT_NAME = 'TabsContent'
149
+ export const TabsTab = createStyledHOC(
150
+ TabsTabFrame,
151
+ function TabsTab(props: TabsTabProps, forwardedRef) {
152
+ const {
153
+ __scopeTabs,
154
+ value,
155
+ disabled: disabledProp,
156
+ onInteraction,
157
+ activeStyle,
158
+ activeTheme,
159
+ onLayout,
160
+ onMouseEnter,
161
+ onMouseLeave,
162
+ onPress,
163
+ onKeyDown,
164
+ onFocus,
165
+ onBlur,
166
+ ...triggerProps
167
+ } = props
168
+ const context = useTabsContext(__scopeTabs)
169
+ const listDisabled = React.useContext(TabsListDisabledContext)
170
+ // interaction logic needs one boolean; a conditional flat value (clause
171
+ // string or object) can't drive focus/activation, so only literal true counts
172
+ const disabled = (disabledProp ?? listDisabled) === true
173
+ const { isSelected, tabProps } = useTab({
174
+ baseId: context.baseId,
175
+ value,
176
+ selectedValue: context.value,
177
+ disabled,
178
+ activationMode: context.activationMode,
179
+ onChange: context.onChange,
180
+ })
181
+ const {
182
+ onPress: activateOnPress,
183
+ onKeyDown: activateOnKeyDown,
184
+ onFocus: activateOnFocus,
185
+ ...tabA11yProps
186
+ } = tabProps
187
+ const [layout, setLayout] = React.useState<TabLayout | null>(null)
188
+ const triggerRef = React.useRef<TamaguiElement>(null)
189
+ const emitInteraction = useEvent(
190
+ (type: InteractionType, layout: TabLayout | null) => {
191
+ onInteraction?.(type, layout)
192
+ }
193
+ )
194
+
195
+ React.useEffect(() => {
196
+ context.registerTrigger()
197
+ return () => context.unregisterTrigger()
198
+ }, [])
199
+
200
+ React.useEffect(() => {
201
+ if (!triggerRef.current || !isWeb) return
202
+
203
+ const element = triggerRef.current as unknown as HTMLElement
204
+
205
+ function updateTriggerSize() {
206
+ const next = {
207
+ width: element.offsetWidth,
208
+ height: element.offsetHeight,
209
+ x: element.offsetLeft,
210
+ y: element.offsetTop,
211
+ }
212
+ setLayout((previous) =>
213
+ previous &&
214
+ previous.width === next.width &&
215
+ previous.height === next.height &&
216
+ previous.x === next.x &&
217
+ previous.y === next.y
218
+ ? previous
219
+ : next
220
+ )
221
+ }
222
+
223
+ updateTriggerSize()
224
+ const observer = new ResizeObserver(updateTriggerSize)
225
+ observer.observe(element)
226
+ // x and y are offsets within the offsetParent, so they go stale whenever
227
+ // that box changes even though the tab itself never resizes. a window
228
+ // resize that only reflows the list is exactly that case, and it left a
229
+ // roving indicator sitting at the old position until something unrelated
230
+ // happened to resize a tab.
231
+ const offsetParent = element.offsetParent
232
+ if (offsetParent) {
233
+ observer.observe(offsetParent)
234
+ }
235
+
236
+ return () => observer.disconnect()
237
+ }, [context.triggersCount])
238
+
239
+ React.useEffect(() => {
240
+ if (isSelected && layout) {
241
+ emitInteraction('select', layout)
242
+ }
243
+ }, [emitInteraction, isSelected, layout])
244
+
245
+ return (
246
+ <RovingFocusGroup.Item
247
+ __scopeRovingFocusGroup={__scopeTabs || TABS_CONTEXT}
248
+ asChild
249
+ tabIndex={disabled ? -1 : 0}
250
+ active={isSelected}
251
+ >
252
+ <TabsTabFrame
253
+ onLayout={
254
+ isWeb
255
+ ? onLayout
256
+ : composeEventHandlers(onLayout, (event) => {
257
+ setLayout(event.nativeEvent.layout)
258
+ })
259
+ }
260
+ onMouseEnter={composeEventHandlers(onMouseEnter, () => {
261
+ if (layout) {
262
+ emitInteraction('hover', layout)
263
+ }
264
+ })}
265
+ onMouseLeave={composeEventHandlers(onMouseLeave, () => {
266
+ emitInteraction('hover', null)
267
+ })}
268
+ {...tabA11yProps}
269
+ theme={isSelected ? (activeTheme ?? null) : null}
270
+ size={context.size}
271
+ {...triggerProps}
272
+ // after triggerProps so active styles beat base styles from styled() skins
273
+ style={[triggerProps.style, isSelected && activeStyle]}
274
+ ref={composeRefs(forwardedRef, triggerRef)}
275
+ onPress={composeEventHandlers(onPress ?? undefined, activateOnPress as any)}
276
+ {...(isWeb && {
277
+ onKeyDown: composeEventHandlers(onKeyDown, activateOnKeyDown),
278
+ // emit the focus interaction before activating, matching the order
279
+ // the indicator animations were built against
280
+ onFocus: composeEventHandlers(
281
+ onFocus,
282
+ composeEventHandlers(() => {
283
+ if (layout) {
284
+ emitInteraction('focus', layout)
285
+ }
286
+ }, activateOnFocus)
287
+ ),
288
+ onBlur: composeEventHandlers(onBlur, () => {
289
+ emitInteraction('focus', null)
290
+ }),
291
+ })}
292
+ />
293
+ </RovingFocusGroup.Item>
294
+ )
295
+ }
296
+ )
297
+
298
+ export const TabsContent = createStyledHOC(
299
+ TabsContentFrame,
300
+ function TabsContent(props: TabsContentProps, forwardedRef) {
301
+ const { __scopeTabs, value, forceMount, children, ...contentProps } = props
302
+ const context = useTabsContext(__scopeTabs)
303
+ const { shouldMount, contentProps: headlessContentProps } = useTabContent({
304
+ baseId: context.baseId,
305
+ value,
306
+ selectedValue: context.value,
307
+ orientation: context.orientation,
308
+ forceMount,
309
+ })
310
+
311
+ if (!shouldMount) {
312
+ return null
313
+ }
314
+
315
+ return (
316
+ <TabsContentFrame
317
+ key={value}
318
+ // @ts-ignore hidden is a web-only accessibility attribute
319
+ {...headlessContentProps}
320
+ {...contentProps}
321
+ ref={forwardedRef}
322
+ >
323
+ {children}
324
+ </TabsContentFrame>
325
+ )
326
+ }
327
+ )
328
+
329
+ const TabsComponent = createStyledHOC(
330
+ TabsFrame,
331
+ function Tabs(props: TabsProps, forwardedRef) {
332
+ const {
333
+ __scopeTabs,
334
+ value: valueProp,
335
+ onValueChange,
336
+ defaultValue,
337
+ orientation = 'horizontal',
338
+ dir,
339
+ activationMode = 'automatic',
340
+ size = true,
341
+ ...tabsProps
342
+ } = props
343
+ const {
344
+ value,
345
+ setValue,
346
+ baseId,
347
+ direction,
348
+ triggersCount,
349
+ registerTrigger,
350
+ unregisterTrigger,
351
+ tabsProps: headlessTabsProps,
352
+ } = useTabs({
353
+ value: valueProp,
354
+ onValueChange,
355
+ defaultValue,
356
+ orientation,
357
+ dir,
358
+ activationMode,
359
+ })
360
+
361
+ return (
362
+ <SizeContext.Provider size={size}>
363
+ <TabsProvider
364
+ scope={__scopeTabs}
365
+ baseId={baseId}
366
+ value={value}
367
+ onChange={setValue}
368
+ orientation={orientation}
369
+ dir={direction}
370
+ activationMode={activationMode}
371
+ size={size}
372
+ registerTrigger={registerTrigger}
373
+ triggersCount={triggersCount}
374
+ unregisterTrigger={unregisterTrigger}
375
+ >
376
+ <TabsFrame
377
+ direction={direction}
378
+ {...headlessTabsProps}
379
+ {...tabsProps}
380
+ ref={forwardedRef}
381
+ />
382
+ </TabsProvider>
383
+ </SizeContext.Provider>
384
+ )
385
+ }
386
+ )
66
387
 
67
- export const DefaultTabsContentFrame = styled(ThemeableStack, {
68
- name: CONTENT_NAME,
388
+ export const Tabs = withStaticProperties(TabsComponent, {
389
+ Frame: TabsFrame,
390
+ List: TabsList,
391
+ Tab: TabsTab,
392
+ Content: TabsContent,
69
393
  })
package/src/index.ts CHANGED
@@ -1,10 +1,2 @@
1
- import { createTabs } from './createTabs'
2
- import { DefaultTabsContentFrame, DefaultTabsFrame, DefaultTabsTabFrame } from './Tabs'
3
- export * from './createTabs'
4
1
  export * from './StyledContext'
5
-
6
- export const Tabs = createTabs({
7
- ContentFrame: DefaultTabsContentFrame,
8
- TabFrame: DefaultTabsTabFrame,
9
- TabsFrame: DefaultTabsFrame,
10
- })
2
+ export * from './Tabs'
@@ -1,5 +1,5 @@
1
- import type { SizeTokens } from '@tamagui/core';
2
- import type { TabsProps } from './createTabs';
1
+ import type { TokenSize } from '@tamagui/size';
2
+ import type { TabsProps } from './Tabs';
3
3
  export type TabsContextValue = {
4
4
  baseId: string;
5
5
  value?: string;
@@ -7,7 +7,7 @@ export type TabsContextValue = {
7
7
  orientation?: TabsProps['orientation'];
8
8
  dir?: TabsProps['dir'];
9
9
  activationMode?: TabsProps['activationMode'];
10
- size: SizeTokens;
10
+ size: TokenSize;
11
11
  registerTrigger: () => void;
12
12
  unregisterTrigger: () => void;
13
13
  triggersCount: number;
@@ -1 +1 @@
1
- {"version":3,"file":"StyledContext.d.ts","sourceRoot":"","sources":["../src/StyledContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,CAAA;IACtC,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAA;IACtB,cAAc,CAAC,EAAE,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC5C,IAAI,EAAE,UAAU,CAAA;IAChB,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,iBAAiB,EAAE,MAAM,IAAI,CAAA;IAC7B,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,eAAO,MAAkB,YAAY;;;IAAoB,cAAc,sCAC9B,CAAA"}
1
+ {"version":3,"file":"StyledContext.d.ts","sourceRoot":"","sources":["../src/StyledContext.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,CAAA;IACtC,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAA;IACtB,cAAc,CAAC,EAAE,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC5C,IAAI,EAAE,SAAS,CAAA;IACf,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,iBAAiB,EAAE,MAAM,IAAI,CAAA;IAC7B,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,eAAO,MAAkB,YAAY;;;IAAoB,cAAc,sCAC9B,CAAA"}