diff --git a/apps/website/pages/components/nav-tabs/code.tsx b/apps/website/pages/components/nav-tabs/code.tsx index b6e2985afa..8a24f391f7 100644 --- a/apps/website/pages/components/nav-tabs/code.tsx +++ b/apps/website/pages/components/nav-tabs/code.tsx @@ -6,7 +6,7 @@ import NavTabsCodePage from "screens/components/nav-tabs/code/NavTabsCodePage"; const Code = () => ( <> - Nav Tabs Code — Halstack Design System + Nav tabs code — Halstack Design System diff --git a/apps/website/pages/components/nav-tabs/index.tsx b/apps/website/pages/components/nav-tabs/index.tsx index d7d3c23176..8bd45d9056 100644 --- a/apps/website/pages/components/nav-tabs/index.tsx +++ b/apps/website/pages/components/nav-tabs/index.tsx @@ -6,7 +6,7 @@ import NavTabsOverviewPage from "screens/components/nav-tabs/overview/NavTabsOve const Index = () => ( <> - Nav Tabs — Halstack Design System + Nav tabs — Halstack Design System diff --git a/apps/website/screens/components/nav-tabs/NavTabsPageLayout.tsx b/apps/website/screens/components/nav-tabs/NavTabsPageLayout.tsx index 16077c5cd3..6cd8ccf9e3 100644 --- a/apps/website/screens/components/nav-tabs/NavTabsPageLayout.tsx +++ b/apps/website/screens/components/nav-tabs/NavTabsPageLayout.tsx @@ -14,12 +14,12 @@ const NumberInputPageHeading = ({ children }: { children: ReactNode }) => { - + Nav tabs lets users switch between different views or sections within the same page, organizing related content into a clear and accessible layout. - + {children} diff --git a/apps/website/screens/components/nav-tabs/code/NavTabsCodePage.tsx b/apps/website/screens/components/nav-tabs/code/NavTabsCodePage.tsx index 20157e8204..f91c870af7 100644 --- a/apps/website/screens/components/nav-tabs/code/NavTabsCodePage.tsx +++ b/apps/website/screens/components/nav-tabs/code/NavTabsCodePage.tsx @@ -44,11 +44,11 @@ const sections = [ iconPosition - 'top' | 'left' + 'left' | 'top' Whether the icon should appear above or to the left of the label. - 'top' + 'left' @@ -263,15 +263,13 @@ const sections = [ }, ]; -const NavTabsCodePage = () => { - return ( - - - - - - - ); -}; +const NavTabsCodePage = () => ( + + + + + + +); export default NavTabsCodePage; diff --git a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx index 768921134f..73f4becbff 100644 --- a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx +++ b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx @@ -30,32 +30,30 @@ const sections = [ Nav tabs anatomy - Container: the outer wrapper that holds and organizes all tab items. It defines the overall + Container: the outer wrapper that holds and organizes all tab items. It defines the overall structure and layout of the navigation tabs, ensuring proper alignment and spacing across the component. - Label: the text displayed within each tab that indicates the section or category it leads + Label: the text displayed within each tab that indicates the section or category it leads to. - Notification badge - (Optional): a visual indicator that displays the number of pending actions, alerts or updates - related to a specific tab. + Notification badge (Optional): a visual indicator that displays the number of + pending actions, alerts or updates related to a specific tab. - Selected tab: the active tab currently in focus, representing the visible content section. + Selected tab: the active tab currently in focus, representing the visible content section. It is visually distinguished from unselected tabs using different color, weight or indicator styling. - Icon - (Optional): an optional graphical element placed before the label to visually reinforce the tab's - meaning or category. + Icon (Optional): an optional graphical element placed before the label to visually + reinforce the tab's meaning or category. - Selected tab indicator: a horizontal bar that visually marks the currently active tab. + Selected tab indicator: a horizontal bar that visually marks the currently active tab. - Unselected tab indicator: a horizontal bar that visually marks the currently inactive tab. + Unselected tab indicator: a horizontal bar that visually marks the currently inactive tab. @@ -75,7 +73,7 @@ const sections = [ Use the top position when the tabs are displayed in a horizontal layout and you want to - emphasize the icon as a key visual cue—ideal for dashboards or mobile - first interfaces where vertical + emphasize the icon as a key visual cue—ideal for dashboards or mobile-first interfaces where vertical stacking feels more natural. @@ -140,8 +138,8 @@ const sections = [ Both components improve usability, but tabs are best for grouping related content within - a page, while - nav tabs help users move across different sections or pages of an application. + a page, while nav tabs help users move across different sections or pages of an + application. ), @@ -162,17 +160,22 @@ const sections = [ especially on smaller viewports. - Ensure that tabs follow a logical order — based on frequency of use, workflow, or user + Ensure that tabs follow a logical order—based on frequency of use, workflow, or user priority. Use notification badges to highlight relevant updates only when necessary, and avoid overloading multiple tabs with badges at once. + + While the component is flexible enough to support a mix of label-only and label-with-icon tabs, it's best to + choose one style per set. Mixing both can reduce scannability and create visual imbalance, impacting the + overall usability. + Choose icon placement (left or top) based on the available space and the importance of the icon in the - context of the label. Left is preferred for horizontal layouts; top works best in vertical or space - - constrained scenarios. + context of the label. Left is preferred for horizontal layouts; top works best in vertical or + space-constrained scenarios. Avoid mixing navigation tabs and action buttons within the same group, as this can create confusion around @@ -184,15 +187,13 @@ const sections = [ }, ]; -const NavTabsOverviewPage = () => { - return ( - - - - - - - ); -}; +const NavTabsOverviewPage = () => ( + + + + + + +); export default NavTabsOverviewPage; diff --git a/apps/website/screens/components/tabs/overview/TabsOverviewPage.tsx b/apps/website/screens/components/tabs/overview/TabsOverviewPage.tsx index 6c71061f54..6963a02848 100644 --- a/apps/website/screens/components/tabs/overview/TabsOverviewPage.tsx +++ b/apps/website/screens/components/tabs/overview/TabsOverviewPage.tsx @@ -160,72 +160,37 @@ const sections = [ { title: "Best practices", content: ( - - To ensure an intuitive and user-friendly experience, follow these best practices when designing and implementing - tabs: - + + + Maintain logical order: arrange tabs in a meaningful sequence based on user needs and place + frequently used or primary tabs first. + + + Keep tab labels short & clear: use concise, descriptive labels (1-2 words) that clearly + indicate the content. Avoid using generic or ambiguous labels like "Info" or "More." Instead, choose specific + terms that reflect the content, such as "Account Details" for user-related settings or "Billing" for payment + information and prioritize readability—avoid using all caps unless necessary. + + + Managing the number of tabs effectively: while not a strict rule, keeping the number of tabs + manageable (ideally 5-7) helps maintain clarity and usability. If additional tabs are necessary, assess the + information architecture carefully and consider grouping related items. + + + Use icons thoughtfully: ensure the icon is intuitive and clearly represents the content of + the tab. While they are generally used alongside labels, they can also be used independently. In such cases, + it is crucial to choose highly recognizable icons that clearly convey meaning without additional text. When + used together, the icon and label must work harmoniously to reinforce their meaning and avoid any conflicting + interpretations. Avoid using overly decorative or generic icons that do not provide clear meaning, such as an + abstract shape with no context. + + + Keep design consistent: while the component is flexible enough to support a mix of label-only + and label-with-icon tabs, it's best to choose one style per set. Mixing both can reduce scannability and + create visual imbalance, impacting the overall usability. + + ), - subSections: [ - { - title: "Maintain logical order", - content: ( - - Arrange tabs in a meaningful sequence based on user needs. - Place frequently used or primary tabs first. - - ), - }, - { - title: "Keep tab labels short & clear", - content: ( - - - Use concise, descriptive labels (1-2 words) that clearly indicate the content. - - - Avoid using generic or ambiguous labels like "Info" or "More." Instead, choose specific terms that reflect - the content, such as "Account Details" for user-related settings or "Billing" for payment information. - - Prioritize readability—avoid using all caps unless necessary. - - ), - }, - { - title: "Managing the number of tabs effectively", - content: ( - - - While not a strict rule, keeping the number of tabs manageable (ideally 5-7) helps maintain clarity and - usability. - - - If additional tabs are necessary, assess the information architecture carefully and consider grouping - related items. - - - ), - }, - { - title: "Use icons thoughtfully", - content: ( - - - Ensure the icon is intuitive and clearly represents the content of the tab. While they are generally used - alongside labels, they can also be used independently. In such cases, it is crucial to choose highly - recognizable icons that clearly convey meaning without additional text. - - - When used together, the icon and label must work harmoniously to reinforce their meaning and avoid any - conflicting interpretations. - - - Avoid using overly decorative or generic icons that do not provide clear meaning, such as an abstract - shape with no context. - - - ), - }, - ], }, ]; diff --git a/packages/lib/src/divider/Divider.tsx b/packages/lib/src/divider/Divider.tsx index c0e328d1f3..40fe221723 100644 --- a/packages/lib/src/divider/Divider.tsx +++ b/packages/lib/src/divider/Divider.tsx @@ -11,29 +11,29 @@ const StyledDivider = styled.hr` : "var(--border-color-neutral-strongest)" }; ${orientation === "horizontal" ? "width" : "min-height"}: 100%; - ${orientation === "horizontal" ? "height" : "width"}: 0px; + ${orientation === "horizontal" ? "height" : "width"}: 0; ${ orientation === "horizontal" - ? "border-width: " + (weight === "regular" ? "1px 0 0 0" : "2px 0 0 0") - : "border-width: " + (weight === "regular" ? "0 0 0 1px" : "0 0 0 2px") + ? "border-width: " + (weight === "regular" ? "var(--border-width-s) 0 0 0" : "var(--border-width-m) 0 0 0") + : "border-width: " + (weight === "regular" ? "0 0 0 var(--border-width-s)" : "0 0 0 var(--border-width-m)") }; - margin: 0px; + margin: 0; `} `; -const DxcDivider = ({ - orientation = "horizontal", - weight = "regular", +export default function DxcDivider({ color = "mediumGrey", decorative = true, -}: DividerPropsType) => ( - -); - -export default DxcDivider; + orientation = "horizontal", + weight = "regular", +}: DividerPropsType) { + return ( + + ); +} diff --git a/packages/lib/src/nav-tabs/NavTabs.stories.tsx b/packages/lib/src/nav-tabs/NavTabs.stories.tsx index 77bd08ac54..96c0401d08 100644 --- a/packages/lib/src/nav-tabs/NavTabs.stories.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.stories.tsx @@ -1,3 +1,4 @@ +import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; @@ -7,6 +8,11 @@ import DxcNavTabs from "./NavTabs"; export default { title: "Nav Tabs", component: DxcNavTabs, + parameters: { + viewport: { + viewports: INITIAL_VIEWPORTS, + }, + }, } as Meta; const iconSVG = ( @@ -16,10 +22,6 @@ const iconSVG = ( ); -const favoriteIcon = "filled_Favorite"; - -const pinIcon = "Location_On"; - const NavTabs = () => ( <> @@ -93,103 +95,102 @@ const NavTabs = () => ( - <DxcNavTabs> + <DxcNavTabs iconPosition="top"> <DxcNavTabs.Tab href="#" active icon={iconSVG}> Tab 1 </DxcNavTabs.Tab> <DxcNavTabs.Tab href="#" disabled icon={iconSVG}> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={pinIcon}> + <DxcNavTabs.Tab href="#" icon="Location_On"> Tab 3 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={pinIcon}> + <DxcNavTabs.Tab href="#" icon="Location_On"> Tab 4 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> <ExampleContainer> <Title title="With icon position left" theme="light" level={4} /> - <DxcNavTabs iconPosition="left"> - <DxcNavTabs.Tab href="#" active icon={pinIcon}> + <DxcNavTabs> + <DxcNavTabs.Tab href="#" active icon="Location_On"> Tab 1 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" disabled icon={favoriteIcon}> + <DxcNavTabs.Tab href="#" disabled icon="filled_Favorite"> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite"> Tab 3 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite"> Tab 4 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> <ExampleContainer> - <Title title="With icon and notification number" theme="light" level={4} /> - <DxcNavTabs> - <DxcNavTabs.Tab href="#" active icon={pinIcon} notificationNumber> + <Title title="With icon position top and notification number" theme="light" level={4} /> + <DxcNavTabs iconPosition="top"> + <DxcNavTabs.Tab href="#" active icon="Location_On" notificationNumber> Tab 1 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" disabled icon={favoriteIcon} notificationNumber={5}> + <DxcNavTabs.Tab href="#" disabled icon="filled_Favorite" notificationNumber={5}> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon} notificationNumber={120}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite" notificationNumber={120}> Tab 3 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={pinIcon} notificationNumber={12}> + <DxcNavTabs.Tab href="#" icon="Location_On" notificationNumber={12}> Tab 4 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> <ExampleContainer> - <Title title="With icon on the left and notification number" theme="light" level={4} /> - <DxcNavTabs iconPosition="left"> - <DxcNavTabs.Tab href="#" active icon={favoriteIcon} notificationNumber> + <Title title="With icon position left and notification number" theme="light" level={4} /> + <DxcNavTabs> + <DxcNavTabs.Tab href="#" active icon="filled_Favorite" notificationNumber> Tab 1 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" disabled icon={favoriteIcon} notificationNumber={5}> + <DxcNavTabs.Tab href="#" disabled icon="filled_Favorite" notificationNumber={5}> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={pinIcon} notificationNumber={120}> + <DxcNavTabs.Tab href="#" icon="Location_On" notificationNumber={120}> Tab 3 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon} notificationNumber={12}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite" notificationNumber={12}> Tab 4 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> <ExampleContainer> - <Title title="With long label" theme="light" level={4} /> - <DxcNavTabs> + <Title title="With long label and icon position top" theme="light" level={4} /> + <DxcNavTabs iconPosition="top"> <DxcNavTabs.Tab href="#" active> Lorem ipsum dolor sit amet, consectetur adipiscing elit </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon} disabled notificationNumber={3}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite" disabled notificationNumber={3}> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite"> Tab 3 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> <ExampleContainer> <Title title="With long label and left icon alignment" theme="light" level={4} /> - <DxcNavTabs iconPosition="left"> + <DxcNavTabs> <DxcNavTabs.Tab href="#" active> Lorem ipsum dolor sit amet, consectetur adipiscing elit </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon} disabled notificationNumber={3}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite" disabled notificationNumber={3}> Tab 2 </DxcNavTabs.Tab> - <DxcNavTabs.Tab href="#" icon={favoriteIcon}> + <DxcNavTabs.Tab href="#" icon="filled_Favorite"> Tab 3 </DxcNavTabs.Tab> </DxcNavTabs> </ExampleContainer> - <Title title="Opinionated theme" theme="light" level={2} /> <ExampleContainer> - <Title title="NavTabs in a limited space container" theme="light" level={4} /> + <Title title="With limited space" theme="light" level={4} /> <DxcContainer width="500px"> <DxcNavTabs> <DxcNavTabs.Tab href="#" active> @@ -206,8 +207,40 @@ const NavTabs = () => ( </> ); +const Scroll = () => ( + <> + <Title title="Scrollable tabs" theme="light" level={2} /> + <ExampleContainer> + <DxcNavTabs> + <DxcNavTabs.Tab href="#" active icon="filled_Favorite" notificationNumber> + Tab 1 + </DxcNavTabs.Tab> + <DxcNavTabs.Tab href="#" disabled icon="filled_Favorite" notificationNumber={5}> + Tab 2 + </DxcNavTabs.Tab> + <DxcNavTabs.Tab href="#" icon="Location_On" notificationNumber={120}> + Tab 3 + </DxcNavTabs.Tab> + <DxcNavTabs.Tab href="#" icon="filled_Favorite" notificationNumber={12}> + Tab 4 + </DxcNavTabs.Tab> + </DxcNavTabs> + </ExampleContainer> + </> +); + type Story = StoryObj<typeof DxcNavTabs>; export const Chromatic: Story = { render: NavTabs, }; + +export const ScrollableNavTabs: Story = { + render: Scroll, + parameters: { + viewport: { + defaultViewport: "iphonex", + }, + chromatic: { viewports: [375], delay: 5000 }, + }, +}; diff --git a/packages/lib/src/nav-tabs/NavTabs.tsx b/packages/lib/src/nav-tabs/NavTabs.tsx index e9cf9d1b64..880508f94e 100644 --- a/packages/lib/src/nav-tabs/NavTabs.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.tsx @@ -1,79 +1,32 @@ -import { Children, KeyboardEvent, ReactElement, ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { Children, KeyboardEvent, ReactElement, useMemo, useState } from "react"; import styled from "styled-components"; import NavTabsPropsType from "./types"; -import DxcTab from "./Tab"; +import Tab from "./Tab"; import NavTabsContext from "./NavTabsContext"; +import { getLabelFromTab, getPropInChild, getPreviousTabIndex, getNextTabIndex } from "./utils"; -const getPropInChild = (child: ReactNode, propName: string) => { - if (child && typeof child === "object" && "props" in child) { - const childWithProps = child as ReactElement; - if (childWithProps.props[propName]) { - return childWithProps.props[propName]; - } else if (childWithProps.props.children) { - return getPropInChild(childWithProps.props.children, propName); - } - } -}; - -const getLabelFromTab = (child: ReactNode) => { - if (typeof child === "string") { - return child; - } else if (child && typeof child === "object" && "props" in child) { - const childWithProps = child as ReactElement; - if (Array.isArray(childWithProps.props.children)) { - return getLabelFromTab(childWithProps.props.children[0]); - } else { - return getLabelFromTab(childWithProps.props.children); - } - } -}; - -const getPreviousTabIndex = (array: ReactElement[], initialIndex: number): number => { - let index = initialIndex === 0 ? array.length - 1 : initialIndex - 1; - while (getPropInChild(array[index], "disabled")) { - index = index === 0 ? array.length - 1 : index - 1; - } - return index; -}; - -const getNextTabIndex = (array: ReactElement[], initialIndex: number): number => { - let index = initialIndex === array.length - 1 ? 0 : initialIndex + 1; - while (getPropInChild(array[index], "disabled")) { - index = index === array.length - 1 ? 0 : index + 1; - } - return index; -}; +const NavTabsContainer = styled.div` + position: relative; + display: flex; + overflow: auto hidden; +`; -const Underline = styled.div<{ underlineWidth: number }>` +const Underline = styled.div` position: absolute; bottom: 0; left: 0; height: var(--border-width-m); background-color: var(--border-color-neutral-medium); - z-index: -1; - width: ${(props) => props.underlineWidth}px; + width: 100%; `; -const NavTabsContainer = styled.div` - display: flex; - position: relative; - overflow: auto; - z-index: 0; -`; - -const DxcNavTabs = ({ iconPosition = "top", tabIndex = 0, children }: NavTabsPropsType): JSX.Element => { +const DxcNavTabs = ({ iconPosition = "left", tabIndex = 0, children }: NavTabsPropsType): JSX.Element => { const [innerFocusIndex, setInnerFocusIndex] = useState<number | null>(null); - const [underlineWidth, setUnderlineWidth] = useState<number | null>(null); - const refNavTabList = useRef<HTMLDivElement | null>(null); const childArray = Children.toArray(children).filter( (child) => typeof child === "object" && "props" in child ) as ReactElement[]; - useEffect(() => { - setUnderlineWidth(refNavTabList?.current?.scrollWidth ?? null); - }, [children]); - const contextValue = useMemo( () => ({ iconPosition, @@ -103,13 +56,13 @@ const DxcNavTabs = ({ iconPosition = "top", tabIndex = 0, children }: NavTabsPro }; return ( - <NavTabsContainer onKeyDown={handleOnKeyDown} ref={refNavTabList} role="tablist" aria-label="Navigation tabs"> + <NavTabsContainer onKeyDown={handleOnKeyDown} role="tablist" aria-label="Navigation tabs"> + <Underline /> <NavTabsContext.Provider value={contextValue}>{children}</NavTabsContext.Provider> - <Underline underlineWidth={underlineWidth ?? 0} /> </NavTabsContainer> ); }; -DxcNavTabs.Tab = DxcTab; +DxcNavTabs.Tab = Tab; export default DxcNavTabs; diff --git a/packages/lib/src/nav-tabs/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index 5aec52514b..fda9312a8a 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -5,57 +5,54 @@ import DxcFlex from "../flex/Flex"; import NavTabsPropsType, { TabProps } from "./types"; import NavTabsContext from "./NavTabsContext"; import DxcIcon from "../icon/Icon"; +import DxcInset from "../inset/Inset"; -const TabContainer = styled.div<{ active: TabProps["active"] }>` - align-items: stretch; - border-bottom: var(--border-width-s) var(--border-style-default) - ${(props) => (props.active ? "var(--border-color-primary-stronger)" : "transparent")}; - padding: var(--spacing-padding-xs); +const TabContainer = styled.div` + position: relative; + display: flex; + flex-direction: column; `; -const Tab = styled.a<{ +const TabLink = styled.a<{ disabled: TabProps["disabled"]; - hasIcon: boolean; iconPosition: NavTabsPropsType["iconPosition"]; }>` box-sizing: border-box; display: flex; - flex-direction: ${(props) => (props.hasIcon && props.iconPosition === "top" ? "column" : "row")}; + flex-direction: ${({ iconPosition }) => (iconPosition === "top" ? "column" : "row")}; justify-content: center; align-items: center; gap: var(--spacing-gap-xs); - height: ${(props) => (props.hasIcon && props.iconPosition === "top" ? "78px" : "100%")}; + height: ${({ iconPosition }) => (iconPosition === "top" ? "78px" : "100%")}; min-width: 176px; min-height: 48px; padding: var(--spacing-padding-none) var(--spacing-padding-xs); border-radius: var(--border-radius-s); - background: var(--color-bg-neutral-lightest); - text-decoration-color: transparent; - text-decoration-line: none; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + background-color: var(--color-bg-neutral-lightest); + text-decoration: none; + cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; ${(props) => !props.disabled && ` :hover { - background: var(--color-bg-primary-lighter); + background-color: var(--color-bg-primary-lighter); } :focus { outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); - outline-offset: var(--border-width-m); + outline-offset: calc(var(--border-width-m) * -1); } :active { - background: var(--color-bg-primary-lighter); + background-color: var(--color-bg-primary-lighter); } `} `; const Label = styled.span<{ disabled: TabProps["disabled"]; - active: TabProps["active"]; }>` display: inline; - color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-stronger)")}; + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-stronger)")}; font-family: var(--typography-font-family); font-size: var(--typography-label-l); font-weight: var(--typography-label-semibold); @@ -65,27 +62,35 @@ const Label = styled.span<{ white-space: normal; `; -const TabIconContainer = styled.div<{ - iconPosition: NavTabsPropsType["iconPosition"]; - active: TabProps["active"]; +const IconContainer = styled.div<{ disabled: TabProps["disabled"]; }>` display: flex; + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-stronger)")}; font-size: var(--height-s); - color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-stronger)")}; svg { height: var(--height-s); width: 24px; } `; -const DxcTab = forwardRef( +const Underline = styled.span<{ active: TabProps["active"] }>` + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: var(--border-width-m); + background-color: ${({ active }) => + active ? "var(--border-color-primary-stronger)" : "var(--border-color-neutral-medium)"}; +`; + +const Tab = forwardRef( ( { href, active = false, icon, disabled = false, notificationNumber = false, children, ...otherProps }: TabProps, ref: Ref<HTMLAnchorElement> - ): JSX.Element => { - const tabRef = useRef<HTMLAnchorElement>(); + ) => { const { iconPosition, tabIndex, focusedLabel } = useContext(NavTabsContext) ?? {}; + const tabRef = useRef<HTMLAnchorElement>(); const innerRef = useRef<HTMLAnchorElement | null>(null); useImperativeHandle(ref, () => innerRef.current!, []); @@ -93,7 +98,7 @@ const DxcTab = forwardRef( if (focusedLabel === children.toString()) { tabRef?.current?.focus(); } - }, [focusedLabel]); + }, [children, focusedLabel]); const handleOnKeyDown = (event: KeyboardEvent<HTMLAnchorElement>) => { switch (event.key) { @@ -108,51 +113,47 @@ const DxcTab = forwardRef( }; return ( - <TabContainer active={active}> - <Tab - href={!disabled ? href : undefined} - disabled={disabled} - iconPosition={iconPosition} - hasIcon={icon != null} - ref={(anchorRef: HTMLAnchorElement) => { - tabRef.current = anchorRef; - - if (ref) { - if (typeof ref === "function") { - ref(anchorRef); - } else { - innerRef.current = anchorRef; + <TabContainer> + <DxcInset space="var(--spacing-padding-xs)"> + <TabLink + href={!disabled ? href : undefined} + disabled={disabled} + iconPosition={iconPosition} + ref={(anchorRef: HTMLAnchorElement) => { + tabRef.current = anchorRef; + if (ref) { + if (typeof ref === "function") ref(anchorRef); + else innerRef.current = anchorRef; } - } - }} - onKeyDown={handleOnKeyDown} - tabIndex={active ? tabIndex : -1} - role="tab" - aria-selected={active} - aria-disabled={disabled} - {...otherProps} - > - {icon && ( - <TabIconContainer iconPosition={iconPosition} active={active} disabled={disabled}> - {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} - </TabIconContainer> - )} - <DxcFlex alignItems="center" gap="var(--spacing-gap-s)"> - <Label active={active} disabled={disabled}> - {children} - </Label> - {notificationNumber && !disabled && ( - <DxcBadge - mode="notification" - size="small" - label={typeof notificationNumber === "number" ? notificationNumber : undefined} - /> + }} + onKeyDown={handleOnKeyDown} + tabIndex={active ? tabIndex : -1} + role="tab" + aria-selected={active} + aria-disabled={disabled} + {...otherProps} + > + {icon && ( + <IconContainer disabled={disabled}> + {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} + </IconContainer> )} - </DxcFlex> - </Tab> + <DxcFlex alignItems="center" gap="var(--spacing-gap-s)"> + <Label disabled={disabled}>{children}</Label> + {notificationNumber && !disabled && ( + <DxcBadge + mode="notification" + size="small" + label={typeof notificationNumber === "number" ? notificationNumber : undefined} + /> + )} + </DxcFlex> + </TabLink> + </DxcInset> + <Underline active={active} /> </TabContainer> ); } ); -export default DxcTab; +export default Tab; diff --git a/packages/lib/src/nav-tabs/utils.ts b/packages/lib/src/nav-tabs/utils.ts new file mode 100644 index 0000000000..1cc2452f85 --- /dev/null +++ b/packages/lib/src/nav-tabs/utils.ts @@ -0,0 +1,39 @@ +import { ReactNode, ReactElement } from "react"; + +export const getPropInChild = (child: ReactNode, propName: string): string | undefined => { + if (child && typeof child === "object" && "props" in child) { + const childWithProps = child as ReactElement; + if (childWithProps.props[propName]) { + return childWithProps.props[propName]; + } else if (childWithProps.props.children) { + return getPropInChild(childWithProps.props.children, propName); + } + } +}; + +export const getLabelFromTab = (child: ReactNode): string | undefined => { + if (typeof child === "string") { + return child; + } else if (child && typeof child === "object" && "props" in child) { + const childWithProps = child as ReactElement; + return Array.isArray(childWithProps.props.children) + ? getLabelFromTab(childWithProps.props.children[0]) + : getLabelFromTab(childWithProps.props.children); + } +}; + +export const getPreviousTabIndex = (array: ReactElement[], initialIndex: number): number => { + let index = initialIndex === 0 ? array.length - 1 : initialIndex - 1; + while (getPropInChild(array[index], "disabled")) { + index = index === 0 ? array.length - 1 : index - 1; + } + return index; +}; + +export const getNextTabIndex = (array: ReactElement[], initialIndex: number): number => { + let index = initialIndex === array.length - 1 ? 0 : initialIndex + 1; + while (getPropInChild(array[index], "disabled")) { + index = index === array.length - 1 ? 0 : index + 1; + } + return index; +};