diff --git a/packages/lib/src/tabs/Tab.tsx b/packages/lib/src/tabs/Tab.tsx index 1b5fabf00..727de2939 100644 --- a/packages/lib/src/tabs/Tab.tsx +++ b/packages/lib/src/tabs/Tab.tsx @@ -94,7 +94,7 @@ const Underline = styled.span<{ active: boolean }>` const DxcTab = forwardRef( ( - { active, disabled, icon, label, notificationNumber, onClick, onHover, title, tabId }: TabProps, + { active, disabled, icon, label, notificationNumber, onClick, onHover, title, tabId = label }: TabProps, ref: Ref ) => { const { @@ -124,7 +124,7 @@ const DxcTab = forwardRef( }, [focusedTabId, tabId]); useEffect(() => { - if (active) setActiveTabId?.(tabId); + if (active) setActiveTabId?.(tabId ?? ""); }, [active, tabId, setActiveTabId]); return ( @@ -135,7 +135,9 @@ const DxcTab = forwardRef( hasLabelAndIcon={Boolean(icon && label)} iconPosition={iconPosition} onClick={() => { - if (!isControlled) setActiveTabId?.(tabId); + if (!isControlled) { + setActiveTabId?.(tabId ?? ""); + } onClick?.(); }} onKeyDown={handleOnKeyDown} @@ -153,10 +155,11 @@ const DxcTab = forwardRef( role="tab" tabIndex={activeTabId === label && !disabled ? tabIndex : -1} type="button" + aria-label={label ?? tabId ?? "tab"} > {icon && {typeof icon === "string" ? : icon}} - + {label && } {!disabled && notificationNumber && ( diff --git a/packages/lib/src/tabs/Tabs.stories.tsx b/packages/lib/src/tabs/Tabs.stories.tsx index 656de288a..31c4522a2 100644 --- a/packages/lib/src/tabs/Tabs.stories.tsx +++ b/packages/lib/src/tabs/Tabs.stories.tsx @@ -24,25 +24,25 @@ const iconSVG = ( const tabs = (margin?: Space | Margin) => ( - + <> - + <> - + <> - + <> - + <> - + <> - + <> @@ -50,13 +50,13 @@ const tabs = (margin?: Space | Margin) => ( const disabledTabs = ( - + <> - + <> - + <> @@ -64,13 +64,13 @@ const disabledTabs = ( const firstDisabledTabs = ( - + <> - + <> - + <> @@ -78,25 +78,25 @@ const firstDisabledTabs = ( const tabsNotification = (iconPosition?: "top" | "left") => ( - + <> - + <> - + <> - + <> - + <> - + <> - + <> @@ -104,25 +104,51 @@ const tabsNotification = (iconPosition?: "top" | "left") => ( const tabsIcon = (iconPosition?: "top" | "left") => ( - + <> - + <> - + <> - + <> - + <> - + <> - + + <> + + +); + +const tabsIconLabel = (iconPosition?: "top" | "left") => ( + + + <> + + + <> + + + <> + + + <> + + + <> + + + <> + + <> @@ -130,25 +156,25 @@ const tabsIcon = (iconPosition?: "top" | "left") => ( const tabsNotificationIcon = (iconPosition?: "top" | "left") => ( - + <> - + <> - + <> - + <> - + <> - + <> - + <> @@ -187,10 +213,12 @@ const Tabs = () => ( {tabsIcon()} + {tabsIconLabel()} </ExampleContainer> <ExampleContainer> <Title title="With icon position top" theme="light" level={4} /> {tabsIcon("top")} + {tabsIconLabel("top")} </ExampleContainer> <ExampleContainer> <Title title="With icon and notification number" theme="light" level={4} /> diff --git a/packages/lib/src/tabs/Tabs.test.tsx b/packages/lib/src/tabs/Tabs.test.tsx index 30ded3b65..6a2e59658 100644 --- a/packages/lib/src/tabs/Tabs.test.tsx +++ b/packages/lib/src/tabs/Tabs.test.tsx @@ -10,26 +10,26 @@ import DxcTabs from "./Tabs"; const sampleTabs = ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" notificationNumber={10} defaultActive> + <DxcTabs.Tab label="Tab-1" notificationNumber={10} defaultActive> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" notificationNumber={20}> + <DxcTabs.Tab label="Tab-2" notificationNumber={20}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" notificationNumber={30}> + <DxcTabs.Tab label="Tab-3" notificationNumber={30}> <></> </DxcTabs.Tab> </DxcTabs> ); const sampleTabsWithBadge = ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" notificationNumber={10}> + <DxcTabs.Tab label="Tab-1" notificationNumber={10}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" notificationNumber={20}> + <DxcTabs.Tab label="Tab-2" notificationNumber={20}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" notificationNumber={101} defaultActive> + <DxcTabs.Tab label="Tab-3" notificationNumber={101} defaultActive> <></> </DxcTabs.Tab> </DxcTabs> @@ -37,36 +37,36 @@ const sampleTabsWithBadge = ( const sampleTabsFirstDisabled = ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" disabled> + <DxcTabs.Tab label="Tab-1" disabled> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2"> + <DxcTabs.Tab label="Tab-2"> <></> </DxcTabs.Tab> </DxcTabs> ); const sampleTabsInteraction = (onTabClick: (() => void)[]) => ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" onClick={onTabClick[0]} defaultActive> + <DxcTabs.Tab label="Tab-1" onClick={onTabClick[0]} defaultActive> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" onClick={onTabClick[1]}> + <DxcTabs.Tab label="Tab-2" onClick={onTabClick[1]}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" onClick={onTabClick[2]}> + <DxcTabs.Tab label="Tab-3" onClick={onTabClick[2]}> <></> </DxcTabs.Tab> </DxcTabs> ); const sampleTabsMiddleDisabled = (onTabClick: (() => void)[]) => ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" onClick={onTabClick[0]}> + <DxcTabs.Tab label="Tab-1" onClick={onTabClick[0]}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" onClick={onTabClick[1]} disabled> + <DxcTabs.Tab label="Tab-2" onClick={onTabClick[1]} disabled> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" onClick={onTabClick[2]}> + <DxcTabs.Tab label="Tab-3" onClick={onTabClick[2]}> <></> </DxcTabs.Tab> </DxcTabs> @@ -74,13 +74,13 @@ const sampleTabsMiddleDisabled = (onTabClick: (() => void)[]) => ( const sampleTabsLastTabNonDisabledFirstActive = (onTabClick: (() => void)[]) => ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" onClick={onTabClick[0]} disabled defaultActive> + <DxcTabs.Tab label="Tab-1" onClick={onTabClick[0]} disabled defaultActive> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" onClick={onTabClick[1]} disabled> + <DxcTabs.Tab label="Tab-2" onClick={onTabClick[1]} disabled> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" onClick={onTabClick[2]}> + <DxcTabs.Tab label="Tab-3" onClick={onTabClick[2]}> <></> </DxcTabs.Tab> </DxcTabs> @@ -88,13 +88,27 @@ const sampleTabsLastTabNonDisabledFirstActive = (onTabClick: (() => void)[]) => const sampleControlledTabsInteraction = (onTabClick: (() => void)[]) => ( <DxcTabs> - <DxcTabs.Tab tabId="Tab-1" label="Tab-1" onClick={onTabClick[0]} active> + <DxcTabs.Tab label="Tab-1" onClick={onTabClick[0]} active> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-2" label="Tab-2" onClick={onTabClick[1]}> + <DxcTabs.Tab label="Tab-2" onClick={onTabClick[1]}> <></> </DxcTabs.Tab> - <DxcTabs.Tab tabId="Tab-3" label="Tab-3" onClick={onTabClick[2]}> + <DxcTabs.Tab label="Tab-3" onClick={onTabClick[2]}> + <></> + </DxcTabs.Tab> + </DxcTabs> +); + +const sampleTabsWithoutLabel = (onTabClick: (() => void)[]) => ( + <DxcTabs> + <DxcTabs.Tab tabId="Tab 1" icon="api" onClick={onTabClick[0]}> + <></> + </DxcTabs.Tab> + <DxcTabs.Tab tabId="Tab 2" icon="api" onClick={onTabClick[1]}> + <></> + </DxcTabs.Tab> + <DxcTabs.Tab tabId="Tab 3" icon="api" onClick={onTabClick[2]}> <></> </DxcTabs.Tab> </DxcTabs> @@ -243,6 +257,7 @@ describe("Tabs component tests", () => { expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); expect(onTabClick[2]).toHaveBeenCalled(); }); + test("Controlled tabs interaction", () => { const onTabClick = [jest.fn(), jest.fn(), jest.fn()]; const { getAllByRole } = render(sampleControlledTabsInteraction(onTabClick)); @@ -263,4 +278,25 @@ describe("Tabs component tests", () => { expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); }); + + test("Tabs without label interaction", () => { + const onTabClick = [jest.fn(), jest.fn(), jest.fn()]; + const { getAllByRole } = render(sampleTabsWithoutLabel(onTabClick)); + const tabs = getAllByRole("tab"); + tabs[0] && fireEvent.click(tabs[0]); + expect(onTabClick[0]).toHaveBeenCalled(); + expect(tabs[0]?.getAttribute("aria-selected")).toBe("true"); + expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); + expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); + tabs[1] && fireEvent.click(tabs[1]); + expect(onTabClick[1]).toHaveBeenCalled(); + expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); + expect(tabs[1]?.getAttribute("aria-selected")).toBe("true"); + expect(tabs[2]?.getAttribute("aria-selected")).toBe("false"); + tabs[2] && fireEvent.click(tabs[2]); + expect(onTabClick[2]).toHaveBeenCalled(); + expect(tabs[0]?.getAttribute("aria-selected")).toBe("false"); + expect(tabs[1]?.getAttribute("aria-selected")).toBe("false"); + expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); + }); }); diff --git a/packages/lib/src/tabs/Tabs.tsx b/packages/lib/src/tabs/Tabs.tsx index 3404e6aeb..e8f73e353 100644 --- a/packages/lib/src/tabs/Tabs.tsx +++ b/packages/lib/src/tabs/Tabs.tsx @@ -116,7 +116,7 @@ const DxcTabs = ({ ) : childrenArray.find((child) => isValidElement(child) && !child.props.disabled); - return isValidElement(initialActiveTab) ? initialActiveTab.props.tabId : ""; + return isValidElement(initialActiveTab) ? (initialActiveTab.props.label ?? initialActiveTab.props.tabId) : ""; }); const [countClick, setCountClick] = useState(0); const [innerFocusIndex, setInnerFocusIndex] = useState<number | null>(null); @@ -131,7 +131,7 @@ const DxcTabs = ({ const focusedChild = innerFocusIndex != null ? childrenArray[innerFocusIndex] : null; return { activeTabId: activeTabId, - focusedTabId: isValidElement(focusedChild) ? focusedChild.props.tabId : "", + focusedTabId: isValidElement(focusedChild) ? (focusedChild.props.label ?? focusedChild.props.tabId) : "", iconPosition, isControlled: childrenArray.some((child) => isValidElement(child) && typeof child.props.active !== "undefined"), setActiveTabId: setActiveTabId, @@ -172,7 +172,9 @@ const DxcTabs = ({ }; const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { - const activeTab = childrenArray.findIndex((child: ReactElement) => child.props.tabId === activeTabId); + const activeTab = childrenArray.findIndex( + (child: ReactElement) => (child.props.label ?? child.props.tabId) === activeTabId + ); switch (event.key) { case "Left": case "ArrowLeft": diff --git a/packages/lib/src/tabs/types.ts b/packages/lib/src/tabs/types.ts index 0f0157e5b..e940f2b70 100644 --- a/packages/lib/src/tabs/types.ts +++ b/packages/lib/src/tabs/types.ts @@ -18,8 +18,8 @@ type TabCommonProps = { }; export type TabsContextProps = { - activeTabId: string; - focusedTabId: string; + activeTabId?: string; + focusedTabId?: string; iconPosition?: "top" | "left"; isControlled: boolean; setActiveTabId: (_tab: string) => void; @@ -63,7 +63,7 @@ export type TabProps = { defaultActive?: boolean; active?: boolean; title?: string; - tabId: string; + tabId?: string; disabled?: boolean; notificationNumber?: boolean | number; children: ReactNode; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index a1dbd9d8d..ad4b33f44 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@dxc-technology/typescript-config/react-library.json", "compilerOptions": { + "jsx": "react-jsx", "outDir": "dist", "forceConsistentCasingInFileNames": true },