Skip to content

Commit d184457

Browse files
Tabs design updates (#1785)
* Tabs updates * update chromatic story * Remove Content wrapper and title from panels * lint Co-authored-by: Rob Snow <[email protected]>
1 parent b916b32 commit d184457

File tree

12 files changed

+345
-246
lines changed

12 files changed

+345
-246
lines changed

packages/@adobe/spectrum-css-temp/components/tabs/index.css

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ governing permissions and limitations under the License.
4040
z-index: 0;
4141

4242
margin: 0;
43-
padding: 0 var(--spectrum-tabs-focus-ring-padding-x);
43+
padding: 0;
4444

4545
/* Friends should align to the top of the tabs */
4646
vertical-align: top;
@@ -259,16 +259,22 @@ governing permissions and limitations under the License.
259259

260260
.spectrum-TabsPanel-collapseWrapper {
261261
display: flex;
262-
overflow: hidden;
263262
position: relative;
264263
}
265264

266265
.spectrum-TabsPanel-tabs {
267-
flex-grow: 1;
266+
flex-grow: 0;
268267
flex-shrink: 0;
269268
flex-basis: 0%;
270269
}
271270

272271
.spectrum-TabsPanel-tabpanel {
273272
flex-grow: 1;
273+
border: var(--spectrum-tabs-focus-ring-size) solid transparent;
274+
}
275+
276+
.spectrum-TabsPanel--vertical {
277+
.spectrum-Tabs {
278+
padding-right: var(--spectrum-global-dimension-size-160);
279+
}
274280
}

packages/@adobe/spectrum-css-temp/components/tabs/skin.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,13 @@ governing permissions and limitations under the License.
9595
}
9696
}
9797
}
98+
99+
.spectrum-TabsPanel-tabpanel {
100+
&:focus {
101+
outline: none;
102+
}
103+
104+
&:focus-ring {
105+
border-color: var(--spectrum-tabs-focus-ring-color);
106+
}
107+
}

packages/@react-aria/tabs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@babel/runtime": "^7.6.2",
21+
"@react-aria/focus": "^3.2.4",
2122
"@react-aria/i18n": "^3.3.0",
2223
"@react-aria/interactions": "^3.3.3",
2324
"@react-aria/selection": "^3.3.2",

packages/@react-aria/tabs/src/useTabPanel.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,52 @@
1212

1313
import {AriaTabPanelProps} from '@react-types/tabs';
1414
import {generateId} from './utils';
15-
import {HTMLAttributes} from 'react';
15+
import {getFocusableTreeWalker} from '@react-aria/focus';
16+
import {HTMLAttributes, RefObject, useLayoutEffect, useState} from 'react';
1617
import {mergeProps} from '@react-aria/utils';
1718
import {TabListState} from '@react-stately/tabs';
1819

