diff --git a/packages/lib/src/tabs/Tabs.accessibility.test.tsx b/packages/lib/src/tabs/Tabs.accessibility.test.tsx index 33f6d052c..27a8f562e 100644 --- a/packages/lib/src/tabs/Tabs.accessibility.test.tsx +++ b/packages/lib/src/tabs/Tabs.accessibility.test.tsx @@ -2,6 +2,12 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcTabs from "./Tabs"; +(global as any).ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + const iconSVG = ( diff --git a/packages/lib/src/tabs/Tabs.stories.tsx b/packages/lib/src/tabs/Tabs.stories.tsx index 31c4522a2..a27c8ca70 100644 --- a/packages/lib/src/tabs/Tabs.stories.tsx +++ b/packages/lib/src/tabs/Tabs.stories.tsx @@ -36,7 +36,7 @@ const tabs = (margin?: Space | Margin) => ( <> - + <> @@ -281,6 +281,36 @@ const Scroll = () => ( ); +const ResponsiveFocused = () => ( + <> + + + + <> + + + <> + + + <> + + + <> + + + <> + + + <> + + + <> + + + + +); + type Story = StoryObj; export const Chromatic: Story = { @@ -301,3 +331,13 @@ export const ScrollableTabs: Story = { chromatic: { viewports: [375], delay: 5000 }, }, }; + +export const ResponsiveFocusedTabs: Story = { + render: ResponsiveFocused, + parameters: { + viewport: { + defaultViewport: "iphonex", + }, + chromatic: { viewports: [375], delay: 5000 }, + }, +}; diff --git a/packages/lib/src/tabs/Tabs.tsx b/packages/lib/src/tabs/Tabs.tsx index 556f3df9f..3d6d3dce1 100644 --- a/packages/lib/src/tabs/Tabs.tsx +++ b/packages/lib/src/tabs/Tabs.tsx @@ -4,6 +4,7 @@ import { KeyboardEvent, ReactElement, useContext, + useEffect, useLayoutEffect, useMemo, useRef, @@ -81,26 +82,13 @@ const TabsContent = styled.div` const ScrollableTabsList = styled.div<{ enabled: boolean; iconPosition: TabsPropsType["iconPosition"]; - translateScroll: number; }>` display: flex; - ${({ enabled, translateScroll }) => - enabled ? `transform: translateX(${translateScroll}px)` : "transform: translateX(0px)"}; transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; height: ${({ iconPosition }) => (iconPosition === "top" ? "72px" : "var(--height-xxl)")}; `; -const DxcTabs = ({ - activeTabIndex, - children, - defaultActiveTabIndex, - iconPosition = "left", - margin, - onTabClick, - onTabHover, - tabIndex = 0, - tabs, -}: TabsPropsType) => { +const DxcTabs = ({ children, iconPosition = "left", margin, tabIndex = 0 }: TabsPropsType) => { const childrenArray: ReactElement[] = useMemo( () => Children.toArray(children) as ReactElement[], [children] @@ -117,12 +105,11 @@ const DxcTabs = ({ return isValidElement(initialActiveTab) ? (initialActiveTab.props.label ?? initialActiveTab.props.tabId) : ""; }); - const [countClick, setCountClick] = useState(0); const [innerFocusIndex, setInnerFocusIndex] = useState(null); const [scrollLeftEnabled, setScrollLeftEnabled] = useState(false); const [scrollRightEnabled, setScrollRightEnabled] = useState(true); - const [translateScroll, setTranslateScroll] = useState(0); const [totalTabsWidth, setTotalTabsWidth] = useState(0); + const refTabListContainer = useRef(null); const refTabList = useRef(null); const translatedLabels = useContext(HalstackLanguageContext); const viewWidth = useWidth(refTabList.current); @@ -138,52 +125,51 @@ const DxcTabs = ({ }; }, [activeTabId, childrenArray, iconPosition, innerFocusIndex, tabIndex]); + const scrollLimitCheck = () => { + const container = refTabListContainer.current; + if (container) { + const currentScroll = container.scrollLeft; + const scrollingLength = container.scrollWidth - container.offsetWidth; + const startingScroll = currentScroll <= 1; + const endScroll = currentScroll >= scrollingLength - 1; + + setScrollLeftEnabled(!startingScroll); + setScrollRightEnabled(!endScroll); + } + }; + const scrollLeft = () => { - const offsetHeight = refTabList?.current?.offsetHeight ?? 0; - let moveX = 0; - if (countClick <= offsetHeight) { - moveX = 0; - setScrollLeftEnabled(false); - setScrollRightEnabled(true); - } else { - moveX = countClick - offsetHeight * 2; - setScrollRightEnabled(true); - setScrollLeftEnabled(true); + if (refTabListContainer.current) { + refTabListContainer.current.scrollLeft -= 100; + scrollLimitCheck(); } - setTranslateScroll(-moveX); - setCountClick(moveX); }; const scrollRight = () => { - const offsetHeight = refTabList?.current?.offsetHeight ?? 0; - let moveX = 0; - if (countClick + offsetHeight >= totalTabsWidth) { - moveX = totalTabsWidth - offsetHeight; - setScrollRightEnabled(false); - setScrollLeftEnabled(true); - } else { - moveX = countClick + offsetHeight * 2; - setScrollLeftEnabled(true); - setScrollRightEnabled(true); + if (refTabListContainer.current) { + refTabListContainer.current.scrollLeft += 100; + scrollLimitCheck(); } - setTranslateScroll(-moveX); - setCountClick(moveX); }; const handleOnKeyDown = (event: KeyboardEvent) => { const activeTab = childrenArray.findIndex( (child: ReactElement) => (child.props.label ?? child.props.tabId) === activeTabId ); + let index; switch (event.key) { case "Left": case "ArrowLeft": event.preventDefault(); - setInnerFocusIndex(getPreviousTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex)); + index = getPreviousTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex); + setInnerFocusIndex(index); + break; case "Right": case "ArrowRight": event.preventDefault(); - setInnerFocusIndex(getNextTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex)); + index = getNextTabIndex(childrenArray, innerFocusIndex === null ? activeTab : innerFocusIndex); + setInnerFocusIndex(index); break; case "Tab": if (activeTab !== innerFocusIndex) { @@ -193,18 +179,25 @@ const DxcTabs = ({ default: break; } + setTimeout(() => { + scrollLimitCheck(); + }, 0); }; - useLayoutEffect(() => { + useEffect(() => { if (refTabList.current) setTotalTabsWidth(() => { let total = 0; - refTabList.current?.querySelectorAll('[role="tab"]').forEach((tab) => { + refTabList.current?.querySelectorAll('[role="tab"]').forEach((tab, index) => { + if (tab.ariaSelected === "true" && viewWidth && viewWidth < totalTabsWidth) { + setInnerFocusIndex(index); + } total += (tab as HTMLElement).offsetWidth; }); return total; }); - }, []); + scrollLimitCheck(); + }, [viewWidth, totalTabsWidth]); return ( <> @@ -221,14 +214,13 @@ const DxcTabs = ({ )} - + {children} diff --git a/packages/lib/src/tabs/types.ts b/packages/lib/src/tabs/types.ts index e940f2b70..b3d4578de 100644 --- a/packages/lib/src/tabs/types.ts +++ b/packages/lib/src/tabs/types.ts @@ -2,21 +2,6 @@ import { ReactNode } from "react"; import type { Space, Margin, SVG } from "../common/utils"; -type TabCommonProps = { - /** - * Whether the tab is disabled or not. - */ - isDisabled?: boolean; - /** - * If the value is 'true', an empty badge will appear. - * If it is 'false', no badge will appear. - * If a number is put it will be shown as the label of the notification - * in the tab, taking into account that if that number is greater than 99, - * it will appear as '+99' in the badge. - */ - notificationNumber?: boolean | number; -}; - export type TabsContextProps = { activeTabId?: string; focusedTabId?: string; @@ -48,17 +33,6 @@ export type TabIconProps = { icon: string | SVG; }; -export type TabPropsLegacy = { - tab: TabCommonProps & (TabLabelProps | TabIconProps); - active: boolean; - tabIndex: number; - hasLabelAndIcon: boolean; - iconPosition: "top" | "left"; - onClick: () => void; - onMouseEnter: () => void; - onMouseLeave: () => void; -}; - export type TabProps = { defaultActive?: boolean; active?: boolean; @@ -71,51 +45,7 @@ export type TabProps = { onHover?: () => void; } & (TabLabelProps | TabIconProps); -type LegacyProps = { - /** - * @deprecated This prop is deprecated and will be removed in future versions. Use the children prop instead. - * The index of the active tab. If undefined, the component will be - * uncontrolled and the active tab will be managed internally by the component. - */ - activeTabIndex?: number; - /** - * @deprecated This prop is deprecated and will be removed in future versions. - * Initially active tab, only when it is uncontrolled. - */ - defaultActiveTabIndex?: number; - /** - * Whether the icon should appear above or to the left of the label. - */ - iconPosition?: "top" | "left"; - /** - * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). - * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. - */ - margin?: Space | Margin; - /** - * @deprecated This prop is deprecated and will be removed in future versions. - * This function will be called when the user clicks on a tab. The index of the - * clicked tab will be passed as a parameter. - */ - onTabClick?: (index: number) => void; - /** - * @deprecated This prop is deprecated and will be removed in future versions. - * This function will be called when the user hovers a tab.The index of the - * hovered tab will be passed as a parameter. - */ - onTabHover?: (index: number | null) => void; - /** - * Value of the tabindex attribute applied to each tab. - */ - tabIndex?: number; - /** - * @deprecated This prop is deprecated and will be removed in future versions. - * An array of objects representing the tabs. - */ - tabs?: (TabCommonProps & (TabLabelProps | TabIconProps))[]; -}; - -type NewProps = { +type TabsProps = { /** * Whether the icon should appear above or to the left of the label. */ @@ -135,6 +65,6 @@ type NewProps = { children?: ReactNode; }; -type Props = LegacyProps & NewProps; +type Props = TabsProps; export default Props;