diff --git a/packages/@react-spectrum/s2/src/Toast.tsx b/packages/@react-spectrum/s2/src/Toast.tsx index 92ae9dae582..4696f22dbf4 100644 --- a/packages/@react-spectrum/s2/src/Toast.tsx +++ b/packages/@react-spectrum/s2/src/Toast.tsx @@ -68,10 +68,7 @@ function startViewTransition(fn: () => void, type: string) { if ('startViewTransition' in document) { // Safari doesn't support :active-view-transition-type() yet, so we fall back to a class on the html element. document.documentElement.classList.add(toastCss[type]); - let viewTransition = document.startViewTransition({ - update: () => flushSync(fn), - types: [toastCss[type]] - }); + let viewTransition = document.startViewTransition(() => flushSync(fn)); viewTransition.ready.catch(() => {}); viewTransition.finished.then(() => { diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx index afb9dd8ccea..bf01e70ad86 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx @@ -16,37 +16,65 @@ export const tags = ['navigation']; ```tsx render docs={docs.exports.Tabs} links={docs.links} props={['orientation', 'keyboardActivation', 'isDisabled']} type="vanilla" files={["starters/docs/src/Tabs.tsx", "starters/docs/src/Tabs.css"]} "use client"; - import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs'; - import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home'; - import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen'; - import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search'; - import Settings from '@react-spectrum/s2/illustrations/gradient/generic1/GearSetting'; + import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs'; + import {Form} from 'vanilla-starter/Form'; + import {TextField} from 'vanilla-starter/TextField'; + import {Button} from 'vanilla-starter/Button'; + import {RadioGroup, Radio} from 'vanilla-starter/RadioGroup'; + import {CheckboxGroup} from 'vanilla-starter/CheckboxGroup'; + import {Checkbox} from 'vanilla-starter/Checkbox'; - - Home - Files - Search - Settings + + General + Appearance + Notifications + Profile - - - - - - - - - - - - + + +
+ + Show sidebar + Show status bar + +
+ +
+ + Auto + Light + Dark + + + Small + Medium + Large + +
+
+ + + Account activity + Mentions + Direct message + Marketing emails + + + +
+ + + + +
+
``` ```tsx render docs={docs.exports.Tabs} links={docs.links} props={['orientation', 'keyboardActivation', 'isDisabled']} type="tailwind" files={["starters/tailwind/src/Tabs.tsx"]} "use client"; - import {Tabs, TabList, Tab, TabPanel} from 'tailwind-starter/Tabs'; + import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'tailwind-starter/Tabs'; import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home'; import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen'; import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search'; @@ -59,18 +87,20 @@ export const tags = ['navigation']; Search Settings - - - - - - - - - - - - + + + + + + + + + + + + + + ``` @@ -82,9 +112,8 @@ export const tags = ['navigation']; ```tsx render "use client"; -import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs'; +import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs'; import {Button} from 'vanilla-starter/Button'; -import {Collection} from 'react-aria-components'; import {useState} from 'react'; function Example() { @@ -127,9 +156,9 @@ function Example() { - + {item => {item.content}} - + ); } @@ -137,7 +166,7 @@ function Example() { ```css render hidden .button-group { - border-bottom: 1px solid gray; + border-bottom: 0.5px solid var(--border-color); display: flex; align-items: center; gap: 8px; @@ -150,7 +179,7 @@ Use the `href` prop on a `` to create a link. See the **client side routing ```tsx render "use client"; -import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs'; +import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs'; import {useSyncExternalStore} from 'react'; export default function Example() { @@ -165,9 +194,11 @@ export default function Example() { Shared Deleted - Home - Shared - Deleted + + Home + Shared + Deleted + ); } @@ -193,7 +224,7 @@ Use the `defaultSelectedKey` or `selectedKey` prop to set the selected tab. The ```tsx render "use client"; import type {Key} from 'react-aria-components'; -import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs'; +import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs'; import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home'; import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen'; import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search'; @@ -217,18 +248,20 @@ function Example() { Search Settings - - - - - - - - - - - - + + + + + + + + + + + + + +

Selected tab: {tab}

@@ -240,14 +273,16 @@ function Example() { -```tsx links={{Tabs: '#tabs', TabList: '#tablist', Tab: '#tab', TabPanel: '#tabpanel', SelectionIndicator: 'selection.html#animated-selectionindicator'}} +```tsx links={{Tabs: '#tabs', TabList: '#tablist', Tab: '#tab', TabPanels: '#tabpanels', TabPanel: '#tabpanel', SelectionIndicator: 'selection.html#animated-selectionindicator'}} - + + + ``` @@ -263,6 +298,17 @@ function Example() { +### TabPanels + + + ### TabPanel diff --git a/packages/dev/s2-docs/src/PropTable.tsx b/packages/dev/s2-docs/src/PropTable.tsx index fc9e19b2282..2c9b0b69399 100644 --- a/packages/dev/s2-docs/src/PropTable.tsx +++ b/packages/dev/s2-docs/src/PropTable.tsx @@ -1,8 +1,8 @@ import {Code, styles as codeStyles} from './Code'; +import {CSSVariables, StateTable} from './StateTable'; import {DisclosureRow} from './DisclosureRow'; import React from 'react'; import {renderHTMLfromMarkdown, setLinks, TComponent, TInterface, TType, Type} from './types'; -import {StateTable} from './StateTable'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from './Table'; @@ -104,9 +104,9 @@ export function PropTable({component, links, showDescription, hideRenderProps, s style={!defaultClassName ? {marginTop: 16} : undefined} properties={renderProps.properties} showOptional={showOptionalRenderProps} - hideSelector={hideSelector} - cssVariables={cssVariables} /> - ) : null} + hideSelector={hideSelector} /> + ) : null} + {cssVariables && } ); } diff --git a/packages/dev/s2-docs/src/StateTable.tsx b/packages/dev/s2-docs/src/StateTable.tsx index 59a9370bb77..3c384923d3b 100644 --- a/packages/dev/s2-docs/src/StateTable.tsx +++ b/packages/dev/s2-docs/src/StateTable.tsx @@ -61,32 +61,38 @@ export function StateTable({properties, links, showOptional, hideSelector, cssVa table = ( <> {table} - - - - CSS Variable - - - - {Object.entries(cssVariables).map(([name, description]) => ( - - - - - {name} - - - - - {renderHTMLfromMarkdown(description, {forceInline: true})} - - - ))} - -
+ ); } return table; } + +export function CSSVariables({cssVariables}: {cssVariables: {[name: string]: string}}) { + return ( + + + + CSS Variable + + + + {Object.entries(cssVariables).map(([name, description]) => ( + + + + + {name} + + + + + {renderHTMLfromMarkdown(description, {forceInline: true})} + + + ))} + +
+ ); +} diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index 0fc5bd2b798..36fc44a13eb 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -12,22 +12,12 @@ import {AriaLabelingProps, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {AriaTabListProps, AriaTabPanelProps, mergeProps, Orientation, useFocusRing, useHover, useTab, useTabList, useTabPanel} from 'react-aria'; -import { - ClassNameOrFunction, - ContextValue, - Provider, - RenderProps, - SlotProps, - StyleRenderProps, - useContextProps, - useRenderProps, - useSlottedContext -} from './utils'; +import {ClassNameOrFunction, ContextValue, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlottedContext} from './utils'; import {Collection, CollectionBuilder, CollectionNode, createHideableComponent, createLeafComponent} from '@react-aria/collections'; import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, usePersistedKeys} from './Collection'; -import {filterDOMProps, inertValue, useObjectRef} from '@react-aria/utils'; +import {filterDOMProps, inertValue, useEnterAnimation, useExitAnimation, useLayoutEffect, useObjectRef} from '@react-aria/utils'; import {Collection as ICollection, Node, TabListState, useTabListState} from 'react-stately'; -import React, {createContext, ForwardedRef, forwardRef, JSX, useContext, useMemo} from 'react'; +import React, {createContext, ForwardedRef, forwardRef, JSX, useContext, useMemo, useRef, useState} from 'react'; import {SelectionIndicatorContext} from './SelectionIndicator'; import {SharedElementTransition} from './SharedElementTransition'; @@ -143,6 +133,16 @@ export interface TabPanelRenderProps { * @selector [data-inert] */ isInert: boolean, + /** + * Whether the tab panel is currently entering. Use this to apply animations. + * @selector [data-entering] + */ + isEntering: boolean, + /** + * Whether the tab panel is currently exiting. Use this to apply animations. + * @selector [data-exiting] + */ + isExiting: boolean, /** * State of the tab list. */ @@ -327,18 +327,123 @@ export const Tab = /*#__PURE__*/ createLeafComponent(TabItemNode, (props: TabPro ); }); +export interface TabPanelsProps extends CollectionProps, StyleProps, GlobalDOMAttributes { + /** + * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. + * @default 'react-aria-TabPanels' + */ + className?: string +} + +/** + * Groups multiple `` elements, and provides CSS variables for animated transitions. + */ +export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanels(props: TabPanelsProps, forwardedRef: ForwardedRef) { + let state = useContext(TabListStateContext)!; + let ref = useObjectRef(forwardedRef); + + let selectedKeyRef = useRef(state.selectedKey); + let prevSize = useRef(null); + let hasTransition = useRef(null); + useLayoutEffect(() => { + let el = ref.current; + if (!el) { + return; + } + + if (hasTransition.current == null) { + hasTransition.current = /width|height|all/.test(window.getComputedStyle(el).transition); + } + + if (hasTransition.current && selectedKeyRef.current != null && selectedKeyRef.current !== state.selectedKey) { + // Measure auto size. + el.style.setProperty('--tab-panel-width', 'auto'); + el.style.setProperty('--tab-panel-height', 'auto'); + let {width, height} = el.getBoundingClientRect(); + + if (prevSize.current && (prevSize.current.width !== width || prevSize.current.height !== height)) { + // Revert to previous size. + el.style.setProperty('--tab-panel-width', prevSize.current.width + 'px'); + el.style.setProperty('--tab-panel-height', prevSize.current.height + 'px'); + + // Force style re-calculation to trigger animations. + window.getComputedStyle(el).height; + + // Animate to current pixel size. + el.style.setProperty('--tab-panel-width', width + 'px'); + el.style.setProperty('--tab-panel-height', height + 'px'); + + // When animations complete, revert back to auto size. + Promise.all(el.getAnimations().map(a => a.finished)) + .then(() => { + el.style.setProperty('--tab-panel-width', 'auto'); + el.style.setProperty('--tab-panel-height', 'auto'); + }) + .catch(() => {}); + } + } + + selectedKeyRef.current = state.selectedKey; + }, [ref, state.selectedKey]); + + // Store previous size before DOM updates occur. + // This breaks the rules of hooks because there is no effect that runs _before_ DOM updates. + // eslint-disable-next-line rulesdir/pure-render + if (state.selectedKey != null && state.selectedKey !== selectedKeyRef.current && ref.current && hasTransition.current) { + // eslint-disable-next-line rulesdir/pure-render + prevSize.current = ref.current.getBoundingClientRect(); + } + + return ( +
+ +
+ ); +}); + /** * A TabPanel provides the content for a tab. */ export const TabPanel = /*#__PURE__*/ createHideableComponent(function TabPanel(props: TabPanelProps, forwardedRef: ForwardedRef) { const state = useContext(TabListStateContext)!; let ref = useObjectRef(forwardedRef); + + // Track if the tab panel was initially selected on mount (after extra render to populate the collection). + // In this case, we don't want to trigger animations. + let isSelected = state.selectedKey === props.id; + let [isInitiallySelected, setInitiallySelected] = useState(state.selectedKey != null ? isSelected : null); + if (isInitiallySelected == null && state.selectedKey != null) { + setInitiallySelected(isSelected); + } else if (!isSelected && isInitiallySelected) { + setInitiallySelected(false); + } + + let isExiting = useExitAnimation(ref, isSelected); + if (!isSelected && !props.shouldForceMount && !isExiting) { + return null; + } + + return ( + + ); +}); + +function TabPanelInner(props: TabPanelProps & {tabPanelRef: RefObject, isInitiallySelected: boolean, isExiting: boolean}) { + let state = useContext(TabListStateContext)!; // eslint-disable-next-line @typescript-eslint/no-unused-vars - let {id, ...otherProps} = props; + let {id, tabPanelRef: ref, isInitiallySelected, isExiting, ...otherProps} = props; let {tabPanelProps} = useTabPanel(props, state, ref); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let isSelected = state.selectedKey === props.id; + let isEntering = useEnterAnimation(ref) && !isInitiallySelected; let renderProps = useRenderProps({ ...props, defaultClassName: 'react-aria-TabPanel', @@ -347,14 +452,12 @@ export const TabPanel = /*#__PURE__*/ createHideableComponent(function TabPanel( isFocusVisible, // @ts-ignore - compatibility with React < 19 isInert: inertValue(!isSelected), + isEntering, + isExiting, state } }); - if (!isSelected && !props.shouldForceMount) { - return null; - } - let DOMProps = filterDOMProps(otherProps, {global: true}); delete DOMProps.id; @@ -370,7 +473,9 @@ export const TabPanel = /*#__PURE__*/ createHideableComponent(function TabPanel( data-focus-visible={isFocusVisible || undefined} // @ts-ignore inert={inertValue(!isSelected || props.inert)} - data-inert={!isSelected ? 'true' : undefined}> + data-inert={!isSelected ? 'true' : undefined} + data-entering={isEntering || undefined} + data-exiting={isExiting || undefined}> ); -}); +} diff --git a/packages/react-aria-components/src/index.ts b/packages/react-aria-components/src/index.ts index 6d4bc432c8b..3b70bc2e2b7 100644 --- a/packages/react-aria-components/src/index.ts +++ b/packages/react-aria-components/src/index.ts @@ -67,7 +67,7 @@ export {Slider, SliderOutput, SliderTrack, SliderThumb, SliderContext, SliderOut export {Switch, SwitchContext} from './Switch'; export {TableLoadMoreItem, Table, Row, Cell, Column, ColumnResizer, TableHeader, TableBody, TableContext, ResizableTableContainer, useTableOptions, TableStateContext, TableColumnResizeStateContext} from './Table'; export {TableLayout} from './TableLayout'; -export {Tabs, TabList, TabPanel, Tab, TabsContext, TabListStateContext} from './Tabs'; +export {Tabs, TabList, TabPanels, TabPanel, Tab, TabsContext, TabListStateContext} from './Tabs'; export {TagGroup, TagGroupContext, TagList, TagListContext, Tag} from './TagGroup'; export {Text, TextContext} from './Text'; export {TextArea, TextAreaContext} from './TextArea'; @@ -133,7 +133,7 @@ export type {SeparatorProps} from './Separator'; export type {SliderOutputProps, SliderProps, SliderRenderProps, SliderThumbProps, SliderTrackProps, SliderTrackRenderProps, SliderThumbRenderProps} from './Slider'; export type {SwitchProps, SwitchRenderProps} from './Switch'; export type {TableProps, TableRenderProps, TableHeaderProps, TableBodyProps, TableBodyRenderProps, ResizableTableContainerProps, ColumnProps, ColumnRenderProps, ColumnResizerProps, ColumnResizerRenderProps, RowProps, RowRenderProps, CellProps, CellRenderProps, TableLoadMoreItemProps} from './Table'; -export type {TabListProps, TabListRenderProps, TabPanelProps, TabPanelRenderProps, TabProps, TabsProps, TabRenderProps, TabsRenderProps} from './Tabs'; +export type {TabListProps, TabListRenderProps, TabPanelsProps, TabPanelProps, TabPanelRenderProps, TabProps, TabsProps, TabRenderProps, TabsRenderProps} from './Tabs'; export type {TagGroupProps, TagListProps, TagListRenderProps, TagProps, TagRenderProps} from './TagGroup'; export type {TextAreaProps} from './TextArea'; export type {TextFieldProps, TextFieldRenderProps} from './TextField'; diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 81ed7336c98..d05350f4267 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -11,7 +11,7 @@ */ import {act, fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal'; -import {Button, Collection, Tab, TabList, TabPanel, Tabs, Tooltip, TooltipTrigger} from '../'; +import {Button, Tab, TabList, TabPanel, TabPanels, Tabs, Tooltip, TooltipTrigger} from '../'; import React, {useState} from 'react'; import {TabsExample} from '../stories/Tabs.stories'; import {User} from '@react-aria/test-utils'; @@ -565,7 +565,7 @@ describe('Tabs', () => { - + {(item) => ( { {item.content} )} - + ); } diff --git a/starters/docs/src/Tabs.css b/starters/docs/src/Tabs.css index fd96c3a72d7..8b1a50b4956 100644 --- a/starters/docs/src/Tabs.css +++ b/starters/docs/src/Tabs.css @@ -2,6 +2,7 @@ .react-aria-Tabs { display: flex; + gap: var(--spacing-2); color: var(--text-color); &[data-orientation=horizontal] { @@ -10,6 +11,10 @@ &[data-orientation=vertical] { flex-direction: row; + width: 100%; + .react-aria-TabPanels { + flex: 1; + } } } @@ -28,7 +33,7 @@ bottom: -1.5px; width: 100%; height: 3px; - border-radius: 3px; + transition-property: translate, width; } } @@ -41,6 +46,7 @@ right: -1.5px; height: 100%; width: 3px; + transition-property: translate, height; } } } @@ -64,8 +70,8 @@ .react-aria-SelectionIndicator { position: absolute; - transition-property: translate, width, height; transition-duration: 200ms; + border-radius: 3px; @media (prefers-reduced-motion: reduce) { transition: none; @@ -96,13 +102,38 @@ } } +.react-aria-TabPanels { + position: relative; + height: var(--tab-panel-height); + width: var(--tab-panel-width); + transition: height 400ms; + overflow: clip; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + .react-aria-TabPanel { - margin-top: 4px; - padding: 10px; - border-radius: 4px; + padding: var(--spacing-3); + border-radius: var(--radius); outline: none; + box-sizing: border-box; + transition: opacity 400ms; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } + + &[data-entering], + &[data-exiting] { + opacity: 0; + } + + &[data-exiting] { + position: absolute; + top: 0; + left: 0; + width: 100%; + } } diff --git a/starters/docs/src/Tabs.tsx b/starters/docs/src/Tabs.tsx index ac922e957e2..474784a033b 100644 --- a/starters/docs/src/Tabs.tsx +++ b/starters/docs/src/Tabs.tsx @@ -6,10 +6,12 @@ import { TabProps, Tab as RACTab, TabsProps, + TabPanels as RACTabPanels, TabPanelProps, TabPanel as RACTabPanel, composeRenderProps, - SelectionIndicator + SelectionIndicator, + TabPanelsProps } from 'react-aria-components'; import './Tabs.css'; @@ -32,6 +34,10 @@ export function Tab(props: TabProps) { ); } +export function TabPanels(props: TabPanelsProps) { + return ; +} + export function TabPanel(props: TabPanelProps) { return ; -} \ No newline at end of file +} diff --git a/starters/docs/stories/Tabs.stories.tsx b/starters/docs/stories/Tabs.stories.tsx index 0d72159ae25..da8287fef5f 100644 --- a/starters/docs/stories/Tabs.stories.tsx +++ b/starters/docs/stories/Tabs.stories.tsx @@ -1,4 +1,4 @@ -import {Tabs, Tab, TabList, TabPanel} from '../src/Tabs'; +import {Tabs, Tab, TabList, TabPanel, TabPanels} from '../src/Tabs'; import {fn} from '@storybook/test'; import type {Meta, StoryFn} from '@storybook/react'; @@ -25,14 +25,16 @@ export const Example: Story = (args) => ( Monarchy and Republic Empire - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + ); diff --git a/starters/tailwind/src/Tabs.tsx b/starters/tailwind/src/Tabs.tsx index 05bf8c673d9..9440dac0420 100644 --- a/starters/tailwind/src/Tabs.tsx +++ b/starters/tailwind/src/Tabs.tsx @@ -3,17 +3,20 @@ import React from 'react'; import { Tab as RACTab, TabList as RACTabList, + TabPanels as RACTabPanels, TabPanel as RACTabPanel, Tabs as RACTabs, SelectionIndicator, TabListProps, TabPanelProps, + TabPanelsProps, TabProps, TabsProps, composeRenderProps } from 'react-aria-components'; import { tv } from 'tailwind-variants'; import { focusRing } from './utils'; +import { twMerge } from 'tailwind-merge'; const tabsStyles = tv({ base: 'flex gap-4 font-sans', @@ -59,10 +62,10 @@ export function TabList(props: TabListProps) { const tabProps = tv({ extend: focusRing, - base: 'relative flex items-center cursor-default rounded-full px-4 py-1.5 text-sm font-medium transition forced-color-adjust-none', + base: 'group relative flex items-center cursor-default rounded-full px-4 py-1.5 text-sm font-medium transition forced-color-adjust-none', variants: { isDisabled: { - true: 'text-gray-200 dark:text-zinc-600 forced-colors:text-[GrayText] selected:text-gray-300 dark:selected:text-zinc-500 forced-colors:selected:text-[HighlightText] selected:bg-gray-200 dark:selected:bg-zinc-600 forced-colors:selected:bg-[GrayText]' + true: 'text-gray-200 dark:text-zinc-600 forced-colors:text-[GrayText] selected:text-white dark:selected:text-zinc-500 forced-colors:selected:text-[HighlightText] selected:bg-gray-200 dark:selected:bg-zinc-600 forced-colors:selected:bg-[GrayText]' } } }); @@ -77,15 +80,23 @@ export function Tab(props: TabProps) { )}> {composeRenderProps(props.children, children => (<> {children} - + ))} ); } +export function TabPanels(props: TabPanelsProps) { + return ( + + ); +} + const tabPanelStyles = tv({ extend: focusRing, - base: 'flex-1 p-4 text-sm text-gray-900 dark:text-zinc-100' + base: 'flex-1 box-border p-4 text-sm text-gray-900 dark:text-zinc-100 transition entering:opacity-0 exiting:opacity-0 exiting:absolute exiting:top-0 exiting:left-0 exiting:w-full' }); export function TabPanel(props: TabPanelProps) { diff --git a/starters/tailwind/stories/Tabs.stories.tsx b/starters/tailwind/stories/Tabs.stories.tsx index 4c501e1eadd..d76a8dae1bd 100644 --- a/starters/tailwind/stories/Tabs.stories.tsx +++ b/starters/tailwind/stories/Tabs.stories.tsx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/react'; import React from 'react'; -import { Tab, TabList, TabPanel, Tabs } from '../src/Tabs'; +import { Tab, TabList, TabPanel, TabPanels, Tabs } from '../src/Tabs'; const meta: Meta = { component: Tabs, @@ -19,14 +19,16 @@ export const Example = (args: any) => ( Monarchy and Republic Empire - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + );