import { composeRefs } from '@tamagui/compose-refs' import { isWeb } from '@tamagui/constants' import type { GroupProps } from '@tamagui/group' import { Group, useGroupItem } from '@tamagui/group' import { composeEventHandlers, withStaticProperties } from '@tamagui/helpers' import { RovingFocusGroup, type RovingFocusGroupProps } from '@tamagui/roving-focus' import { SizableContext } from '@tamagui/sizable-context' import { useControllableState } from '@tamagui/use-controllable-state' import { useDirection } from '@tamagui/use-direction' import type { GetProps, TamaguiElement, ViewProps } from '@tamagui/web' import { useEvent } from '@tamagui/web' import * as React from 'react' import type { LayoutRectangle } from 'react-native' import { TabsProvider, useTabsContext } from './StyledContext' import { DefaultTabsContentFrame, DefaultTabsFrame, DefaultTabsTabFrame } from './Tabs' type TabsComponent = (props: { direction: 'horizontal' | 'vertical' } & ViewProps) => any type TabComponent = (props: { active?: boolean } & ViewProps) => any type ContentComponent = (props: ViewProps) => any export function createTabs< C extends TabsComponent, T extends TabComponent, F extends ContentComponent, >(createProps: { ContentFrame: F; TabFrame: T; TabsFrame: C }) { const { ContentFrame = DefaultTabsContentFrame, TabFrame = DefaultTabsTabFrame, TabsFrame = DefaultTabsFrame, } = createProps as unknown as { ContentFrame: typeof DefaultTabsContentFrame TabFrame: typeof DefaultTabsTabFrame TabsFrame: typeof DefaultTabsFrame } const TABS_CONTEXT = 'TabsContext' const TAB_LIST_NAME = 'TabsList' const TabsList = React.forwardRef( (props: ScopedProps, forwardedRef) => { const { __scopeTabs, loop = true, children, ...listProps } = props const context = useTabsContext(__scopeTabs) return ( {children} ) } ) TabsList.displayName = TAB_LIST_NAME /* ------------------------------------------------------------------------------------------------- * TabsTrigger * -----------------------------------------------------------------------------------------------*/ const TRIGGER_NAME = 'TabsTrigger' /** * @deprecated Use `TabLayout` instead */ const TabsTrigger = TabFrame.styleable>( (props, forwardedRef) => { const { __scopeTabs, value, disabled = false, onInteraction, activeStyle, activeTheme, unstyled = false, ...triggerProps } = props const context = useTabsContext(__scopeTabs) const triggerId = makeTriggerId(context.baseId, value) const contentId = makeContentId(context.baseId, value) const isSelected = value === context.value const [layout, setLayout] = React.useState(null) const triggerRef = React.useRef(null) const groupItemProps = useGroupItem({ disabled: !!disabled }) React.useEffect(() => { context.registerTrigger() return () => context.unregisterTrigger() }, []) React.useEffect(() => { if (!triggerRef.current || !isWeb) return const el = triggerRef.current as unknown as HTMLElement function getTriggerSize() { if (!el) return setLayout({ width: el.offsetWidth, height: el.offsetHeight, x: el.offsetLeft, y: el.offsetTop, }) } getTriggerSize() const observer = new ResizeObserver(getTriggerSize) observer.observe(el) return () => { observer.disconnect() } }, [context.triggersCount]) React.useEffect(() => { if (isSelected && layout) { onInteraction?.('select', layout) } }, [isSelected, value, layout]) return ( { setLayout(event.nativeEvent.layout) }, })} onMouseEnter={composeEventHandlers(props.onMouseEnter, () => { if (layout) { onInteraction?.('hover', layout) } })} onMouseLeave={composeEventHandlers(props.onMouseLeave, () => { onInteraction?.('hover', null) })} role="tab" aria-selected={isSelected} aria-controls={contentId} data-state={isSelected ? 'active' : 'inactive'} data-disabled={disabled ? '' : undefined} id={triggerId} theme={activeTheme ?? null} unstyled={unstyled} {...(!unstyled && { size: context.size, })} {...(isSelected && { ...(!unstyled && !activeStyle && { backgroundColor: '$backgroundActive', }), ...(activeStyle as object), })} {...groupItemProps} disabled={disabled ?? groupItemProps.disabled} {...triggerProps} ref={composeRefs(forwardedRef, triggerRef)} onPress={composeEventHandlers(props.onPress ?? undefined, (event) => { // only call handler if it's the left button (mousedown gets triggered by all mouse buttons) // but not when the control key is pressed (avoiding MacOS right click) const webChecks = !isWeb || ((event as unknown as React.MouseEvent).button === 0 && (event as unknown as React.MouseEvent).ctrlKey === false) if (!disabled && !isSelected && webChecks) { context.onChange(value) } })} {...(isWeb && { onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { if ([' ', 'Enter'].includes(event.key)) { context.onChange(value) event.preventDefault() } }), onFocus: composeEventHandlers(props.onFocus, (event) => { if (layout) { onInteraction?.('focus', layout) } // handle "automatic" activation if necessary // ie. activate tab following focus const isAutomaticActivation = context.activationMode !== 'manual' if (!isSelected && !disabled && isAutomaticActivation) { context.onChange(value) } }), onBlur: composeEventHandlers(props.onBlur, () => { onInteraction?.('focus', null) }), })} /> ) } ) TabsTrigger.displayName = TRIGGER_NAME /* ------------------------------------------------------------------------------------------------- * TabsContent * -----------------------------------------------------------------------------------------------*/ const TabsContent = ContentFrame.styleable(function TabsContent( props: ScopedProps, forwardedRef ) { const { __scopeTabs, value, forceMount, children, ...contentProps } = props const context = useTabsContext(__scopeTabs) const isSelected = value === context.value const show = forceMount || isSelected const triggerId = makeTriggerId(context.baseId, value) const contentId = makeContentId(context.baseId, value) if (!show) { return null } return ( ) }) /* ------------------------------------------------------------------------------------------------- * Tabs * -----------------------------------------------------------------------------------------------*/ type ScopedProps

= P & { __scopeTabs?: string } // const [createTabsContext, createTabsScope] = createContextScope(TABS_NAME, [ // createRovingFocusGroupScope, // ]) // const useRovingFocusGroupScope = createRovingFocusGroupScope() type RovingFocusGroupProps = React.ComponentPropsWithoutRef const TabsComponent = TabsFrame.styleable(function Tabs( props: ScopedProps, forwardedRef ) { const { __scopeTabs, value: valueProp, onValueChange, defaultValue, orientation = 'horizontal', dir, activationMode = 'manual', size = '$true', ...tabsProps } = props const direction = useDirection(dir) const [value, setValue] = useControllableState({ prop: valueProp, onChange: onValueChange, defaultProp: defaultValue ?? '', }) const [triggersCount, setTriggersCount] = React.useState(0) const registerTrigger = useEvent(() => setTriggersCount((v) => v + 1)) const unregisterTrigger = useEvent(() => setTriggersCount((v) => v - 1)) return ( ) }) // make it so it can accept a generic // this broke things outside our repo, but not sure why, all non style props were missing // like onPress etc // as ( // props: TabsProps & { ref?: React.Ref } // ) => React.JSX.Element return withStaticProperties(TabsComponent, { List: TabsList, /** * @deprecated Use Tabs.Tab instead */ Trigger: TabsTrigger, Tab: TabsTrigger, Content: TabsContent, }) } /* ---------------------------------------------------------------------------------------------- */ function makeTriggerId(baseId: string, value: string) { return `${baseId}-trigger-${value}` } function makeContentId(baseId: string, value: string) { return `${baseId}-content-${value}` } // Types type TabsFrameProps = GetProps type TabsExtraProps = { /** The value for the selected tab, if controlled */ value?: string /** The value of the tab to select by default, if uncontrolled */ defaultValue?: Tab /** A function called when a new tab is selected */ onValueChange?: (value: Tab) => void /** * The orientation the tabs are layed out. * Mainly so arrow navigation is done accordingly (left & right vs. up & down) * @defaultValue horizontal */ orientation?: RovingFocusGroupProps['orientation'] /** * The direction of navigation between toolbar items. */ dir?: RovingFocusGroupProps['dir'] /** * Whether a tab is activated automatically or manually. Only supported in web. * @defaultValue automatic * */ activationMode?: 'automatic' | 'manual' } type TabsProps = TabsFrameProps & TabsExtraProps type TabsListFrameProps = GroupProps type TabsListProps = TabsListFrameProps & { /** * Whether to loop over after reaching the end or start of the items * @default true */ loop?: boolean } type InteractionType = 'select' | 'focus' | 'hover' type TabLayout = LayoutRectangle type TabsTriggerFrameProps = GetProps /** * @deprecated use `TabTabsProps` instead */ type TabsTriggerProps = TabsTriggerFrameProps & { /** The value for the tabs state to be changed to after activation of the trigger */ value: string /** Used for making custom indicators when trigger interacted with */ onInteraction?: (type: InteractionType, layout: TabLayout | null) => void /** Custom styles to apply when tab is active */ activeStyle?: TabsTriggerFrameProps /** Theme to apply when tab is active (use null for no theme) */ activeTheme?: string | null } type TabsTabProps = TabsTriggerProps type TabsTriggerLayout = LayoutRectangle type TabsContentFrameProps = GetProps type TabsContentExtraProps = { /** Will show the content when the value matches the state of Tabs root */ value: string /** * Used to force mounting when more control is needed. Useful when * controlling animation with Tamagui animations. */ forceMount?: boolean } type TabsContentProps = TabsContentFrameProps & TabsContentExtraProps export type { TabLayout, TabsContentProps, TabsListProps, TabsProps, TabsTabProps, TabsTriggerLayout, TabsTriggerProps, }