1920
interface TabPanelAria {
2021
/** Props for the tab panel element. */
2122
tabPanelProps: HTMLAttributes<HTMLElement>
2223
}
23-
24-
export function useTabPanel<T>(props: AriaTabPanelProps, state: TabListState<T>): TabPanelAria {
24+
25+
export function useTabPanel<T>(props: AriaTabPanelProps, state: TabListState<T>, ref: RefObject<HTMLElement>): TabPanelAria {
26+
let [tabIndex, setTabIndex] = useState(0);
27+
28+
// The tabpanel should have tabIndex=0 when there are no tabbable elements within it.
29+
// Otherwise, tabbing from the focused tab should go directly to the first tabbable element
30+
// within the tabpanel.
31+
useLayoutEffect(() => {
32+
if (ref?.current) {
33+
let update = () => {
34+
// Detect if there are any tabbable elements and update the tabIndex accordingly.
35+
let walker = getFocusableTreeWalker(ref.current, {tabbable: true});
36+
setTabIndex(walker.nextNode() ? undefined : 0);
37+
};
38+
39+
update();
40+
41+
// Update when new elements are inserted, or the tabIndex/disabled attribute updates.
42+
let observer = new MutationObserver(update);
43+
observer.observe(ref.current, {
44+
subtree: true,
45+
childList: true,
46+
attributes: true,
47+
attributeFilter: ['tabIndex', 'disabled']
48+
});
49+
50+
return () => {
51+
observer.disconnect();
52+
};
53+
}
54+
}, [ref]);
55+
2556
return {
2657
tabPanelProps: mergeProps(props, {
2758
id: generateId(state, state?.selectedKey, 'tabpanel'),
2859
'aria-labelledby': generateId(state, state?.selectedKey, 'tab'),
29-
tabIndex: 0,
60+
tabIndex,
3061
role: 'tabpanel'
3162
})
3263
};

packages/@react-spectrum/tabs/chromatic/Tabs.chromatic.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Content} from '@react-spectrum/view';
1413
import {Heading, Text} from '@react-spectrum/text';
15-
import {Item, Tabs} from '../';
14+
import {Item, TabList, TabPanels, Tabs} from '../';
1615
import {Meta, Story} from '@storybook/react';
1716
import React from 'react';
1817
import {SpectrumTabsProps} from '@react-types/tabs';
@@ -32,47 +31,47 @@ export default meta;
3231

3332

3433
const Template = <T extends object>(): Story<SpectrumTabsProps<T>> => (args) => (
34+
3535
<Tabs {...args} aria-label="Tab example" maxWidth={500}>
36-
<Item title="Tab 1" key="val1">
37-
<Content margin="size-160">
36+
<TabList>
37+
<Item key="val1">Tab 1</Item>
38+
<Item key="val2">Tab 2</Item>
39+
<Item key="val3">Tab 3</Item>
40+
<Item key="val4">Tab 4</Item>
41+
<Item key="val5">Tab 5</Item>
42+
</TabList>
43+
<TabPanels>
44+
<Item key="val1">
3845
<Heading>Tab Body 1</Heading>
3946
<Text>
4047
Dolore ex esse laboris elit magna esse sunt. Pariatur in veniam Lorem est occaecat do magna nisi mollit ipsum sit adipisicing fugiat ex. Pariatur ullamco exercitation ea qui adipisicing.
4148
</Text>
42-
</Content>
43-
</Item>
44-
<Item title="Tab 2" key="val2">
45-
<Content margin="size-160">
49+
</Item>
50+
<Item key="val2">
4651
<Heading>Tab Body 2</Heading>
4752
<Text>
4853
Dolore ex esse laboris elit magna esse sunt. Pariatur in veniam Lorem est occaecat do magna nisi mollit ipsum sit adipisicing fugiat ex. Pariatur ullamco exercitation ea qui adipisicing.
4954
</Text>
50-
</Content>
51-
</Item>
52-
<Item title="Tab 3" key="val3">
53-
<Content margin="size-160">
55+
</Item>
56+
<Item key="val3">
5457
<Heading>Tab Body 3</Heading>
5558
<Text>
5659
Dolore ex esse laboris elit magna esse sunt. Pariatur in veniam Lorem est occaecat do magna nisi mollit ipsum sit adipisicing fugiat ex. Pariatur ullamco exercitation ea qui adipisicing.
5760
</Text>
58-
</Content>
59-
</Item>
60-
<Item title="Tab 4" key="val4">
61-
<Content margin="size-160">
61+
</Item>
62+
<Item key="val4">
6263
<Heading>Tab Body 4</Heading>
6364
<Text>
6465
Dolore ex esse laboris elit magna esse sunt. Pariatur in veniam Lorem est occaecat do magna nisi mollit ipsum sit adipisicing fugiat ex. Pariatur ullamco exercitation ea qui adipisicing.
6566
</Text>
66-
</Content>
67-
</Item>
68-
<Item title="Tab 5" key="val5">
69-
<Content margin="size-160">
67+
</Item>
68+
<Item key="val5">
7069
<Heading>Tab Body 5</Heading>
7170
<Text>
7271
Dolore ex esse laboris elit magna esse sunt. Pariatur in veniam Lorem est occaecat do magna nisi mollit ipsum sit adipisicing fugiat ex. Pariatur ullamco exercitation ea qui adipisicing.
7372
</Text>
74-
</Content>
75-
</Item>
73+
</Item>
74+
</TabPanels>
7675
</Tabs>
7776
);
7877

packages/@react-spectrum/tabs/src/Tabs.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ function TabLine(props: TabLineProps) {
213213
selectedKey
214214
} = props;
215215

216-
let verticalSelectionIndicatorOffset = 12;
217216
let {direction} = useLocale();
218217
let {scale} = useProvider();
219218

@@ -228,18 +227,18 @@ function TabLine(props: TabLineProps) {
228227
// In RTL, calculate the transform from the right edge of the tablist so that resizing the window doesn't break the Tabline position due to offsetLeft changes
229228
let offset = direction === 'rtl' ? -1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) : selectedTab.offsetLeft;
230229
styleObj.transform = orientation === 'vertical'
231-
? `translateY(${selectedTab.offsetTop + verticalSelectionIndicatorOffset / 2}px)`
230+
? `translateY(${selectedTab.offsetTop}px)`
232231
: `translateX(${offset}px)`;
233232

234233
if (orientation === 'horizontal') {
235234
styleObj.width = `${selectedTab.offsetWidth}px`;
236235
} else {
237-
styleObj.height = `${selectedTab.offsetHeight - verticalSelectionIndicatorOffset}px`;
236+
styleObj.height = `${selectedTab.offsetHeight}px`;
238237
}
239238
setStyle(styleObj);
240239
}
241240

