Skip to content

Commit 9a10ecf

Browse files
committed
feat(tabs): move withArrows to root context & add tab scroll on focus
1 parent fb265e8 commit 9a10ecf

File tree

9 files changed

+124
-64
lines changed

9 files changed

+124
-64
lines changed

packages/ods-react/src/components/tabs/src/components/tab-list/TabList.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,17 @@ import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useCallback,
44
import { debounce } from '../../../../../utils/debounce';
55
import { BUTTON_SIZE, BUTTON_VARIANT, Button } from '../../../../button/src';
66
import { ICON_NAME, Icon } from '../../../../icon/src';
7+
import { useTabs } from '../../contexts/useTabs';
78
import style from './tabList.module.scss';
89

9-
interface TabListProp extends ComponentPropsWithRef<'div'> {
10-
/**
11-
* Whether the component displays navigation arrows around the tabs.
12-
*/
13-
withArrows?: boolean,
14-
}
10+
interface TabListProp extends ComponentPropsWithRef<'div'> {}
1511

1612
const TabList: FC<TabListProp> = forwardRef(({
1713
children,
1814
className,
19-
withArrows,
2015
...props
2116
}, ref): JSX.Element => {
17+
const { withArrows } = useTabs();
2218
const [isLeftButtonDisabled, setIsLeftButtonDisabled] = useState(false);
2319
const [isRightButtonDisabled, setIsRightButtonDisabled] = useState(false);
2420
const scrollRef = useRef<HTMLDivElement>(null);
@@ -85,6 +81,7 @@ const TabList: FC<TabListProp> = forwardRef(({
8581
disabled={ isLeftButtonDisabled }
8682
onClick={ onLeftScrollClick }
8783
size={ BUTTON_SIZE.xs }
84+
tabIndex={ -1 }
8885
variant={ BUTTON_VARIANT.ghost }>
8986
<Icon name={ ICON_NAME.chevronLeft } />
9087
</Button>
@@ -111,6 +108,7 @@ const TabList: FC<TabListProp> = forwardRef(({
111108
disabled={ isRightButtonDisabled }
112109
onClick={ onRightScrollClick }
113110
size={ BUTTON_SIZE.xs }
111+
tabIndex={ -1 }
114112
variant={ BUTTON_VARIANT.ghost }>
115113
<Icon name={ ICON_NAME.chevronRight } />
116114
</Button>

packages/ods-react/src/components/tabs/src/components/tab-list/tabList.module.scss

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
@use '../../../../../style/scroll';
33

44
$tab-list-border-bottom-size: 2px;
5-
$tab-list-arrow-padding: calc(var(--ods-outline-offset) * 2);
65

76
@layer ods-molecules {
87
.tab-list {
@@ -16,7 +15,6 @@ $tab-list-arrow-padding: calc(var(--ods-outline-offset) * 2);
1615
position: relative;
1716
align-items: center;
1817
align-self: stretch;
19-
padding: 0 $tab-list-arrow-padding;
2018
height: auto;
2119

2220
&--active {
@@ -36,7 +34,7 @@ $tab-list-arrow-padding: calc(var(--ods-outline-offset) * 2);
3634
&__left-arrow {
3735
&--active {
3836
&::after {
39-
left: calc(#{button.$ods-button-size-xs} + ($tab-list-arrow-padding * 2));
37+
left: button.$ods-button-size-xs;
4038
background: linear-gradient(270deg, rgba(255,255,255,0) 0, rgba(255,255,255,1) 100%);
4139
}
4240
}
@@ -45,7 +43,7 @@ $tab-list-arrow-padding: calc(var(--ods-outline-offset) * 2);
4543
&__right-arrow {
4644
&--active {
4745
&::after {
48-
right: calc(#{button.$ods-button-size-xs} + ($tab-list-arrow-padding * 2));;
46+
right: button.$ods-button-size-xs;
4947
background: linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,1) 100%);
5048
}
5149
}

packages/ods-react/src/components/tabs/src/components/tab/Tab.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Tabs } from '@ark-ui/react/tabs';
1+
import { Tabs, useTabsContext } from '@ark-ui/react/tabs';
22
import classNames from 'classnames';
3-
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react';
3+
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
44
import style from './tab.module.scss';
55

66
interface TabProp extends ComponentPropsWithRef<'button'> {
@@ -16,11 +16,25 @@ const Tab: FC<TabProp> = forwardRef(({
1616
value,
1717
...props
1818
}, ref): JSX.Element => {
19+
const { focusedValue } = useTabsContext();
20+
const innerRef = useRef<HTMLButtonElement>(null);
21+
22+
useImperativeHandle(ref, () => innerRef.current!, [innerRef]);
23+
24+
useEffect(() => {
25+
if (innerRef.current && focusedValue === value) {
26+
innerRef.current.scrollIntoView({
27+
behavior: 'smooth',
28+
block: 'nearest',
29+
});
30+
}
31+
}, [focusedValue, value]);
32+
1933
return (
2034
<Tabs.Trigger
2135
className={ classNames(style['tab'], className) }
2236
data-ods="tab"
23-
ref={ ref }
37+
ref={ innerRef }
2438
value={ value }
2539
{ ...props }>
2640
{ children }
Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,34 @@
11
import { Tabs as VendorTabs } from '@ark-ui/react/tabs';
22
import { type ComponentPropsWithRef, type FC, type JSX, forwardRef } from 'react';
3+
import { TabsProvider, type TabsRootProp } from '../../contexts/useTabs';
34

4-
interface TabsValueChangeEvent {
5-
value: string,
6-
}
7-
8-
interface TabsProp extends ComponentPropsWithRef<'div'> {
9-
/**
10-
* The initial value of the selected tab. Use when you don't need to control the value of the tabs.
11-
*/
12-
defaultValue?: string,
13-
/**
14-
* Callback fired when the state of selected tab changes.
15-
*/
16-
onValueChange?: (event: TabsValueChangeEvent) => void,
17-
/**
18-
* The controlled value of the selected tab.
19-
*/
20-
value?: string,
21-
}
5+
/**
6+
* @inheritDoc TabsRootProp
7+
*/
8+
interface TabsProp extends Omit<ComponentPropsWithRef<'div'>, 'defaultValue'>, TabsRootProp {}
229

2310
const Tabs: FC<TabsProp> = forwardRef(({
2411
children,
2512
className,
2613
defaultValue,
2714
onValueChange,
2815
value,
16+
withArrows,
2917
...props
3018
}, ref): JSX.Element => {
3119
return (
32-
<VendorTabs.Root
33-
className={ className }
34-
data-ods="tabs"
35-
defaultValue={ defaultValue }
36-
onValueChange={ onValueChange }
37-
ref={ ref }
38-
value={ value }
39-
{ ...props }>
40-
{ children }
41-
</VendorTabs.Root>
20+
<TabsProvider withArrows={ withArrows }>
21+
<VendorTabs.Root
22+
className={ className }
23+
data-ods="tabs"
24+
defaultValue={ defaultValue }
25+
onValueChange={ onValueChange }
26+
ref={ ref }
27+
value={ value }
28+
{ ...props }>
29+
{ children }
30+
</VendorTabs.Root>
31+
</TabsProvider>
4232
);
4333
});
4434

@@ -47,5 +37,4 @@ Tabs.displayName = 'Tabs';
4737
export {
4838
Tabs,
4939
type TabsProp,
50-
type TabsValueChangeEvent,
5140
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type JSX, type ReactNode, createContext, useContext } from 'react';
2+
3+
interface TabsValueChangeEvent {
4+
value: string,
5+
}
6+
7+
interface TabsRootProp {
8+
/**
9+
* The initial value of the selected tab. Use when you don't need to control the value of the tabs.
10+
*/
11+
defaultValue?: string,
12+
/**
13+
* Callback fired when the state of selected tab changes.
14+
*/
15+
onValueChange?: (event: TabsValueChangeEvent) => void,
16+
/**
17+
* The controlled value of the selected tab.
18+
*/
19+
value?: string,
20+
/**
21+
* Whether the component displays navigation arrows around the tabs.
22+
*/
23+
withArrows?: boolean,
24+
}
25+
26+
type TabsContextType = Pick<TabsRootProp, 'withArrows'>;
27+
28+
interface TabsProviderProp extends TabsContextType {
29+
children: ReactNode;
30+
}
31+
32+
const TabsContext = createContext<TabsContextType>({});
33+
34+
function TabsProvider({ children, withArrows }: TabsProviderProp): JSX.Element {
35+
return (
36+
<TabsContext.Provider value={{ withArrows }}>
37+
{ children }
38+
</TabsContext.Provider>
39+
);
40+
}
41+
42+
function useTabs(): TabsContextType {
43+
return useContext(TabsContext);
44+
}
45+
46+
export {
47+
TabsProvider,
48+
type TabsRootProp,
49+
type TabsValueChangeEvent,
50+
useTabs,
51+
};

packages/ods-react/src/components/tabs/src/dev.stories.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ export const CustomStyle = () => (
100100
export const WithArrows = () => (
101101
<>
102102
<p>No container size</p>
103-
<Tabs defaultValue="tab11">
104-
<TabList withArrows>
103+
<Tabs
104+
defaultValue="tab11"
105+
withArrows>
106+
<TabList>
105107
<Tab value="tab1">Tab 1</Tab>
106108
<Tab value="tab2">Tab 2</Tab>
107109
<Tab value="tab3">Tab 3</Tab>
@@ -123,8 +125,9 @@ export const WithArrows = () => (
123125
<p>In limited container</p>
124126
<Tabs
125127
defaultValue="tab1"
126-
style={{ width: '400px' }}>
127-
<TabList withArrows>
128+
style={{ width: '400px' }}
129+
withArrows>
130+
<TabList>
128131
<Tab value="tab1">Tab 1</Tab>
129132
<Tab value="tab2">Tab 2</Tab>
130133
<Tab value="tab3">Tab 3</Tab>
@@ -144,17 +147,21 @@ export const WithArrows = () => (
144147
</Tabs>
145148

146149
<p>No overflow</p>
147-
<Tabs defaultValue="tab1">
148-
<TabList withArrows>
150+
<Tabs
151+
defaultValue="tab1"
152+
withArrows>
153+
<TabList>
149154
<Tab value="tab1">Tab 1</Tab>
150155
<Tab value="tab2">Tab 2</Tab>
151156
<Tab value="tab3">Tab 3</Tab>
152157
</TabList>
153158
</Tabs>
154159

155160
<p>Long tab text</p>
156-
<Tabs defaultValue="tab1">
157-
<TabList withArrows>
161+
<Tabs
162+
defaultValue="tab1"
163+
withArrows>
164+
<TabList>
158165
<Tab value="tab1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</Tab>
159166
<Tab value="tab2">Tab 2</Tab>
160167
<Tab value="tab3">ed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</Tab>
@@ -174,8 +181,9 @@ export const WithArrowsDynamicResize = () => {
174181
<>
175182
<Tabs
176183
defaultValue="tab1"
177-
style={{ width: `${width}px` }}>
178-
<TabList withArrows>
184+
style={{ width: `${width}px` }}
185+
withArrows>
186+
<TabList>
179187
<Tab value="tab1">Tab 1</Tab>
180188
<Tab value="tab2">Tab 2</Tab>
181189
<Tab value="tab3">Tab 3</Tab>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
export { Tabs, type TabsProp, type TabsValueChangeEvent } from './components/tabs/Tabs';
1+
export { Tabs, type TabsProp } from './components/tabs/Tabs';
22
export { TabList, type TabListProp } from './components/tab-list/TabList';
33
export { Tab, type TabProp } from './components/tab/Tab';
44
export { TabContent, type TabContentProp } from './components/tab-content/TabContent';
5+
export { type TabsValueChangeEvent } from './contexts/useTabs';

packages/storybook/stories/components/tabs/documentation.mdx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ When focus moves to the **Tabs** component, it is set on the active **Tab**.
9191

9292
Once a **Tab** is focused, its associated content is also activated.
9393

94-
If scroll buttons are displayed, they can also receive focus and be activated via keyboard.
95-
9694
<Heading label="General Keyboard Shortcuts" level={ 3 } />
9795

9896
Pressing `Tab` moves focus into or out of the **Tabs** component while keeping the active **Tab** selected.

packages/storybook/stories/components/tabs/tabs.stories.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { type Meta, type StoryObj } from '@storybook/react';
22
import React, { useState } from 'react';
3-
import { Tabs, TabList, type TabListProp, Tab, TabContent, type TabsProp, type TabsValueChangeEvent } from '../../../../ods-react/src/components/tabs/src';
3+
import { Tabs, TabList, Tab, TabContent, type TabsProp, type TabsValueChangeEvent } from '../../../../ods-react/src/components/tabs/src';
44
import { excludeFromDemoControls, orderControls } from '../../../src/helpers/controls';
55
import { staticSourceRenderConfig } from '../../../src/helpers/source';
66
import { CONTROL_CATEGORY } from '../../../src/constants/controls';
77

88
type Story = StoryObj<TabsProp>;
9-
type DemoArg = Partial<TabListProp>;
9+
// type DemoArg = Partial<TabListProp>;
1010

1111
const meta: Meta<TabsProp> = {
1212
component: Tabs,
@@ -18,9 +18,11 @@ const meta: Meta<TabsProp> = {
1818
export default meta;
1919

2020
export const Demo: Story = {
21-
render: (arg: DemoArg) => (
22-
<Tabs defaultValue='tab1'>
23-
<TabList withArrows={ arg.withArrows }>
21+
render: (arg) => (
22+
<Tabs
23+
defaultValue='tab1'
24+
withArrows={ arg.withArrows }>
25+
<TabList >
2426
<Tab value="tab1">Tab 1</Tab>
2527
<Tab value="tab2">Tab 2</Tab>
2628
<Tab value="tab3">Tab 3</Tab>
@@ -58,7 +60,6 @@ export const Demo: Story = {
5860
withArrows: {
5961
table: {
6062
category: CONTROL_CATEGORY.design,
61-
type: { summary: 'boolean' }
6263
},
6364
control: { type: 'boolean' },
6465
},
@@ -172,8 +173,10 @@ export const WithArrows: Story = {
172173
},
173174
tags: ['!dev'],
174175
render: ({}) => (
175-
<Tabs defaultValue="tab1">
176-
<TabList withArrows>
176+
<Tabs
177+
defaultValue="tab1"
178+
withArrows>
179+
<TabList>
177180
<Tab value="tab1">Tab 1</Tab>
178181
<Tab value="tab2">Tab 2</Tab>
179182
<Tab value="tab3">Tab 3</Tab>

0 commit comments

Comments
 (0)