/** @jsxImportSource @fluentui-react-native/framework-base */ import React from 'react'; import { View, Pressable } from 'react-native'; import type { ViewProps } from 'react-native'; import type { UseTokens } from '@fluentui-react-native/framework'; import { compressible, useSlot, useFluentTheme, applyTokenLayers, mergeProps } from '@fluentui-react-native/framework'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import type { IconPropsV1 as IconProps } from '@fluentui-react-native/icon'; import type { PressablePropsExtended } from '@fluentui-react-native/interactive-hooks'; import type { TextProps } from '@fluentui-react-native/text'; import { Text } from '@fluentui-react-native/text'; import { useTabSlotProps } from './Tab.styling'; import { tabName } from './Tab.types'; import type { TabProps, TabState, TabTokens } from './Tab.types'; import { tabStates, useTabTokens } from './TabTokens'; import { useTab } from './useTab'; import { useTabAnimation } from './useTabAnimation'; import type { TabListState } from '../TabList/TabList.types'; import { TabListContext } from '../TabList/TabListContext'; const tabLookup = (layer: string, state: TabState, props: TabProps, tablistContext: TabListState): boolean => { return ( state[layer] || props[layer] || tablistContext[layer] || layer === tablistContext.appearance || layer === tablistContext.size || (layer === 'hasIcon' && props.icon) ); }; export const Tab = compressible((props: TabProps, useTokens: UseTokens) => { const tablist = React.useContext(TabListContext); const tab = useTab(props); const theme = useFluentTheme(); let [tokens, cache] = useTokens(theme); // Calculate component states to get the correct tokens here (this happens in useSlots for compose components) [tokens, cache] = applyTokenLayers(tokens, tabStates, cache, (layer) => tabLookup(layer, tab.state, tab.props, tablist)); // Get styling props for each Tab slot const slotProps = useTabSlotProps(tab.props, tokens, theme, tablist); const rootProps = useTabAnimation(props, tablist, tokens, slotProps.root); const RootSlot = useSlot(Pressable, rootProps); const StackSlot = useSlot(View, slotProps.stack as ViewProps); const IndicatorContainerSlot = useSlot(View, slotProps.indicatorContainer as ViewProps); const IndicatorSlot = useSlot(View, slotProps.indicator as ViewProps); const ContentContainerSlot = useSlot(View, slotProps.contentContainer as ViewProps); const ContentSlot = useSlot(Text, slotProps.content); const IconSlot = useSlot(Icon, slotProps.icon); return (final: TabProps, ...children: React.ReactNode[]) => { if (!tab.state) { return null; } // Get label for Tab to use if there's no accessibilityLabel prop passed in. let label = ''; let hasChildren = false; React.Children.forEach(children, (child) => { if (child !== null) { hasChildren = true; if (typeof child === 'string') { label = child; } } }); // `onLayout` is unused and excluded from the rest of the mergedProps to be passed into the RootSlot. // This is to ensure that the chained layout callback created in useTabAnimation isn't overwritten. const { icon, tabKey, onLayout: _, ...mergedProps } = mergeProps(tab.props, final, { accessibilityLabel: tab.props.accessibilityLabel || final.accessibilityLabel || label, }); if (__DEV__ && !hasChildren && !icon) { console.warn('A Tab component must render content. Children, an icon, or both should be passed in.'); } return ( {icon && } {hasChildren && ( {React.Children.map(children, (child, i) => typeof child === 'string' ? ( {child} ) : ( child ), )} )} ); }; }, useTabTokens); Tab.displayName = tabName; export default Tab;