Skip to content

Commit e2a44bc

Browse files
authored
feat: Animated transitions between tab panels (#9077)
1 parent 8fb1b61 commit e2a44bc

File tree

11 files changed

+351
-142
lines changed

11 files changed

+351
-142
lines changed

packages/dev/s2-docs/pages/react-aria/Tabs.mdx

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,65 @@ export const tags = ['navigation'];
1616
<ExampleSwitcher>
1717
```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"]}
1818
"use client";
19-
import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs';
20-
import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home';
21-
import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen';
22-
import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search';
23-
import Settings from '@react-spectrum/s2/illustrations/gradient/generic1/GearSetting';
19+
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
20+
import {Form} from 'vanilla-starter/Form';
21+
import {TextField} from 'vanilla-starter/TextField';
22+
import {Button} from 'vanilla-starter/Button';
23+
import {RadioGroup, Radio} from 'vanilla-starter/RadioGroup';
24+
import {CheckboxGroup} from 'vanilla-starter/CheckboxGroup';
25+
import {Checkbox} from 'vanilla-starter/Checkbox';
2426

2527
<Tabs/* PROPS */>
26-
<TabList aria-label="Tabs">
27-
<Tab id="home">Home</Tab>
28-
<Tab id="files">Files</Tab>
29-
<Tab id="search">Search</Tab>
30-
<Tab id="settings">Settings</Tab>
28+
<TabList aria-label="Settings">
29+
<Tab id="general">General</Tab>
30+
<Tab id="appearance">Appearance</Tab>
31+
<Tab id="notifications">Notifications</Tab>
32+
<Tab id="profile">Profile</Tab>
3133
</TabList>
32-
<TabPanel id="home" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
33-
<Home />
34-
</TabPanel>
35-
<TabPanel id="files" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
36-
<Folder />
37-
</TabPanel>
38-
<TabPanel id="search" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
39-
<Search />
40-
</TabPanel>
41-
<TabPanel id="settings" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
42-
<Settings />
43-
</TabPanel>
34+
<TabPanels>
35+
<TabPanel id="general">
36+
<Form>
37+
<TextField label="Homepage" defaultValue="react-aria.adobe.com" />
38+
<Checkbox defaultSelected>Show sidebar</Checkbox>
39+
<Checkbox>Show status bar</Checkbox>
40+
</Form>
41+
</TabPanel>
42+
<TabPanel id="appearance">
43+
<Form>
44+
<RadioGroup label="Theme" defaultValue="auto">
45+
<Radio value="auto">Auto</Radio>
46+
<Radio value="light">Light</Radio>
47+
<Radio value="dark">Dark</Radio>
48+
</RadioGroup>
49+
<RadioGroup label="Font size" defaultValue="medium">
50+
<Radio value="small">Small</Radio>
51+
<Radio value="medium">Medium</Radio>
52+
<Radio value="large">Large</Radio>
53+
</RadioGroup>
54+
</Form>
55+
</TabPanel>
56+
<TabPanel id="notifications">
57+
<CheckboxGroup label="Notifications settings" defaultValue={['account', 'dms']}>
58+
<Checkbox value="account">Account activity</Checkbox>
59+
<Checkbox value="mentions">Mentions</Checkbox>
60+
<Checkbox value="dms">Direct message</Checkbox>
61+
<Checkbox value="marketing">Marketing emails</Checkbox>
62+
</CheckboxGroup>
63+
</TabPanel>
64+
<TabPanel id="profile">
65+
<Form>
66+
<TextField label="Name" defaultValue="Devon Govett" />
67+
<TextField label="Username" defaultValue="@devongovett" />
68+
<Button>Update profile</Button>
69+
</Form>
70+
</TabPanel>
71+
</TabPanels>
4472
</Tabs>
4573
```
4674

4775
```tsx render docs={docs.exports.Tabs} links={docs.links} props={['orientation', 'keyboardActivation', 'isDisabled']} type="tailwind" files={["starters/tailwind/src/Tabs.tsx"]}
4876
"use client";
49-
import {Tabs, TabList, Tab, TabPanel} from 'tailwind-starter/Tabs';
77+
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'tailwind-starter/Tabs';
5078
import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home';
5179
import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen';
5280
import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search';
@@ -59,18 +87,20 @@ export const tags = ['navigation'];
5987
<Tab id="search">Search</Tab>
6088
<Tab id="settings">Settings</Tab>
6189
</TabList>
62-
<TabPanel id="home" className="flex items-center justify-center">
63-
<Home />
64-
</TabPanel>
65-
<TabPanel id="files" className="flex items-center justify-center">
66-
<Folder />
67-
</TabPanel>
68-
<TabPanel id="search" className="flex items-center justify-center">
69-
<Search />
70-
</TabPanel>
71-
<TabPanel id="settings" className="flex items-center justify-center">
72-
<Settings />
73-
</TabPanel>
90+
<TabPanels>
91+
<TabPanel id="home" className="flex items-center justify-center">
92+
<Home />
93+
</TabPanel>
94+
<TabPanel id="files" className="flex items-center justify-center">
95+
<Folder />
96+
</TabPanel>
97+
<TabPanel id="search" className="flex items-center justify-center">
98+
<Search />
99+
</TabPanel>
100+
<TabPanel id="settings" className="flex items-center justify-center">
101+
<Settings />
102+
</TabPanel>
103+
</TabPanels>
74104
</Tabs>
75105
```
76106

@@ -82,9 +112,8 @@ export const tags = ['navigation'];
82112

83113
```tsx render
84114
"use client";
85-
import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs';
115+
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
86116
import {Button} from 'vanilla-starter/Button';
87-
import {Collection} from 'react-aria-components';
88117
import {useState} from 'react';
89118

90119
function Example() {
@@ -127,17 +156,17 @@ function Example() {
127156
<Button onPress={removeTab}>Remove tab</Button>
128157
</div>
129158
</div>
130-
<Collection items={tabs}>
159+
<TabPanels items={tabs}>
131160
{item => <TabPanel>{item.content}</TabPanel>}
132-
</Collection>
161+
</TabPanels>
133162
</Tabs>
134163
);
135164
}
136165
```
137166

138167
```css render hidden
139168
.button-group {
140-
border-bottom: 1px solid gray;
169+
border-bottom: 0.5px solid var(--border-color);
141170
display: flex;
142171
align-items: center;
143172
gap: 8px;
@@ -150,7 +179,7 @@ Use the `href` prop on a `<Tab>` to create a link. See the **client side routing
150179

151180
```tsx render
152181
"use client";
153-
import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs';
182+
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
154183
import {useSyncExternalStore} from 'react';
155184

156185
export default function Example() {
@@ -165,9 +194,11 @@ export default function Example() {
165194
<Tab id="#/shared" href="#/shared">Shared</Tab>
166195
<Tab id="#/deleted" href="#/deleted">Deleted</Tab>
167196
</TabList>
168-
<TabPanel id="#/">Home</TabPanel>
169-
<TabPanel id="#/shared">Shared</TabPanel>
170-
<TabPanel id="#/deleted">Deleted</TabPanel>
197+
<TabPanels>
198+
<TabPanel id="#/">Home</TabPanel>
199+
<TabPanel id="#/shared">Shared</TabPanel>
200+
<TabPanel id="#/deleted">Deleted</TabPanel>
201+
</TabPanels>
171202
</Tabs>
172203
);
173204
}
@@ -193,7 +224,7 @@ Use the `defaultSelectedKey` or `selectedKey` prop to set the selected tab. The
193224
```tsx render
194225
"use client";
195226
import type {Key} from 'react-aria-components';
196-
import {Tabs, TabList, Tab, TabPanel} from 'vanilla-starter/Tabs';
227+
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
197228
import Home from '@react-spectrum/s2/illustrations/gradient/generic2/Home';
198229
import Folder from '@react-spectrum/s2/illustrations/gradient/generic2/FolderOpen';
199230
import Search from '@react-spectrum/s2/illustrations/gradient/generic2/Search';
@@ -217,18 +248,20 @@ function Example() {
217248
<Tab id="search" isDisabled>Search</Tab>
218249
<Tab id="settings">Settings</Tab>
219250
</TabList>
220-
<TabPanel id="home" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
221-
<Home />
222-
</TabPanel>
223-
<TabPanel id="files" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
224-
<Folder />
225-
</TabPanel>
226-
<TabPanel id="search" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
227-
<Search />
228-
</TabPanel>
229-
<TabPanel id="settings" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
230-
<Settings />
231-
</TabPanel>
251+
<TabPanels>
252+
<TabPanel id="home" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
253+
<Home />
254+
</TabPanel>
255+
<TabPanel id="files" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
256+
<Folder />
257+
</TabPanel>
258+
<TabPanel id="search" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
259+
<Search />
260+
</TabPanel>
261+
<TabPanel id="settings" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
262+
<Settings />
263+
</TabPanel>
264+
</TabPanels>
232265
</Tabs>
233266
<p>Selected tab: {tab}</p>
234267
</div>
@@ -240,14 +273,16 @@ function Example() {
240273

241274
<Anatomy />
242275

243-
```tsx links={{Tabs: '#tabs', TabList: '#tablist', Tab: '#tab', TabPanel: '#tabpanel', SelectionIndicator: 'selection.html#animated-selectionindicator'}}
276+
```tsx links={{Tabs: '#tabs', TabList: '#tablist', Tab: '#tab', TabPanels: '#tabpanels', TabPanel: '#tabpanel', SelectionIndicator: 'selection.html#animated-selectionindicator'}}
244277
<Tabs>
245278
<TabList>
246279
<Tab>
247280
<SelectionIndicator />
248281
</Tab>
249282
</TabList>
250-
<TabPanel />
283+
<TabPanels>
284+
<TabPanel />
285+
</TabPanels>
251286
</Tabs>
252287
```
253288

@@ -263,6 +298,17 @@ function Example() {
263298

264299
<PropTable component={docs.exports.Tab} links={docs.links} showDescription />
265300

301+
### TabPanels
302+
303+
<PropTable
304+
component={docs.exports.TabPanels}
305+
links={docs.links}
306+
showDescription
307+
cssVariables={{
308+
'--tab-panel-width': 'The width of the active tab panel in pixels. Useful for animations.',
309+
'--tab-panel-height': 'The height of the active tab panel in pixels. Useful for animations.'
310+
}} />
311+
266312
### TabPanel
267313

268314
<PropTable component={docs.exports.TabPanel} links={docs.links} showDescription />

packages/dev/s2-docs/src/PropTable.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {Code, styles as codeStyles} from './Code';
2+
import {CSSVariables, StateTable} from './StateTable';
23
import {DisclosureRow} from './DisclosureRow';
34
import React from 'react';
45
import {renderHTMLfromMarkdown, setLinks, TComponent, TInterface, TType, Type} from './types';
5-
import {StateTable} from './StateTable';
66
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
77
import {Table, TableBody, TableCell, TableColumn, TableHeader, TableRow} from './Table';
88

@@ -104,9 +104,9 @@ export function PropTable({component, links, showDescription, hideRenderProps, s
104104
style={!defaultClassName ? {marginTop: 16} : undefined}
105105
properties={renderProps.properties}
106106
showOptional={showOptionalRenderProps}
107-
hideSelector={hideSelector}
108-
cssVariables={cssVariables} />
109-
) : null}
107+
hideSelector={hideSelector} />
108+
) : null}
109+
{cssVariables && <CSSVariables cssVariables={cssVariables} />}
110110
</>
111111
);
112112
}

