Skip to content

Commit 9280496

Browse files
refactor: Tabs
1 parent bee2db4 commit 9280496

6 files changed

Lines changed: 143 additions & 50 deletions

File tree

packages/@primereact/headless/src/tabs/useTabs.props.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export const defaultProps: useTabsProps = {
55
defaultValue: undefined,
66
onValueChange: undefined,
77
selectOnFocus: false,
8-
scrollStrategy: 'nearest'
8+
scrollStrategy: 'nearest',
9+
tabIndex: 0
910
};

packages/@primereact/headless/src/tabs/useTabs.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { defaultProps } from './useTabs.props';
77
export const useTabs = withHeadless({
88
name: 'useTabs',
99
defaultProps,
10-
setup: ({ props }) => {
10+
setup: ({ props, id }) => {
1111
const [activeTabState, setActiveTabState] = useControlledState({
1212
value: props.value,
1313
defaultValue: props.defaultValue,
@@ -303,6 +303,7 @@ export const useTabs = withHeadless({
303303
[canScrollPrev, canScrollNext]
304304
);
305305

306+
// prop getters
306307
const contentProps = React.useMemo(
307308
() => ({
308309
onScroll: onContentScroll,
@@ -312,6 +313,38 @@ export const useTabs = withHeadless({
312313
[onContentScroll, canScrollPrev, canScrollNext]
313314
);
314315

316+
const getTabProps = (value: string | number | undefined, disabled?: boolean) => {
317+
const active = isItemActive(value);
318+
319+
return {
320+
role: 'tab' as const,
321+
tabIndex: active ? (props.tabIndex ?? 0) : -1,
322+
ref: active ? (activeTabRef as React.RefObject<HTMLButtonElement>) : undefined,
323+
disabled,
324+
'aria-selected': active,
325+
'aria-disabled': disabled,
326+
'aria-controls': `${id}_tabpanel_${value}`,
327+
...(active && { 'data-active': '' as const }),
328+
...(disabled && { 'data-disabled': '' as const }),
329+
...scrollStateAttrs,
330+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => onTabClick(event, value),
331+
onFocus: (event: React.FocusEvent<HTMLButtonElement>) => onTabFocus(event, value),
332+
onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => onTabKeyDown(event, value)
333+
};
334+
};
335+
336+
const getIndicatorProps = () => {
337+
return {
338+
ref: activeBarRef as React.RefObject<HTMLSpanElement>,
339+
style: {
340+
'--active-bar-width': activeBarRef.current?.offsetWidth + 'px',
341+
'--active-bar-height': activeBarRef.current?.offsetHeight + 'px',
342+
'--active-bar-left': activeBarRef.current?.offsetLeft + 'px',
343+
'--active-bar-top': activeBarRef.current?.offsetTop + 'px'
344+
} as React.CSSProperties
345+
};
346+
};
347+
315348
return {
316349
state,
317350
// methods
@@ -321,6 +354,9 @@ export const useTabs = withHeadless({
321354
onTabKeyDown,
322355
onTabClick,
323356
onTabFocus,
357+
// prop getters
358+
getTabProps,
359+
getIndicatorProps,
324360
// scroll
325361
scrollPrev,
326362
scrollNext,

packages/@primereact/types/src/headless/tabs/useTabs.types.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export interface useTabsProps {
4848
* @default 'nearest'
4949
*/
5050
scrollStrategy?: 'nearest' | 'center' | false | ((content: HTMLElement, tab: HTMLElement) => void);
51+
/**
52+
* The tabIndex of the active tab.
53+
* @default 0
54+
*/
55+
tabIndex?: number;
5156
/**
5257
* Callback fired when the tabs's value changes.
5358
* @param event The event that triggered the change.
@@ -75,6 +80,82 @@ export interface useTabsState {
7580
canScrollNext: boolean;
7681
}
7782

83+
/**
84+
* Props returned by getTabProps for a tab element.
85+
*/
86+
export interface useTabsTabProps {
87+
/**
88+
* The role of the tab element.
89+
*/
90+
role: 'tab';
91+
/**
92+
* The tabIndex of the tab element.
93+
*/
94+
tabIndex: number;
95+
/**
96+
* Ref for the active tab element.
97+
*/
98+
ref: React.RefObject<HTMLButtonElement> | undefined;
99+
/**
100+
* Whether the tab is disabled.
101+
*/
102+
disabled: boolean | undefined;
103+
/**
104+
* Whether the tab is selected.
105+
*/
106+
'aria-selected': boolean;
107+
/**
108+
* Whether the tab is disabled (aria).
109+
*/
110+
'aria-disabled': boolean | undefined;
111+
/**
112+
* The id of the tabpanel controlled by this tab.
113+
*/
114+
'aria-controls': string;
115+
/**
116+
* Present when the tab is active.
117+
*/
118+
'data-active'?: '';
119+
/**
120+
* Present when the tab is disabled.
121+
*/
122+
'data-disabled'?: '';
123+
/**
124+
* Present when the tab list can scroll to the previous page.
125+
*/
126+
'data-can-scroll-prev'?: '';
127+
/**
128+
* Present when the tab list can scroll to the next page.
129+
*/
130+
'data-can-scroll-next'?: '';
131+
/**
132+
* Click event handler for the tab.
133+
*/
134+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
135+
/**
136+
* Focus event handler for the tab.
137+
*/
138+
onFocus: (event: React.FocusEvent<HTMLButtonElement>) => void;
139+
/**
140+
* Keyboard event handler for the tab.
141+
*/
142+
onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
143+
}
144+
145+
/**
146+
* Props returned by getIndicatorProps for the active tab indicator element.
147+
*/
148+
export interface useTabsIndicatorProps {
149+
/**
150+
* Ref for the active bar element.
151+
*/
152+
ref: React.RefObject<HTMLSpanElement>;
153+
/**
154+
* CSS custom properties for positioning the indicator.
155+
*/
156+
style: React.CSSProperties;
157+
}
158+
78159
/**
79160
* Defines the methods and properties exposed by useTabs.
80161
*/
@@ -161,6 +242,16 @@ export interface useTabsExposes {
161242
'data-can-scroll-prev'?: '';
162243
'data-can-scroll-next'?: '';
163244
};
245+
/**
246+
* Returns accessibility and interaction props for a tab element.
247+
* @param value The value of the tab.
248+
* @param disabled Whether the tab is disabled.
249+
*/
250+
getTabProps: (value: string | number | undefined, disabled?: boolean) => useTabsTabProps;
251+
/**
252+
* Returns props for the active tab indicator element.
253+
*/
254+
getIndicatorProps: () => useTabsIndicatorProps;
164255
}
165256

166257
/**

packages/@primereact/types/src/primitive/tabs/TabsRoot.types.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
*
1010
*/
1111
import type { ComponentInstance } from '@primereact/types/core';
12-
import type { BaseComponentProps, PassThroughType } from '../..';
1312
import type { useTabsChangeEvent, useTabsExposes, useTabsProps, useTabsState } from '@primereact/types/headless/tabs';
13+
import type { BaseComponentProps, PassThroughType } from '../..';
1414

1515
/**
1616
* Defines passthrough(pt) options type in Tabs component.
@@ -36,11 +36,6 @@ export interface TabsRootProps extends BaseComponentProps<TabsRootInstance, useT
3636
* @default false
3737
*/
3838
lazy?: boolean | undefined;
39-
/**
40-
* Index of the element in tabbing order.
41-
* @default 0
42-
*/
43-
tabIndex?: number;
4439
}
4540

4641
/**

packages/primereact/src/tabs/root/TabsRoot.props.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@ import type { TabsRootProps } from '@primereact/types/primitive/tabs';
44
export const defaultRootProps: TabsRootProps = {
55
...HeadlessTabs.defaultProps,
66
as: 'div',
7-
lazy: false,
8-
tabIndex: 0
7+
lazy: false
98
};

packages/primereact/src/tabs/tab/TabsTab.tsx

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,22 @@ export const TabsTab = withComponent({
1212
const { props } = instance;
1313
const tabs = useTabsContext();
1414

15-
const active = React.useMemo(() => tabs?.isItemActive(props.value) ?? false, [tabs, props.value]);
15+
const tabProps = tabs?.getTabProps(props.value, props.disabled);
16+
const active = tabs?.isItemActive(props.value) ?? false;
1617

17-
const handleClick = React.useCallback(
18-
(event: React.MouseEvent<HTMLButtonElement>) => {
19-
tabs?.onTabClick(event, props.value);
20-
},
21-
[tabs, props.value]
22-
);
23-
24-
const handleFocus = React.useCallback(
25-
(event: React.FocusEvent<HTMLButtonElement>) => {
26-
tabs?.onTabFocus(event, props.value);
27-
},
28-
[tabs, props.value]
29-
);
30-
31-
const handleKeyDown = React.useCallback(
32-
(event: React.KeyboardEvent<HTMLButtonElement>) => {
33-
tabs?.onTabKeyDown(event, props.value);
34-
},
35-
[tabs, props.value]
36-
);
37-
38-
const tabProps = {
39-
role: 'tab' as const,
40-
tabIndex: active ? tabs?.props.tabIndex : -1,
41-
ref: active ? tabs?.activeTabRef : undefined,
42-
disabled: props.disabled,
43-
'aria-selected': active,
44-
'aria-disabled': props.disabled,
45-
'aria-controls': `${tabs?.id}_tabpanel_${props.value}`,
46-
...(active && { 'data-active': '' }),
47-
...(props.disabled && { 'data-disabled': '' }),
48-
...tabs?.scrollStateAttrs,
49-
onClick: handleClick,
50-
onFocus: handleFocus,
51-
onKeyDown: handleKeyDown
52-
};
53-
54-
return { tabs, active, handleClick, handleFocus, handleKeyDown, tabProps };
18+
return { tabs, active, tabProps };
5519
},
5620
render(instance) {
5721
const { props, ptmi, tabs, active, tabProps } = instance;
5822

59-
const rootProps = mergeProps(tabProps, { className: tabs?.cx('tab', { active, disabled: props.disabled }) }, ptmi('tab'), tabs?.ptm('tab'));
23+
const rootProps = mergeProps(
24+
tabProps,
25+
{
26+
className: tabs?.cx('tab', { active, disabled: props.disabled })
27+
},
28+
ptmi('tab'),
29+
tabs?.ptm('tab')
30+
);
6031

6132
return <Component instance={instance} attrs={rootProps} children={props.children} />;
6233
}

0 commit comments

Comments
 (0)