From 8ea011bee07098fac861d9563bc852ae0b570eab Mon Sep 17 00:00:00 2001 From: Mihai Albu Date: Tue, 29 Oct 2024 05:58:31 +0000 Subject: [PATCH] fix(tabs): support for post-rebrand validation pattern added the necessary changes to support the new validation design update --- .../tab-title/tab-title.component.tsx | 57 ++- .../__internal__/tab-title/tab-title.style.ts | 90 +++- .../tabs-header/tabs-header.component.tsx | 9 +- .../tabs-header/tabs-header.style.ts | 7 +- src/components/tabs/tabs.stories.tsx | 391 ++++++++++++++++++ src/components/tabs/tabs.test.tsx | 69 ++++ 6 files changed, 600 insertions(+), 23 deletions(-) diff --git a/src/components/tabs/__internal__/tab-title/tab-title.component.tsx b/src/components/tabs/__internal__/tab-title/tab-title.component.tsx index 2bb97a664c..d3ad0e16a9 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.component.tsx +++ b/src/components/tabs/__internal__/tab-title/tab-title.component.tsx @@ -1,10 +1,11 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useContext } from "react"; import { StyledTabTitleButton, StyledTabTitleLink, StyledTitleContent, StyledLayoutWrapper, StyledSelectedIndicator, + StyledVerticalIndicator, } from "./tab-title.style"; import tagComponent from "../../../../__internal__/utils/helpers/tags/tags"; import ValidationIcon from "../../../../__internal__/validations/validation-icon.component"; @@ -12,6 +13,8 @@ import Icon from "../../../icon"; import Events from "../../../../__internal__/utils/helpers/events"; import { TooltipProvider } from "../../../../__internal__/tooltip-provider"; import TabTitleContext from "./tab-title.context"; +import Typography from "../../../typography"; +import NewValidationContext from "../../../carbon-provider/__internal__/new-validation.context"; export interface TabTitleProps { /** Identifier used for testing purposes */ @@ -77,6 +80,7 @@ const TabTitle = React.forwardRef( onKeyDown, align, tabIndex, + id, ...tabTitleProps }: TabTitleProps, ref: React.ForwardedRef @@ -86,6 +90,7 @@ const TabTitle = React.forwardRef( const hasFailedValidation = error || warning || info; const [shouldShowTooltip, setShouldShowTooltip] = useState(false); const hasHover = useRef(false); + const { validationRedesignOptIn } = useContext(NewValidationContext); const showTooltip = () => { setShouldShowTooltip(true); @@ -142,6 +147,11 @@ const TabTitle = React.forwardRef( return ( {customLayout} + {validationRedesignOptIn && hasFailedValidation && ( + + {errorMessage || warningMessage} + + )} ); } @@ -164,6 +174,11 @@ const TabTitle = React.forwardRef( onClick: handleClick, }); })} + {validationRedesignOptIn && hasFailedValidation && ( + + {errorMessage || warningMessage} + + )} ); @@ -190,6 +205,7 @@ const TabTitle = React.forwardRef( alternateStyling={hasAlternateStyling} align={align} hasHref={!!href} + validationRedesignOptIn={validationRedesignOptIn} > {renderContent()} {isHref && } @@ -198,6 +214,7 @@ const TabTitle = React.forwardRef( {error && ( @@ -213,16 +230,28 @@ const TabTitle = React.forwardRef( {!warning && !error && info && ( )} + {validationRedesignOptIn && hasFailedValidation && ( + + {errorMessage || warningMessage} + + )} )} - {!(hasFailedValidation || hasAlternateStyling) && isTabSelected && ( - + {validationRedesignOptIn && position === "left" && ( + )} + {(!(hasFailedValidation || hasAlternateStyling) || + validationRedesignOptIn) && + isTabSelected && ( + + )} ); @@ -243,8 +272,9 @@ const TabTitle = React.forwardRef( borders, isInSidebar, tabIndex, + id, ...tabTitleProps, - ...tagComponent("tab-header", tabTitleProps), + ...tagComponent("tab-header", { id, ...tabTitleProps }), onKeyDown: handleKeyDown, onClick: handleClick, size, @@ -258,6 +288,13 @@ const TabTitle = React.forwardRef( }, onFocus: showTooltip, onBlur: hideTooltip, + ...(validationRedesignOptIn && + hasFailedValidation && { + "aria-invalid": true, + "aria-errormessage": `${id}-message`, + "aria-describedby": `${id}-message`, + }), + validationRedesignOptIn, }; const tabTitle = isHref ? ( @@ -280,7 +317,9 @@ const TabTitle = React.forwardRef( ); return ( - + {tabTitle} diff --git a/src/components/tabs/__internal__/tab-title/tab-title.style.ts b/src/components/tabs/__internal__/tab-title/tab-title.style.ts index 006baa1789..773e0bbf82 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.style.ts +++ b/src/components/tabs/__internal__/tab-title/tab-title.style.ts @@ -23,6 +23,7 @@ interface StyledTitleContentProps hasCustomLayout?: boolean; hasHref?: boolean; hasSiblings?: boolean; + validationRedesignOptIn?: boolean; } const oldFocusStyling = ` @@ -51,6 +52,7 @@ const StyledTitleContent = styled.span` hasHref, alternateStyling, align, + validationRedesignOptIn, }) => css` text-align: ${align}; @@ -65,6 +67,12 @@ const StyledTitleContent = styled.span` border-top-right-radius: var(--borderRadius000); `} + ${position === "left" && + validationRedesignOptIn && + css` + justify-content: space-between; + `} + ${position === "top" && css` border-top-left-radius: var(--borderRadius100); @@ -149,7 +157,7 @@ const StyledTitleContent = styled.span` css` outline: 1px solid; outline-offset: -1px; - z-index: 2; + z-index: ${validationRedesignOptIn ? 1 : 2}; ${info && !warning && @@ -210,7 +218,7 @@ const StyledTitleContent = styled.span` css` outline: 2px solid var(--colorsSemanticNegative500); outline-offset: -2px; - z-index: 2; + z-index: ${validationRedesignOptIn ? 1 : 2}; ${position === "top" && css` @@ -289,7 +297,9 @@ const StyledTitleContent = styled.span` `} `; -const tabTitleStyles = css` +const tabTitleStyles = css< + TabTitleProps & { validationRedesignOptIn?: boolean } +>` background-color: transparent; display: inline-block; border-top-left-radius: var(--borderRadius100); @@ -328,6 +338,7 @@ const tabTitleStyles = css` warning, info, isInSidebar, + validationRedesignOptIn, }) => css` height: ${size === "large" ? "var(--sizing600)" : "var(--sizing500)"}; @@ -360,9 +371,17 @@ const tabTitleStyles = css` ${!isTabSelected && css` color: var(--colorsActionMinorYin090); + ${validationRedesignOptIn && + css` + background: transparent; + `} &:hover { background: var(--colorsActionMinor100); + ${validationRedesignOptIn && + css` + background: var(--colorsUtilityMajor100); + `} color: var(--colorsActionMinorYin090); outline: none; } @@ -415,7 +434,11 @@ const tabTitleStyles = css` ${!isInSidebar && !error && css` - border-right: ${alternateStyling ? "1px" : "2px"} solid + --border-right-value: ${validationRedesignOptIn ? "0px" : "2px"} + + border-right: ${ + alternateStyling ? "1px" : "var(--border-right-value)" + } solid var(--colorsActionMinor100); `} @@ -443,6 +466,11 @@ const tabTitleStyles = css` border-right: none; `} + ${!isTabSelected && + css` + border-right-color: var(--colorsActionMinor100); + `} + ${isTabSelected && css` ${alternateStyling && @@ -456,7 +484,9 @@ const tabTitleStyles = css` padding-bottom: 0px; ${StyledTitleContent} { - ${!(error || warning || info) && "margin-right: 2px;"} + ${!(error || warning || info) && + !validationRedesignOptIn && + "margin-right: 2px;"} border-right: none; } `} @@ -510,6 +540,7 @@ interface StyledLayoutWrapperProps extends Pick { hasCustomLayout?: boolean; hasCustomSibling?: boolean; + validationRedesignOptIn?: boolean; } const StyledLayoutWrapper = styled.div` @@ -518,6 +549,7 @@ const StyledLayoutWrapper = styled.div` titlePosition = "before", hasCustomSibling, position, + validationRedesignOptIn, }) => css` ${hasCustomLayout && css` @@ -547,29 +579,64 @@ const StyledLayoutWrapper = styled.div` z-index: 10; ${StyledIcon} { - height: 16px; + --top-position-value: ${validationRedesignOptIn ? "6px" : "3px"}; + + height: ${validationRedesignOptIn ? "8px" : "16px"}; left: -2px; - top: ${position === "left" ? "1px" : "3px"}; + top: ${position === "left" ? "1px" : "var(--top-position-value)"}; } } `} `} `; -type StyledSelectedIndicatorProps = Pick; +const StyledVerticalIndicator = styled.div` + position: absolute; + top: 0px; + bottom: 0px; + right: 0px; + box-shadow: inset calc(-1 * var(--sizing050)) 0px 0px 0px + var(--colorsActionMinor100); + width: 2px; + z-index: 1; +`; + +type StyledSelectedIndicatorProps = Pick< + TabTitleProps, + "position" | "error" | "warning" +> & { + validationRedesignOptIn?: boolean; +}; const StyledSelectedIndicator = styled.div` position: absolute; z-index: 1; + ${(validationRedesignOptIn) => css` + ${validationRedesignOptIn && + css` + z-index: 3; + `} + `} - ${({ position = "top" }) => css` + ${({ position = "top", warning, error }) => css` + --selected-indicator-color: var(--colorsActionMajor500); + + ${warning && + css` + --selected-indicator-color: var(--colorsSemanticCaution500); + `} + + ${error && + css` + --selected-indicator-color: var(--colorsSemanticNegative500); + `} ${position === "top" && css` bottom: 0px; left: 0px; right: 0px; box-shadow: inset 0px calc(-1 * var(--sizing050)) 0px - var(--colorsActionMajor500); + var(--selected-indicator-color); height: var(--sizing050); `} @@ -579,7 +646,7 @@ const StyledSelectedIndicator = styled.div` bottom: 0px; right: 0px; box-shadow: inset calc(-1 * var(--sizing050)) 0px 0px 0px - var(--colorsActionMajor500); + var(--selected-indicator-color); width: var(--sizing050); `} `} @@ -591,4 +658,5 @@ export { StyledTitleContent, StyledLayoutWrapper, StyledSelectedIndicator, + StyledVerticalIndicator, }; diff --git a/src/components/tabs/__internal__/tabs-header/tabs-header.component.tsx b/src/components/tabs/__internal__/tabs-header/tabs-header.component.tsx index 557dfb6ff5..a5565fab67 100644 --- a/src/components/tabs/__internal__/tabs-header/tabs-header.component.tsx +++ b/src/components/tabs/__internal__/tabs-header/tabs-header.component.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useContext } from "react"; import { StyledTabsHeaderWrapper, StyledTabsHeaderList, @@ -8,6 +8,7 @@ import { StyledVerticalTabsWrapper, } from "./tabs-header.style"; import useThrottle from "../../../../hooks/__internal__/useThrottle"; +import NewValidationContext from "../../../carbon-provider/__internal__/new-validation.context"; /* In the original prototype the tabs have shadows that fade out as you scroll horizontally. * This value is the closest replication to the way that the shadow disappears. @@ -44,6 +45,8 @@ const TabsHeader = ({ const [leftScrollOpacity, setLeftScrollOpacity] = useState(0); const [rightScrollOpacity, setRightScrollOpacity] = useState(1); + const { validationRedesignOptIn } = useContext(NewValidationContext); + const ref = useRef(null); let isScrollable = false; @@ -83,7 +86,9 @@ const TabsHeader = ({ > {position === "top" ? ( - + {children} diff --git a/src/components/tabs/__internal__/tabs-header/tabs-header.style.ts b/src/components/tabs/__internal__/tabs-header/tabs-header.style.ts index 63d83372e0..e40934e0e7 100644 --- a/src/components/tabs/__internal__/tabs-header/tabs-header.style.ts +++ b/src/components/tabs/__internal__/tabs-header/tabs-header.style.ts @@ -165,11 +165,16 @@ const StyledVerticalTabsWrapper = styled.div` flex-direction: column; `; -const StyledTabsBottomBorderWrapper = styled.div` +const StyledTabsBottomBorderWrapper = styled.div<{ + validationRedesignOptIn?: boolean; +}>` position: absolute; width: 100%; height: auto; bottom: 0; + ${({ validationRedesignOptIn }) => css` + z-index: ${validationRedesignOptIn ? 2 : ""}; + `} `; const StyledTabsBottomBorder = styled.div` diff --git a/src/components/tabs/tabs.stories.tsx b/src/components/tabs/tabs.stories.tsx index bb30598556..0dec6f932a 100644 --- a/src/components/tabs/tabs.stories.tsx +++ b/src/components/tabs/tabs.stories.tsx @@ -9,6 +9,7 @@ import Pill from "../pill"; import Icon from "../icon"; import Box from "../box"; import { Tabs, Tab } from "."; +import CarbonProvider from "../carbon-provider/carbon-provider.component"; const styledSystemProps = generateStyledSystemProps({ margin: true, @@ -889,6 +890,109 @@ export const WithValidationsPositionedTop: Story = () => { }; WithValidationsPositionedTop.storyName = "With Validations Positioned Top"; +export const WithNewValidationRedesign: Story = () => { + const [errors, setErrors] = useState({ + one: true, + two: false, + three: false, + }); + const [warnings, setWarnings] = useState({ + one: true, + two: true, + three: false, + }); + const [infos, setInfos] = useState({ one: true, two: true, three: true }); + return ( + + + + + setErrors({ ...errors, one: !errors.one })} + checked={errors.one} + /> + setWarnings({ ...warnings, one: !warnings.one })} + checked={warnings.one} + /> + setInfos({ ...infos, one: !infos.one })} + checked={infos.one} + /> + + + setErrors({ ...errors, two: !errors.two })} + /> + setWarnings({ ...warnings, two: !warnings.two })} + checked={warnings.two} + /> + setInfos({ ...infos, two: !infos.two })} + checked={infos.two} + /> + + + setErrors({ ...errors, three: !errors.three })} + /> + + setWarnings({ ...warnings, three: !warnings.three }) + } + /> + setInfos({ ...infos, three: !infos.three })} + checked={infos.three} + /> + + + + + ); +}; +WithNewValidationRedesign.storyName = "With New Validation Positioned Top"; + export const WithValidationsSizedLargePositionedTop: Story = () => { const [errors, setErrors] = useState({ one: true, @@ -1092,6 +1196,110 @@ export const WithValidationsPositionedLeft: Story = () => { }; WithValidationsPositionedLeft.storyName = "With Validations Positioned Left"; +export const WithValidationsPositionedLeftRedesign: Story = () => { + const [errors, setErrors] = useState({ + one: true, + two: false, + three: false, + }); + const [warnings, setWarnings] = useState({ + one: true, + two: true, + three: false, + }); + const [infos, setInfos] = useState({ one: true, two: true, three: true }); + return ( + + + + + setErrors({ ...errors, one: !errors.one })} + checked={errors.one} + /> + setWarnings({ ...warnings, one: !warnings.one })} + checked={warnings.one} + /> + setInfos({ ...infos, one: !infos.one })} + checked={infos.one} + /> + + + setErrors({ ...errors, two: !errors.two })} + /> + setWarnings({ ...warnings, two: !warnings.two })} + checked={warnings.two} + /> + setInfos({ ...infos, two: !infos.two })} + checked={infos.two} + /> + + + setErrors({ ...errors, three: !errors.three })} + /> + + setWarnings({ ...warnings, three: !warnings.three }) + } + /> + setInfos({ ...infos, three: !infos.three })} + checked={infos.three} + /> + + + + + ); +}; +WithValidationsPositionedLeftRedesign.storyName = + "With New Validation Positioned Left"; + export const WithValidationsSizedLargePositionedLeft: Story = () => { const [errors, setErrors] = useState({ one: true, @@ -1267,6 +1475,82 @@ export const WithAdditionalTitleSiblings: Story = () => { }; WithAdditionalTitleSiblings.storyName = "With Additional Title Siblings"; +export const WithAdditionalTitleSiblingsRedesign: Story = () => { + const [errors, setErrors] = useState({ + one: true, + two: false, + three: false, + }); + return ( + + + + + 12 + , + , + ]} + titlePosition="before" + > + setErrors({ ...errors, one: !errors.one })} + checked={errors.one} + /> + + + setErrors({ ...errors, two: !errors.two })} + /> + + + 12 + , + , + ]} + titlePosition="after" + > + setErrors({ ...errors, three: !errors.three })} + /> + + + + + ); +}; +WithAdditionalTitleSiblingsRedesign.storyName = + "With New Validation Additional Title Siblings"; + export const WithAdditionalTitleSiblingsSizeLarge: Story = () => { const [errors, setErrors] = useState({ one: true, @@ -1446,6 +1730,113 @@ export const WithCustomLayout: Story = () => { }; WithCustomLayout.storyName = "With Custom Layout"; +export const WithCustomLayoutRedesign: Story = () => { + const [errors, setErrors] = useState({ + one: false, + two: false, + three: false, + }); + return ( + + + + + + + + + + Tab 1 + + + } + > + setErrors({ ...errors, one: !errors.one })} + /> + + + + + + + + Tab 2 + + + } + > + setErrors({ ...errors, two: !errors.two })} + /> + + + + + + + + Tab 3 + + + } + > + setErrors({ ...errors, three: !errors.three })} + /> + + + + + ); +}; +WithCustomLayoutRedesign.storyName = "With New Validation Custom Layout"; + export const WithAlternateStyling: Story = () => { const [errors, setErrors] = useState({ one: false, diff --git a/src/components/tabs/tabs.test.tsx b/src/components/tabs/tabs.test.tsx index 4b3c3e5827..bad4d02d3c 100644 --- a/src/components/tabs/tabs.test.tsx +++ b/src/components/tabs/tabs.test.tsx @@ -8,6 +8,7 @@ import { testStyledSystemMarginRTL } from "../../__spec_helper__/__internal__/te import Drawer from "../drawer"; import Textbox from "../textbox"; import NumeralDate from "../numeral-date"; +import CarbonProvider from "../carbon-provider/carbon-provider.component"; testStyledSystemMarginRTL( (props) => ( @@ -861,6 +862,74 @@ test("when errors and warnings are both present in a tab, only the error icon is ).not.toBeInTheDocument(); }); +test("error and warning icons are displayed correctly when the new validation flag is present and the tabs have position top", async () => { + const user = userEvent.setup(); + render( + + + + Content for tab 1 + {}} /> + + + Content for tab 2 + {}} /> + + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).getByTestId("icon-error") + ).toBeInTheDocument(); + + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId( + "icon-warning" + ) + ).toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Tab 1" })); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + + await user.click(screen.getByRole("tab", { name: "Tab 2" })); + expect(screen.getByText("Content for tab 2")).toBeVisible(); +}); + +test("error and warning icons are displayed correctly when the new validation flag is present and the tabs have position left", async () => { + const user = userEvent.setup(); + render( + + + + Content for tab 1 + {}} /> + + + Content for tab 2 + {}} /> + + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).getByTestId("icon-error") + ).toBeInTheDocument(); + + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId( + "icon-warning" + ) + ).toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Tab 1" })); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + + await user.click(screen.getByRole("tab", { name: "Tab 2" })); + expect(screen.getByText("Content for tab 2")).toBeVisible(); +}); + test("when errors, warnings and infos are all present in a tab, only the error icon is displayed in the corresponding tab title", () => { render(