Skip to content

Commit c22b99b

Browse files
authored
Merge pull request #2254 from dxc-technology/jialecl/tabs-active-scroll
Old props removed and adding scroll to active tab in responsive mode
2 parents 99a5c3c + 75349b3 commit c22b99b

File tree

4 files changed

+88
-120
lines changed

4 files changed

+88
-120
lines changed

packages/lib/src/tabs/Tabs.accessibility.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { render } from "@testing-library/react";
22
import { axe } from "../../test/accessibility/axe-helper";
33
import DxcTabs from "./Tabs";
44

5+
(global as any).ResizeObserver = class ResizeObserver {
6+
observe() {}
7+
unobserve() {}
8+
disconnect() {}
9+
};
10+
511
const iconSVG = (
612
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" height="20" width="20" fill="currentColor">
713
<path d="m10 17-1.042-.938q-2.083-1.854-3.437-3.177-1.354-1.323-2.136-2.354Q2.604 9.5 2.302 8.646 2 7.792 2 6.896q0-1.854 1.271-3.125T6.396 2.5q1.021 0 1.979.438.958.437 1.625 1.229.667-.792 1.625-1.229.958-.438 1.979-.438 1.854 0 3.125 1.271T18 6.896q0 .896-.292 1.729-.291.833-1.073 1.854-.781 1.021-2.145 2.365-1.365 1.344-3.49 3.26Zm0-2.021q1.938-1.729 3.188-2.948 1.25-1.219 1.989-2.125.74-.906 1.031-1.614.292-.709.292-1.396 0-1.229-.833-2.063Q14.833 4 13.604 4q-.729 0-1.364.302-.636.302-1.094.844L10.417 6h-.834l-.729-.854q-.458-.542-1.114-.844Q7.083 4 6.396 4q-1.229 0-2.063.833-.833.834-.833 2.063 0 .687.271 1.364.271.678.989 1.573.719.896 1.98 2.125Q8 13.188 10 14.979Zm0-5.5Z" />

packages/lib/src/tabs/Tabs.stories.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const tabs = (margin?: Space | Margin) => (
3636
<DxcTabs.Tab label="Tab 4">
3737
<></>
3838
</DxcTabs.Tab>
39-
<DxcTabs.Tab label="Tab 5">
39+
<DxcTabs.Tab label="Tab 5" title="test tooltip 5">
4040
<></>
4141
</DxcTabs.Tab>
4242
<DxcTabs.Tab label="Tab 6">
@@ -281,6 +281,36 @@ const Scroll = () => (
281281
</>
282282
);
283283

284+
const ResponsiveFocused = () => (
285+
<>
286+
<ExampleContainer>
287+
<DxcTabs>
288+
<DxcTabs.Tab label="Tab 1" title="test tooltip">
289+
<></>
290+
</DxcTabs.Tab>
291+
<DxcTabs.Tab label="Tab 2">
292+
<></>
293+
</DxcTabs.Tab>
294+
<DxcTabs.Tab label="Tab 3" disabled>
295+
<></>
296+
</DxcTabs.Tab>
297+
<DxcTabs.Tab label="Tab 4">
298+
<></>
299+
</DxcTabs.Tab>
300+
<DxcTabs.Tab label="Tab 5" title="test tooltip 5">
301+
<></>
302+
</DxcTabs.Tab>
303+
<DxcTabs.Tab label="Tab 6">
304+
<></>
305+
</DxcTabs.Tab>
306+
<DxcTabs.Tab label="Tab 7" defaultActive>
307+
<></>
308+
</DxcTabs.Tab>
309+
</DxcTabs>
310+
</ExampleContainer>
311+
</>
312+
);
313+
284314
type Story = StoryObj<typeof DxcTabs>;
285315

286316
export const Chromatic: Story = {
@@ -301,3 +331,13 @@ export const ScrollableTabs: Story = {
301331
chromatic: { viewports: [375], delay: 5000 },
302332
},
303333
};
334+
335+
export const ResponsiveFocusedTabs: Story = {
336+
render: ResponsiveFocused,
337+
parameters: {
338+
viewport: {
339+
defaultViewport: "iphonex",
340+
},
341+
chromatic: { viewports: [375], delay: 5000 },
342+
},
343+
};

packages/lib/src/tabs/Tabs.tsx

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
KeyboardEvent,
55
ReactElement,
66
useContext,
7+
useEffect,
78
useLayoutEffect,
89
useMemo,
910
useRef,
@@ -81,26 +82,13 @@ const TabsContent = styled.div`
8182
const ScrollableTabsList = styled.div<{
8283
enabled: boolean;
8384
iconPosition: TabsPropsType["iconPosition"];
84-
translateScroll: number;
8585
}>`
8686
display: flex;
87-
${({ enabled, translateScroll }) =>
88-
enabled ? `transform: translateX(${translateScroll}px)` : "transform: translateX(0px)"};
8987
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
9088
height: ${({ iconPosition }) => (iconPosition === "top" ? "72px" : "var(--height-xxl)")};
9189
`;
9290

93-
const DxcTabs = ({
94-
activeTabIndex,
95-
children,
96-
defaultActiveTabIndex,
97-
iconPosition = "left",
98-
margin,
99-
onTabClick,
100-
onTabHover,
101-
tabIndex = 0,
102-
tabs,
103-
}: TabsPropsType) => {
91+
const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: TabsPropsType) => {
10492
const childrenArray: ReactElement<TabProps>[] = useMemo(
10593
() => Children.toArray(children) as ReactElement<TabProps>[],
10694
[children]
@@ -117,12 +105,11 @@ const DxcTabs = ({
117105

118106
return isValidElement(initialActiveTab) ? (initialActiveTab.props.label ?? initialActiveTab.props.tabId) : "";
119107
});
120-
const [countClick, setCountClick] = useState(0);
121108
const [innerFocusIndex, setInnerFocusIndex] = useState<number | null>(null);
122109
const [scrollLeftEnabled, setScrollLeftEnabled] = useState(false);
123110
const [scrollRightEnabled, setScrollRightEnabled] = useState(true);
124-
const [translateScroll, setTranslateScroll] = useState(0);
125111
const [totalTabsWidth, setTotalTabsWidth] = useState(0);
112+
const refTabListContainer = useRef<HTMLDivElement | null>(null);
126113
const refTabList = useRef<HTMLDivElement | null>(null);
127114
const translatedLabels = useContext(HalstackLanguageContext);
128115
const viewWidth = useWidth(refTabList.current);
@@ -138,52 +125,51 @@ const DxcTabs = ({
138125
};
139126
}, [activeTabId, childrenArray, iconPosition, innerFocusIndex, tabIndex]);
140127

128+
const scrollLimitCheck = () => {
129+
const container = refTabListContainer.current;
130+
if (container) {
131+
const currentScroll = container.scrollLeft;
132+
const scrollingLength = container.scrollWidth - container.offsetWidth;
133+
const startingScroll = currentScroll <= 1;
134+
const endScroll = currentScroll >= scrollingLength - 1;
135+
136+
setScrollLeftEnabled(!startingScroll);
137+
setScrollRightEnabled(!endScroll);
138+
}
139+
};
140+
141141
const scrollLeft = () => {
142-
const offsetHeight = refTabList?.current?.offsetHeight ?? 0;
143-
let moveX = 0;
144-
if (countClick <= offsetHeight) {
145-
moveX = 0;
146-
setScrollLeftEnabled(false);
147-
setScrollRightEnabled(true);
148-
} else {
149-
moveX = countClick - offsetHeight * 2;
150-
setScrollRightEnabled(true);
151-
setScrollLeftEnabled(true);
142+
if (refTabListContainer.current) {
143+
refTabListContainer.current.scrollLeft -= 100;
144+
scrollLimitCheck();
152145
}
153-
setTranslateScroll(-moveX);
154-
setCountClick(moveX);
155146
};
156147

157148
const scrollRight = () => {
158-
const offsetHeight = refTabList?.current?.offsetHeight ?? 0;
159-
let moveX = 0;
160-
if (countClick + offsetHeight >= totalTabsWidth) {
161-
moveX = totalTabsWidth - offsetHeight;
162-
setScrollRightEnabled(false);
163-
setScrollLeftEnabled(true);
164-
} else {
165-
moveX = countClick + offsetHeight * 2;
166-
setScrollLeftEnabled(true);
167-
setScrollRightEnabled(true);
149+
if (refTabListContainer.current) {
150+
refTabListContainer.current.scrollLeft += 100;
151+
scrollLimitCheck();
168152
}
169-
setTranslateScroll(-moveX);
170-
setCountClick(moveX);
171153
};
172154

173155
const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
174156
const activeTab = childrenArray.findIndex(
175157
(child: ReactElement) => (child.props.label ?? child.props.tabId) === activeTabId
176158
);
159+
let index;
177160
switch (event.key) {
178161
case "Left":
179162
case "ArrowLeft":
180163
event.preventDefault();
181-
setInnerFocusIndex(getPreviousTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex));
164+
index = getPreviousTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex);
165+
setInnerFocusIndex(index);
166+
182167
break;
183168
case "Right":
184169
case "ArrowRight":
185170
event.preventDefault();
186-
setInnerFocusIndex(getNextTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex));
171+
index = getNextTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex);
172+
setInnerFocusIndex(index);
187173
break;
188174
case "Tab":
189175
if (activeTab !== innerFocusIndex) {
@@ -193,18 +179,25 @@ const DxcTabs = ({
193179
default:
194180
break;
195181
}
182+
setTimeout(() => {
183+
scrollLimitCheck();
184+
}, 0);
196185
};
197186

198-
useLayoutEffect(() => {
187+
useEffect(() => {
199188
if (refTabList.current)
200189
setTotalTabsWidth(() => {
201190
let total = 0;
202-
refTabList.current?.querySelectorAll('[role="tab"]').forEach((tab) => {
191+
refTabList.current?.querySelectorAll('[role="tab"]').forEach((tab, index) => {
192+
if (tab.ariaSelected === "true" && viewWidth && viewWidth < totalTabsWidth) {
193+
setInnerFocusIndex(index);
194+
}
203195
total += (tab as HTMLElement).offsetWidth;
204196
});
205197
return total;
206198
});
207-
}, []);
199+
scrollLimitCheck();
200+
}, [viewWidth, totalTabsWidth]);
208201

209202
return (
210203
<>
@@ -221,14 +214,13 @@ const DxcTabs = ({
221214
<DxcIcon icon="keyboard_arrow_left" />
222215
</ScrollIndicatorButton>
223216
)}
224-
<TabsContent>
217+
<TabsContent ref={refTabListContainer}>
225218
<ScrollableTabsList
226219
enabled={viewWidth < totalTabsWidth}
227220
iconPosition={iconPosition}
228221
onKeyDown={handleOnKeyDown}
229222
ref={refTabList}
230223
role="tablist"
231-
translateScroll={translateScroll}
232224
>
233225
<TabsContext.Provider value={contextValue}>{children}</TabsContext.Provider>
234226
</ScrollableTabsList>

packages/lib/src/tabs/types.ts

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,6 @@ import { ReactNode } from "react";
22

33
import type { Space, Margin, SVG } from "../common/utils";
44

5-
type TabCommonProps = {
6-
/**
7-
* Whether the tab is disabled or not.
8-
*/
9-
isDisabled?: boolean;
10-
/**
11-
* If the value is 'true', an empty badge will appear.
12-
* If it is 'false', no badge will appear.
13-
* If a number is put it will be shown as the label of the notification
14-
* in the tab, taking into account that if that number is greater than 99,
15-
* it will appear as '+99' in the badge.
16-
*/
17-
notificationNumber?: boolean | number;
18-
};
19-
205
export type TabsContextProps = {
216
activeTabId?: string;
227
focusedTabId?: string;
@@ -48,17 +33,6 @@ export type TabIconProps = {
4833
icon: string | SVG;
4934
};
5035

51-
export type TabPropsLegacy = {
52-
tab: TabCommonProps & (TabLabelProps | TabIconProps);
53-
active: boolean;
54-
tabIndex: number;
55-
hasLabelAndIcon: boolean;
56-
iconPosition: "top" | "left";
57-
onClick: () => void;
58-
onMouseEnter: () => void;
59-
onMouseLeave: () => void;
60-
};
61-
6236
export type TabProps = {
6337
defaultActive?: boolean;
6438
active?: boolean;
@@ -71,51 +45,7 @@ export type TabProps = {
7145
onHover?: () => void;
7246
} & (TabLabelProps | TabIconProps);
7347

74-
type LegacyProps = {
75-
/**
76-
* @deprecated This prop is deprecated and will be removed in future versions. Use the children prop instead.
77-
* The index of the active tab. If undefined, the component will be
78-
* uncontrolled and the active tab will be managed internally by the component.
79-
*/
80-
activeTabIndex?: number;
81-
/**
82-
* @deprecated This prop is deprecated and will be removed in future versions.
83-
* Initially active tab, only when it is uncontrolled.
84-
*/
85-
defaultActiveTabIndex?: number;
86-
/**
87-
* Whether the icon should appear above or to the left of the label.
88-
*/
89-
iconPosition?: "top" | "left";
90-
/**
91-
* Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge').
92-
* You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes.
93-
*/
94-
margin?: Space | Margin;
95-
/**
96-
* @deprecated This prop is deprecated and will be removed in future versions.
97-
* This function will be called when the user clicks on a tab. The index of the
98-
* clicked tab will be passed as a parameter.
99-
*/
100-
onTabClick?: (index: number) => void;
101-
/**
102-
* @deprecated This prop is deprecated and will be removed in future versions.
103-
* This function will be called when the user hovers a tab.The index of the
104-
* hovered tab will be passed as a parameter.
105-
*/
106-
onTabHover?: (index: number | null) => void;
107-
/**
108-
* Value of the tabindex attribute applied to each tab.
109-
*/
110-
tabIndex?: number;
111-
/**
112-
* @deprecated This prop is deprecated and will be removed in future versions.
113-
* An array of objects representing the tabs.
114-
*/
115-
tabs?: (TabCommonProps & (TabLabelProps | TabIconProps))[];
116-
};
117-
118-
type NewProps = {
48+
type TabsProps = {
11949
/**
12050
* Whether the icon should appear above or to the left of the label.
12151
*/
@@ -135,6 +65,6 @@ type NewProps = {
13565
children?: ReactNode;
13666
};
13767

138-
type Props = LegacyProps & NewProps;
68+
type Props = TabsProps;
13969

14070
export default Props;

0 commit comments

Comments
 (0)