From 9a612c4239bc0c338e55a3c532b5e8ff5078ba1d Mon Sep 17 00:00:00 2001 From: Jialecl Date: Fri, 23 May 2025 11:27:38 +0200 Subject: [PATCH 1/3] `tabId` prop is no longer required --- packages/lib/src/tabs/Tab.tsx | 10 ++-- packages/lib/src/tabs/Tabs.stories.tsx | 68 +++++++++++------------ packages/lib/src/tabs/Tabs.test.tsx | 76 +++++++++++++++++++------- packages/lib/src/tabs/Tabs.tsx | 8 ++- packages/lib/src/tabs/types.ts | 6 +- packages/lib/tsconfig.json | 3 +- 6 files changed, 106 insertions(+), 65 deletions(-) diff --git a/packages/lib/src/tabs/Tab.tsx b/packages/lib/src/tabs/Tab.tsx index 1b5fabf006..762fe0832b 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} @@ -156,7 +158,7 @@ const DxcTab = forwardRef( > {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 656de288a5..1597ce830c 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,25 @@ const tabsNotification = (iconPosition?: "top" | "left") => ( const tabsIcon = (iconPosition?: "top" | "left") => ( - + <> - + <> - + <> - + <> - + <> - + <> - + <> @@ -130,25 +130,25 @@ const tabsIcon = (iconPosition?: "top" | "left") => ( const tabsNotificationIcon = (iconPosition?: "top" | "left") => ( - + <> - + <> - + <> - + <> - + <> - + <> - + <> diff --git a/packages/lib/src/tabs/Tabs.test.tsx b/packages/lib/src/tabs/Tabs.test.tsx index 30ded3b65c..6a2e59658b 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 = ( - + <> - + <> - + <> ); const sampleTabsWithBadge = ( - + <> - + <> - + <> @@ -37,36 +37,36 @@ const sampleTabsWithBadge = ( const sampleTabsFirstDisabled = ( - + <> - + <> ); const sampleTabsInteraction = (onTabClick: (() => void)[]) => ( - + <> - + <> - + <> ); const sampleTabsMiddleDisabled = (onTabClick: (() => void)[]) => ( - + <> - + <> - + <> @@ -74,13 +74,13 @@ const sampleTabsMiddleDisabled = (onTabClick: (() => void)[]) => ( const sampleTabsLastTabNonDisabledFirstActive = (onTabClick: (() => void)[]) => ( - + <> - + <> - + <> @@ -88,13 +88,27 @@ const sampleTabsLastTabNonDisabledFirstActive = (onTabClick: (() => void)[]) => const sampleControlledTabsInteraction = (onTabClick: (() => void)[]) => ( - + <> - + <> - + + <> + + +); + +const sampleTabsWithoutLabel = (onTabClick: (() => void)[]) => ( + + + <> + + + <> + + <> @@ -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 3404e6aebd..e8f73e3532 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(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) => { - 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 0f0157e5b8..e940f2b70a 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 a1dbd9d8db..8d4b4b16c3 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,6 +1,7 @@ { - "extends": "@dxc-technology/typescript-config/react-library.json", + "extends": "../typescript-config/react-library.json", "compilerOptions": { + "jsx": "react-jsx", "outDir": "dist", "forceConsistentCasingInFileNames": true }, From f068e8c7a249f03b45f7a65cfe2b7682d9e6704b Mon Sep 17 00:00:00 2001 From: Jialecl Date: Mon, 26 May 2025 10:27:26 +0200 Subject: [PATCH 2/3] Adding aria-label to tab --- packages/lib/src/tabs/Tab.tsx | 1 + packages/lib/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/tabs/Tab.tsx b/packages/lib/src/tabs/Tab.tsx index 762fe0832b..727de29394 100644 --- a/packages/lib/src/tabs/Tab.tsx +++ b/packages/lib/src/tabs/Tab.tsx @@ -155,6 +155,7 @@ const DxcTab = forwardRef( role="tab" tabIndex={activeTabId === label && !disabled ? tabIndex : -1} type="button" + aria-label={label ?? tabId ?? "tab"} > {icon && {typeof icon === "string" ? : icon}} diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 8d4b4b16c3..ad4b33f44e 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../typescript-config/react-library.json", + "extends": "@dxc-technology/typescript-config/react-library.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "dist", From 66e1efb8c9f9a6c70a6b624eed95621eecfbebc3 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Tue, 27 May 2025 10:47:16 +0200 Subject: [PATCH 3/3] Icon and label added to storybook --- packages/lib/src/tabs/Tabs.stories.tsx | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/lib/src/tabs/Tabs.stories.tsx b/packages/lib/src/tabs/Tabs.stories.tsx index 1597ce830c..31c4522a2e 100644 --- a/packages/lib/src/tabs/Tabs.stories.tsx +++ b/packages/lib/src/tabs/Tabs.stories.tsx @@ -128,6 +128,32 @@ const tabsIcon = (iconPosition?: "top" | "left") => ( ); +const tabsIconLabel = (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} />