packages/dev/s2-docs/src/StateTable.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,38 @@ export function StateTable({properties, links, showOptional, hideSelector, cssVa
6161
table = (
6262
<>
6363
{table}
64-
<Table style={{marginTop: 16}}>
65-
<TableHeader>
66-
<TableRow>
67-
<TableColumn role="columnheader">CSS Variable</TableColumn>
68-
</TableRow>
69-
</TableHeader>
70-
<TableBody>
71-
{Object.entries(cssVariables).map(([name, description]) => (
72-
<Fragment key={name}>
73-
<TableRow>
74-
<TableCell role="rowheader" hideBorder>
75-
<code className={codeStyle}>
76-
<span className={codeStyles.property}>{name}</span>
77-
</code>
78-
</TableCell>
79-
</TableRow>
80-
<TableRow>
81-
<TableCell>{renderHTMLfromMarkdown(description, {forceInline: true})}</TableCell>
82-
</TableRow>
83-
</Fragment>
84-
))}
85-
</TableBody>
86-
</Table>
64+
<CSSVariables cssVariables={cssVariables} />
8765
</>
8866
);
8967
}
9068

9169
return table;
9270
}
71+
72+
export function CSSVariables({cssVariables}: {cssVariables: {[name: string]: string}}) {
73+
return (
74+
<Table style={{marginTop: 16}}>
75+
<TableHeader>
76+
<TableRow>
77+
<TableColumn role="columnheader">CSS Variable</TableColumn>
78+
</TableRow>
79+
</TableHeader>
80+
<TableBody>
81+
{Object.entries(cssVariables).map(([name, description]) => (
82+
<Fragment key={name}>
83+
<TableRow>
84+
<TableCell role="rowheader" hideBorder>
85+
<code className={codeStyle}>
86+
<span className={codeStyles.property}>{name}</span>
87+
</code>
88+
</TableCell>
89+
</TableRow>
90+
<TableRow>
91+
<TableCell>{renderHTMLfromMarkdown(description, {forceInline: true})}</TableCell>
92+
</TableRow>
93+
</Fragment>
94+
))}
95+
</TableBody>
96+
</Table>
97+
);
98+
}

0 commit comments

Comments
 (0)