From 34c753ba76fecd6a44bb5cbca122fdd325f3db0a Mon Sep 17 00:00:00 2001 From: Jialecl Date: Fri, 11 Apr 2025 09:34:00 +0200 Subject: [PATCH 1/8] Tab outline offset hotfix --- packages/lib/src/nav-tabs/Tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/nav-tabs/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index 5aec52514..aaecea54f 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -42,7 +42,7 @@ const Tab = styled.a<{ } :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); From ad1e2c0f528859989e92bacc048ec272eaab7b91 Mon Sep 17 00:00:00 2001 From: Jialecl Date: Fri, 11 Apr 2025 10:27:12 +0200 Subject: [PATCH 2/8] Increased active nav tab underline size --- packages/lib/src/nav-tabs/Tab.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/nav-tabs/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index aaecea54f..c7e15d823 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -8,7 +8,7 @@ import DxcIcon from "../icon/Icon"; const TabContainer = styled.div<{ active: TabProps["active"] }>` align-items: stretch; - border-bottom: var(--border-width-s) var(--border-style-default) + border-bottom: var(--border-width-m) var(--border-style-default) ${(props) => (props.active ? "var(--border-color-primary-stronger)" : "transparent")}; padding: var(--spacing-padding-xs); `; @@ -52,7 +52,6 @@ const Tab = styled.a<{ 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)")}; @@ -66,8 +65,6 @@ const Label = styled.span<{ `; const TabIconContainer = styled.div<{ - iconPosition: NavTabsPropsType["iconPosition"]; - active: TabProps["active"]; disabled: TabProps["disabled"]; }>` display: flex; @@ -133,14 +130,12 @@ const DxcTab = forwardRef( {...otherProps} > {icon && ( - + {typeof icon === "string" ? : icon} )} - + {notificationNumber && !disabled && ( Date: Fri, 11 Apr 2025 14:16:53 +0200 Subject: [PATCH 3/8] Nav tabs updates, fixes and doc --- .../pages/components/nav-tabs/code.tsx | 2 +- .../pages/components/nav-tabs/index.tsx | 2 +- .../components/nav-tabs/NavTabsPageLayout.tsx | 4 +- .../nav-tabs/code/NavTabsCodePage.tsx | 22 ++-- .../nav-tabs/overview/NavTabsOverviewPage.tsx | 23 ++-- .../tabs/overview/TabsOverviewPage.tsx | 95 +++++--------- packages/lib/src/divider/Divider.tsx | 6 +- packages/lib/src/nav-tabs/NavTabs.stories.tsx | 21 ++-- packages/lib/src/nav-tabs/NavTabs.tsx | 75 +++-------- packages/lib/src/nav-tabs/Tab.tsx | 118 +++++++++--------- packages/lib/src/nav-tabs/utils.ts | 39 ++++++ 11 files changed, 185 insertions(+), 222 deletions(-) create mode 100644 packages/lib/src/nav-tabs/utils.ts diff --git a/apps/website/pages/components/nav-tabs/code.tsx b/apps/website/pages/components/nav-tabs/code.tsx index b6e2985af..8a24f391f 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 d7d3c2317..8bd45d905 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 16077c5cd..6cd8ccf9e 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 20157e820..f91c870af 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 768921134..2b135a572 100644 --- a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx +++ b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx @@ -169,6 +169,11 @@ const sections = [ 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 - @@ -184,15 +189,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 6c71061f5..6963a0284 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 c0e328d1f..5ec4fbfb5 100644 --- a/packages/lib/src/divider/Divider.tsx +++ b/packages/lib/src/divider/Divider.tsx @@ -14,10 +14,10 @@ const StyledDivider = styled.hr` ${orientation === "horizontal" ? "height" : "width"}: 0px; ${ 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; `} `; diff --git a/packages/lib/src/nav-tabs/NavTabs.stories.tsx b/packages/lib/src/nav-tabs/NavTabs.stories.tsx index 77bd08ac5..5bcbc3b0f 100644 --- a/packages/lib/src/nav-tabs/NavTabs.stories.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.stories.tsx @@ -93,7 +93,7 @@ const NavTabs = () => ( - <DxcNavTabs> + <DxcNavTabs iconPosition="top"> <DxcNavTabs.Tab href="#" active icon={iconSVG}> Tab 1 </DxcNavTabs.Tab> @@ -110,7 +110,7 @@ const NavTabs = () => ( </ExampleContainer> <ExampleContainer> <Title title="With icon position left" theme="light" level={4} /> - <DxcNavTabs iconPosition="left"> + <DxcNavTabs> <DxcNavTabs.Tab href="#" active icon={pinIcon}> Tab 1 </DxcNavTabs.Tab> @@ -126,8 +126,8 @@ const NavTabs = () => ( </DxcNavTabs> </ExampleContainer> <ExampleContainer> - <Title title="With icon and notification number" theme="light" level={4} /> - <DxcNavTabs> + <Title title="With icon position top and notification number" theme="light" level={4} /> + <DxcNavTabs iconPosition="top"> <DxcNavTabs.Tab href="#" active icon={pinIcon} notificationNumber> Tab 1 </DxcNavTabs.Tab> @@ -143,8 +143,8 @@ const NavTabs = () => ( </DxcNavTabs> </ExampleContainer> <ExampleContainer> - <Title title="With icon on the left and notification number" theme="light" level={4} /> - <DxcNavTabs iconPosition="left"> + <Title title="With icon position left and notification number" theme="light" level={4} /> + <DxcNavTabs> <DxcNavTabs.Tab href="#" active icon={favoriteIcon} notificationNumber> Tab 1 </DxcNavTabs.Tab> @@ -160,8 +160,8 @@ const NavTabs = () => ( </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> @@ -175,7 +175,7 @@ const NavTabs = () => ( </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> @@ -187,9 +187,8 @@ const NavTabs = () => ( </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> diff --git a/packages/lib/src/nav-tabs/NavTabs.tsx b/packages/lib/src/nav-tabs/NavTabs.tsx index e9cf9d1b6..880508f94 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 c7e15d823..379e961eb 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -5,26 +5,25 @@ 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-m) 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); @@ -32,7 +31,7 @@ const Tab = styled.a<{ background: var(--color-bg-neutral-lightest); text-decoration-color: transparent; text-decoration-line: none; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; ${(props) => !props.disabled && @@ -54,7 +53,7 @@ const Label = styled.span<{ disabled: TabProps["disabled"]; }>` 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); @@ -64,25 +63,34 @@ const Label = styled.span<{ white-space: normal; `; -const TabIconContainer = styled.div<{ +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)" : "transparent")}; +`; + +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!, []); @@ -90,7 +98,7 @@ const DxcTab = forwardRef( if (focusedLabel === children.toString()) { tabRef?.current?.focus(); } - }, [focusedLabel]); + }, [children, focusedLabel]); const handleOnKeyDown = (event: KeyboardEvent<HTMLAnchorElement>) => { switch (event.key) { @@ -105,49 +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 disabled={disabled}> - {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} - </TabIconContainer> - )} - <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} - /> + }} + 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 000000000..1cc2452f8 --- /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; +}; From 299282389924b459d468a19c8d39a8aee6922335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20G=C3=B3mez=20Pinta?= <44321109+GomezIvann@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:21:19 +0200 Subject: [PATCH 4/8] Small update --- packages/lib/src/nav-tabs/Tab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/nav-tabs/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index 379e961eb..62bafb34c 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -28,7 +28,7 @@ const TabLink = styled.a<{ min-height: 48px; padding: var(--spacing-padding-none) var(--spacing-padding-xs); border-radius: var(--border-radius-s); - background: var(--color-bg-neutral-lightest); + background-color: var(--color-bg-neutral-lightest); text-decoration-color: transparent; text-decoration-line: none; cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; @@ -37,14 +37,14 @@ const TabLink = styled.a<{ !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: calc(var(--border-width-m) * -1); } :active { - background: var(--color-bg-primary-lighter); + background-color: var(--color-bg-primary-lighter); } `} `; From 67fc5227218ba3ec4c0c4bef76dea7d1cff41c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20G=C3=B3mez=20Pinta?= <44321109+GomezIvann@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:28:12 +0200 Subject: [PATCH 5/8] Divider updates --- packages/lib/src/divider/Divider.tsx | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/lib/src/divider/Divider.tsx b/packages/lib/src/divider/Divider.tsx index 5ec4fbfb5..40fe22172 100644 --- a/packages/lib/src/divider/Divider.tsx +++ b/packages/lib/src/divider/Divider.tsx @@ -11,7 +11,7 @@ const StyledDivider = styled.hr<DividerPropsType>` : "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" ? "var(--border-width-s) 0 0 0" : "var(--border-width-m) 0 0 0") @@ -21,19 +21,19 @@ const StyledDivider = styled.hr<DividerPropsType>` `} `; -const DxcDivider = ({ - orientation = "horizontal", - weight = "regular", +export default function DxcDivider({ color = "mediumGrey", decorative = true, -}: DividerPropsType) => ( - <StyledDivider - orientation={orientation} - weight={weight} - color={color} - aria-orientation={orientation} - aria-hidden={decorative} - /> -); - -export default DxcDivider; + orientation = "horizontal", + weight = "regular", +}: DividerPropsType) { + return ( + <StyledDivider + aria-hidden={decorative} + aria-orientation={orientation} + color={color} + orientation={orientation} + weight={weight} + /> + ); +} From 978bb998c6dbd33c98b1bb814f96bf3bc00ee9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20G=C3=B3mez=20Pinta?= <44321109+GomezIvann@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:34:28 +0200 Subject: [PATCH 6/8] Nav tabs overview fixes --- .../nav-tabs/overview/NavTabsOverviewPage.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx index 2b135a572..a734226c9 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 = [ <Image src={navTabsAnatomy} alt="Nav tabs anatomy" /> <DxcBulletedList type="number"> <DxcBulletedList.Item> - <strong>Container: </strong>the outer wrapper that holds and organizes all tab items. It defines the overall + <strong>Container:</strong> 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. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Label: </strong>the text displayed within each tab that indicates the section or category it leads + <strong>Label:</strong> the text displayed within each tab that indicates the section or category it leads to. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Notification badge </strong> - <em>(Optional)</em>: a visual indicator that displays the number of pending actions, alerts or updates - related to a specific tab. + <strong>Notification badge</strong> <em>(Optional)</em>: a visual indicator that displays the number of + pending actions, alerts or updates related to a specific tab. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Selected tab: </strong>the active tab currently in focus, representing the visible content section. + <strong>Selected tab:</strong> 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. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Icon</strong> - <em>(Optional)</em>: an optional graphical element placed before the label to visually reinforce the tab's - meaning or category. + <strong>Icon</strong> <em>(Optional)</em>: an optional graphical element placed before the label to visually + reinforce the tab's meaning or category. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Selected tab indicator: </strong>a horizontal bar that visually marks the currently active tab. + <strong>Selected tab indicator:</strong> a horizontal bar that visually marks the currently active tab. </DxcBulletedList.Item> <DxcBulletedList.Item> - <strong>Unselected tab indicator: </strong>a horizontal bar that visually marks the currently inactive tab. + <strong>Unselected tab indicator:</strong> a horizontal bar that visually marks the currently inactive tab. </DxcBulletedList.Item> </DxcBulletedList> </> From efe56cd05d3a53dd199fe5073bc768cb55d2836c Mon Sep 17 00:00:00 2001 From: Jialecl <jialestrabajos@gmail.com> Date: Mon, 14 Apr 2025 11:32:09 +0200 Subject: [PATCH 7/8] small format changes --- .../nav-tabs/overview/NavTabsOverviewPage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx index a734226c9..73f4becbf 100644 --- a/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx +++ b/apps/website/screens/components/nav-tabs/overview/NavTabsOverviewPage.tsx @@ -73,7 +73,7 @@ const sections = [ <DxcBulletedList> <DxcBulletedList.Item> Use the <strong>top position</strong> 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. <Example example={icons_top} defaultIsVisible={false} /> </DxcBulletedList.Item> @@ -138,8 +138,8 @@ const sections = [ </DxcBulletedList> <DxcParagraph> Both components improve usability, but <strong>tabs</strong> are best for grouping related content within - a page, while - <strong>nav tabs</strong> help users move across different sections or pages of an application. + a page, while <strong>nav tabs</strong> help users move across different sections or pages of an + application. </DxcParagraph> </> ), @@ -160,7 +160,7 @@ const sections = [ especially on smaller viewports. </DxcBulletedList.Item> <DxcBulletedList.Item> - Ensure that tabs <strong>follow a logical order</strong> — based on frequency of use, workflow, or user + Ensure that tabs <strong>follow a logical order</strong>—based on frequency of use, workflow, or user priority. </DxcBulletedList.Item> <DxcBulletedList.Item> @@ -174,8 +174,8 @@ const sections = [ </DxcBulletedList.Item> <DxcBulletedList.Item> 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. </DxcBulletedList.Item> <DxcBulletedList.Item> Avoid mixing navigation tabs and action buttons within the same group, as this can create confusion around From 07be78af361c52876e7b8891a409b22558d54653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20G=C3=B3mez=20Pinta?= <44321109+GomezIvann@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:39:19 +0200 Subject: [PATCH 8/8] Nav tabs fix --- packages/lib/src/nav-tabs/NavTabs.stories.tsx | 78 +++++++++++++------ packages/lib/src/nav-tabs/Tab.tsx | 6 +- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/lib/src/nav-tabs/NavTabs.stories.tsx b/packages/lib/src/nav-tabs/NavTabs.stories.tsx index 5bcbc3b0f..96c0401d0 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<typeof DxcNavTabs>; const iconSVG = ( @@ -16,10 +22,6 @@ const iconSVG = ( </svg> ); -const favoriteIcon = "filled_Favorite"; - -const pinIcon = "Location_On"; - const NavTabs = () => ( <> <ExampleContainer> @@ -100,10 +102,10 @@ const NavTabs = () => ( <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> @@ -111,16 +113,16 @@ const NavTabs = () => ( <ExampleContainer> <Title title="With icon position left" theme="light" level={4} /> <DxcNavTabs> - <DxcNavTabs.Tab href="#" active icon={pinIcon}> + <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> @@ -128,16 +130,16 @@ const NavTabs = () => ( <ExampleContainer> <Title title="With icon position top and notification number" theme="light" level={4} /> <DxcNavTabs iconPosition="top"> - <DxcNavTabs.Tab href="#" active icon={pinIcon} notificationNumber> + <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> @@ -145,16 +147,16 @@ const NavTabs = () => ( <ExampleContainer> <Title title="With icon position left and notification number" theme="light" level={4} /> <DxcNavTabs> - <DxcNavTabs.Tab href="#" active icon={favoriteIcon} notificationNumber> + <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> @@ -165,10 +167,10 @@ const NavTabs = () => ( <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> @@ -179,10 +181,10 @@ const NavTabs = () => ( <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> @@ -205,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/Tab.tsx b/packages/lib/src/nav-tabs/Tab.tsx index 62bafb34c..fda9312a8 100644 --- a/packages/lib/src/nav-tabs/Tab.tsx +++ b/packages/lib/src/nav-tabs/Tab.tsx @@ -29,8 +29,7 @@ const TabLink = styled.a<{ padding: var(--spacing-padding-none) var(--spacing-padding-xs); border-radius: var(--border-radius-s); background-color: var(--color-bg-neutral-lightest); - text-decoration-color: transparent; - text-decoration-line: none; + text-decoration: none; cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; ${(props) => @@ -81,7 +80,8 @@ const Underline = styled.span<{ active: TabProps["active"] }>` bottom: 0; width: 100%; height: var(--border-width-m); - background-color: ${({ active }) => (active ? "var(--border-color-primary-stronger)" : "transparent")}; + background-color: ${({ active }) => + active ? "var(--border-color-primary-stronger)" : "var(--border-color-neutral-medium)"}; `; const Tab = forwardRef(