242-
}, [direction, setStyle, selectedTab, orientation, scale, verticalSelectionIndicatorOffset, selectedKey]);
241+
}, [direction, setStyle, selectedTab, orientation, scale, selectedKey]);
243242

244243
return <div className={classNames(styles, 'spectrum-Tabs-selectionIndicator')} role="presentation" style={style} />;
245244
}
@@ -257,6 +256,7 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
257256
// Pass original Tab props but override children to create the collection.
258257
const state = useTabListState({...tabProps, children: props.children});
259258

259+
let {styleProps} = useStyleProps(props);
260260
const {tabListProps} = useTabList({...tabProps, ...props}, state, tablistRef);
261261

262262
useEffect(() => {
@@ -265,14 +265,17 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
265265
// eslint-disable-next-line react-hooks/exhaustive-deps
266266
}, [state.disabledKeys, state.selectedItem, state.selectedKey, props.children]);
267267

268+
let tabListclassName = classNames(styles, 'spectrum-TabsPanel-tabs');
268269
const tabContent = (
269270
<div
271+
{...styleProps}
270272
{...tabListProps}
271273
ref={tablistRef}
272274
className={classNames(
273275
styles,
274276
'spectrum-Tabs',
275277
`spectrum-Tabs--${orientation}`,
278+
tabListclassName,
276279
{
277280
'spectrum-Tabs--quiet': isQuiet,
278281
['spectrum-Tabs--compact']: density === 'compact'
@@ -288,7 +291,6 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
288291
if (orientation === 'vertical') {
289292
return tabContent;
290293
} else {
291-
let tabListclassName = classNames(styles, 'spectrum-TabsPanel-tabs');
292294
return (
293295
<div
294296
ref={wrapperRef}
@@ -309,20 +311,24 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
309311
export function TabPanels<T>(props: SpectrumTabPanelsProps<T>) {
310312
const {tabState, tabProps, tabPanelProps: ctxTabPanelProps} = useContext(TabContext);
311313
const {tabListState} = tabState;
312-
const {tabPanelProps} = useTabPanel(props, tabListState);
314+
let ref = useRef();
315+
const {tabPanelProps} = useTabPanel(props, tabListState, ref);
316+
let {styleProps} = useStyleProps(props);
313317

314318
if (ctxTabPanelProps['aria-labelledby']) {
315319
tabPanelProps['aria-labelledby'] = ctxTabPanelProps['aria-labelledby'];
316320
}
317321

318322
const factory = nodes => new ListCollection(nodes);
319-
const collection = useCollection({items: tabProps.items, ...props}, factory);
323+
const collection = useCollection({items: tabProps.items, ...props}, factory, {suppressTextValueWarning: true});
320324
const selectedItem = tabListState ? collection.getItem(tabListState.selectedKey) : null;
321325

322326
return (
323-
<div {...tabPanelProps} className={classNames(styles, 'spectrum-TabsPanel-tabpanel')}>
324-
{selectedItem && selectedItem.props.children}
325-
</div>
327+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
328+
<div {...styleProps} {...tabPanelProps} ref={ref} className={classNames(styles, 'spectrum-TabsPanel-tabpanel')}>
329+
{selectedItem && selectedItem.props.children}
330+
</div>
331+
</FocusRing>
326332
);
327333
}
328334

0 commit comments

Comments
 (0)