diff --git a/cypress/support/step-definitions/tabs-steps.js b/cypress/support/step-definitions/tabs-steps.js index c32ef10141..a1c7b7d945 100644 --- a/cypress/support/step-definitions/tabs-steps.js +++ b/cypress/support/step-definitions/tabs-steps.js @@ -14,7 +14,6 @@ Then("Tab {int} content is visible", (id) => { Then("Second Tab has a link property", () => { tabById(2) - .find("a") .should("have.attr", "href", "https://carbon.sage.com/") .and("have.attr", "target", "_blank"); }); diff --git a/src/components/tabs/__internal__/tab-title/index.d.ts b/src/components/tabs/__internal__/tab-title/index.d.ts new file mode 100644 index 0000000000..ecb72c82ac --- /dev/null +++ b/src/components/tabs/__internal__/tab-title/index.d.ts @@ -0,0 +1 @@ +export { default } from "./tab-title"; diff --git a/src/components/tabs/__internal__/tab-title/tab-title.component.js b/src/components/tabs/__internal__/tab-title/tab-title.component.js index d99620eebd..12969d0986 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.component.js +++ b/src/components/tabs/__internal__/tab-title/tab-title.component.js @@ -37,6 +37,7 @@ const TabTitle = React.forwardRef( isInSidebar, href, onKeyDown, + align, ...tabTitleProps }, ref @@ -73,6 +74,9 @@ const TabTitle = React.forwardRef( return window.open(href, "_blank"); } + // safari does not focus buttons by default + ref.current?.focus(); + return onClick(customEvent); }; @@ -121,17 +125,16 @@ const TabTitle = React.forwardRef( error={error} warning={warning} info={info} - size={size} noRightBorder={noRightBorder} alternateStyling={alternateStyling || isInSidebar} borders={borders} isInSidebar={isInSidebar} {...tabTitleProps} + {...(isHref && { href, target: "_blank", as: "a" })} {...tagComponent("tab-header", tabTitleProps)} onKeyDown={handleKeyDown} > {renderContent()} {isHref && } - - {error && ( - - )} - - {!error && warning && ( - - )} - - {!warning && !error && info && ( - - )} - + {hasFailedValidation && ( + + {error && ( + + )} + + {!error && warning && ( + + )} + + {!warning && !error && info && ( + + )} + + )} {!(hasFailedValidation || hasAlternateStyling) && isTabSelected && ( + ) => void; + onKeyDown?: ( + ev: React.KeyboardEvent + ) => void; +} + +declare function TabTitle(props: TabTitleProps): JSX.Element; + +export default TabTitle; diff --git a/src/components/tabs/__internal__/tab-title/tab-title.spec.js b/src/components/tabs/__internal__/tab-title/tab-title.spec.js index f0b6972163..e764630ba1 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.spec.js +++ b/src/components/tabs/__internal__/tab-title/tab-title.spec.js @@ -28,7 +28,7 @@ describe("TabTitle", () => { backgroundColor: "transparent", display: "inline-block", fontWeight: "bold", - height: "100%", + height: "40px", }, render({}, mount).find(StyledTabTitle) ); @@ -131,7 +131,7 @@ describe("TabTitle", () => { marginLeft: "-1px", }, wrapper.find(StyledTabTitle), - { modifier: ":not(:first-of-type)" } + { modifier: ":nth-of-type(n + 1)" } ); }); @@ -148,7 +148,7 @@ describe("TabTitle", () => { ); assertStyleMatch( - { paddingBottom: "6px" }, + { paddingBottom: "9px" }, wrapper.find(StyledTitleContent) ); @@ -203,7 +203,7 @@ describe("TabTitle", () => { backgroundColor: "transparent", borderBottom: "0px", borderRight: `2px solid ${baseTheme.tab.background}`, - display: "block", + display: "flex", height: "auto", marginLeft: "0px", }, @@ -227,6 +227,24 @@ describe("TabTitle", () => { ); }); + it("renders as expected when `align='left'`", () => { + assertStyleMatch( + { justifyContent: "flex-start", textAlign: "left" }, + render({ position: "left", align: "left" }, mount).find( + StyledTitleContent + ) + ); + }); + + it("renders as expected when `align='right'`", () => { + assertStyleMatch( + { justifyContent: "flex-end", textAlign: "right" }, + render({ align: "right", position: "left" }, mount).find( + StyledTitleContent + ) + ); + }); + it('applies proper styling when size is "large"', () => { wrapper = render({ position: "left", size: "large" }, mount); @@ -261,7 +279,7 @@ describe("TabTitle", () => { marginTop: "-1px", }, wrapper.find(StyledTabTitle), - { modifier: ":not(:first-of-type)" } + { modifier: ":nth-of-type(n + 1)" } ); assertStyleMatch( @@ -308,15 +326,6 @@ describe("TabTitle", () => { ); }); - it("applies proper styling when size is large", () => { - wrapper = render({ isTabSelected: true, size: "large" }, mount); - - assertStyleMatch( - { paddingBottom: "6px" }, - wrapper.find(StyledTitleContent) - ); - }); - it("does not apply selected styling", () => { wrapper = render({ isTabSelected: true, error: true }, mount); @@ -424,29 +433,6 @@ describe("TabTitle", () => { ); }); - it('adjusts padding when isTabSelected is true and position is "top"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: [foo, bar], - titlePosition: "before", - isTabSelected: true, - }, - mount - ); - - expect(wrapper.find(StyledTitleContent).props().hasSiblings).toEqual( - true - ); - expect( - wrapper.find(StyledTitleContent).props().children[0][0].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { paddingBottom: "8px" }, - wrapper.find(StyledTitleContent) - ); - }); - it('does not adjust padding when isTabSelected is true and position is "left"', () => { wrapper = render( { @@ -1077,7 +1063,10 @@ describe("TabTitle", () => { stopPropagation, target: { dataset: { tabid: "uniqueid1" } }, }; - wrapper = render({ onClick }, mount); + wrapper = render( + { onClick, ref: { current: { focus: jest.fn() } } }, + mount + ); wrapper .find(StyledTitleContent) diff --git a/src/components/tabs/__internal__/tab-title/tab-title.style.js b/src/components/tabs/__internal__/tab-title/tab-title.style.js index 046f48e887..9107eab49e 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.style.js +++ b/src/components/tabs/__internal__/tab-title/tab-title.style.js @@ -4,8 +4,9 @@ import BaseTheme from "../../../../style/themes/base"; import StyledIcon from "../../../icon/icon.style"; import StyledValidationIcon from "../../../../__internal__/validations/validation-icon.style"; -const StyledTitleContent = styled.div` +const StyledTitleContent = styled.span` outline: none; + display: inline-block; ${({ theme, @@ -15,17 +16,23 @@ const StyledTitleContent = styled.div` noLeftBorder, noRightBorder, isTabSelected, - hasSiblings, - href, + hasHref, error, - warning, - info, alternateStyling, + align, }) => css` line-height: 20px; margin: 0; + text-align: ${align}; + + ${position === "left" && + css` + display: flex; + width: 100%; + justify-content: ${align === "right" ? "flex-end" : "flex-start"}; + `} - ${href && + ${hasHref && css` color: ${theme.text.color}; display: block; @@ -72,13 +79,8 @@ const StyledTitleContent = styled.div` position === "top" && css` padding: 10px 24px; + ${borders && `padding-bottom: 9px;`} font-size: 16px; - ${isTabSelected && - !hasSiblings && - !(error || warning || info) && - css` - padding-bottom: 6px; - `} `} ${size === "large" && @@ -92,18 +94,16 @@ const StyledTitleContent = styled.div` ${size === "default" && css` padding: 10px 16px; - ${isTabSelected && - !(error || warning || info) && - position === "top" && - css` - padding-bottom: 8px; - `} + + ${borders && `padding-bottom: 9px;`} ${position === "left" && !isTabSelected && !alternateStyling && error && - `margin-right: -2px;`} + ` + margin-right: -2px; + `} `} `} @@ -135,7 +135,7 @@ const StyledTitleContent = styled.div` padding-right: ${size === "large" ? "26px;" : "18px;"}; `} - &:hover { + &:hover { outline: 1px solid; outline-offset: -1px; @@ -182,10 +182,10 @@ const StyledTitleContent = styled.div` ${position === "left" && css` border-right-color: transparent; - padding-right: ${size === "large" ? "26px;" : "18px;"}; + padding-right: ${size === "large" ? "26px" : "18px"}; `} - &:hover { + &:hover { outline: 2px solid ${theme.colors.error}; outline-offset: -2px; ${position === "top" && @@ -201,7 +201,7 @@ const StyledTitleContent = styled.div` ${position === "left" && css` border-right-color: transparent; - padding-right: ${size === "large" ? "26px;" : "18px;"}; + padding-right: ${size === "large" ? "26px" : "18px"}; `} } `} @@ -221,29 +221,14 @@ const StyledTitleContent = styled.div` position === "top" && css` height: 20px; - - ${size === "default" && - css` - padding-top: 10px; - padding-bottom: 10px; - - ${!(error || warning || info) && - isTabSelected && - css` - padding-bottom: 8px; - `} - `} + padding-top: 10px; + padding-bottom: 10px; ${size === "large" && + !(error || warning || info) && + isTabSelected && css` - padding-top: 10px; - padding-bottom: 10px; - - ${!(error || warning || info) && - isTabSelected && - css` - padding-bottom: 6px; - `} + padding-bottom: 6px; `} `} @@ -262,15 +247,15 @@ const StyledTitleContent = styled.div` ${position === "left" && css` - padding: ${size === "large" ? "2px;" : "0px;"} - ${isTabSelected && - css` - padding-right: 0px; - `} - ${(error || warning || info) && - css` - padding-right: ${size === "large" ? "26px" : "18px"}; - `}; + padding: ${size === "large" ? "2px" : "0px"}; + ${isTabSelected && + css` + padding-right: 0px; + `} + ${(error || warning || info) && + css` + padding-right: ${size === "large" ? "26px" : "18px"}; + `} `} ${position === "top" && @@ -282,53 +267,63 @@ const StyledTitleContent = styled.div` `} ${(error || warning || info) && css` - padding-bottom: ${size === "large" ? "4px;" : "2px;"} - padding-right: ${size === "large" ? "18px;" : "14px;"} - - &:hover { padding-bottom: ${size === "large" ? "4px;" : "2px;"} - } - `}; + padding-right: ${size === "large" ? "18px;" : "14px;"} + + &:hover { + padding-bottom: ${size === "large" ? "4px;" : "2px;"} + } + `}; `} `} `; -const StyledTabTitle = styled.li` +const StyledTabTitle = styled.button` background-color: transparent; display: inline-block; font-weight: bold; - height: 100%; position: relative; + border: none; + cursor: pointer; + font-size: 14px; + padding: 0px; + text-decoration: none; + outline-offset: 0px; + margin: 0; + + a:visited { + color: inherit; + } - ${({ position, borders, noRightBorder, noLeftBorder }) => ` - ${ - position === "top" && - css` - ${borders && - !(noRightBorder || noLeftBorder) && - css` - &:not(:first-of-type) { - margin-left: -1px; - } - `} - ` - } - ${ - position === "left" && - css` - ${borders && - css` - &:not(:first-of-type) { - margin-top: -1px; - } - `} - ` - } - `} + ${({ position, borders, noRightBorder, noLeftBorder }) => css` + ${position === "top" && + css` + height: 40px; - &:first-child { - margin-left: 0; - } + ${borders && + !(noRightBorder || noLeftBorder) && + css` + &:nth-of-type(n + 1) { + margin-left: -1px; + } + &:first-child { + margin-left: 0; + } + `} + `} + ${position === "left" && + css` + ${borders && + css` + &:nth-of-type(n + 1) { + margin-top: -1px; + } + &:first-child { + margin-top: 0; + } + `} + `} + `} ${({ isTabSelected, theme }) => !isTabSelected && @@ -351,9 +346,7 @@ const StyledTabTitle = styled.li` error, warning, info, - size, isInSidebar, - position, }) => isTabSelected && css` @@ -361,29 +354,6 @@ const StyledTabTitle = styled.li` color: ${theme.text.color}; background-color: ${theme.colors.white}; - ${ - alternateStyling && - css` - border-bottom: 2px solid ${theme.tab.background}; - ` - } - - ${ - !alternateStyling && - css` - padding-bottom: 2px; - ` - } - - ${ - size === "large" && - css` - ${position === "top" && - ` - padding-bottom: ${alternateStyling ? "3px" : "4px"}; - `} - ` - } ${ (error || warning || info) && css` @@ -412,7 +382,6 @@ const StyledTabTitle = styled.li` ${({ position, - size, borders, theme, alternateStyling, @@ -439,7 +408,7 @@ const StyledTabTitle = styled.li` } `} - display: block; + display: flex; height: auto; margin-left: 0px; @@ -477,13 +446,6 @@ const StyledTabTitle = styled.li` background-color: ${theme.colors.white}; - ${size === "large" && - css` - & ${StyledTitleContent} { - padding-right: 22px; - } - `} - &:hover { ${alternateStyling && ` border-right-color: ${theme.tab.background};`} @@ -529,7 +491,7 @@ const StyledLayoutWrapper = styled.div` min-width: 100px; `} - ${({ hasCustomLayout, titlePosition, hasCustomSibling }) => + ${({ hasCustomLayout, titlePosition, hasCustomSibling, position }) => !hasCustomLayout && css` display: inline-flex; @@ -559,7 +521,7 @@ const StyledLayoutWrapper = styled.div` ${StyledIcon} { height: 16px; left: -2px; - top: 3px; + top: ${position === "left" ? "1px" : "3px"}; } } `} diff --git a/src/components/tabs/__internal__/tabs-header/index.d.ts b/src/components/tabs/__internal__/tabs-header/index.d.ts new file mode 100644 index 0000000000..36fbda7ccf --- /dev/null +++ b/src/components/tabs/__internal__/tabs-header/index.d.ts @@ -0,0 +1 @@ +export { default } from "./tab-header"; diff --git a/src/components/tabs/__internal__/tabs-header/tab-header.d.ts b/src/components/tabs/__internal__/tabs-header/tab-header.d.ts new file mode 100644 index 0000000000..3435c46afd --- /dev/null +++ b/src/components/tabs/__internal__/tabs-header/tab-header.d.ts @@ -0,0 +1,16 @@ +import * as React from "react"; + +export interface TabHeaderProps { + role?: string; + position?: "top" | "left"; + extendedLine?: boolean; + noRightBorder?: boolean; + alternateStyling?: boolean; + isInSidebar?: boolean; + children: React.ReactNode; + align?: "left" | "right"; +} + +declare function TabHeader(props: TabHeaderProps): JSX.Element; + +export default TabHeader; diff --git a/src/components/tabs/__internal__/tabs-header/tabs-header.style.js b/src/components/tabs/__internal__/tabs-header/tabs-header.style.js index 55ea6485a1..bceda7847f 100644 --- a/src/components/tabs/__internal__/tabs-header/tabs-header.style.js +++ b/src/components/tabs/__internal__/tabs-header/tabs-header.style.js @@ -31,7 +31,7 @@ const StyledTabsHeaderWrapper = styled.div` `} `; -const StyledTabsHeaderList = styled.ul` +const StyledTabsHeaderList = styled.div` display: flex; box-shadow: inset 0px ${computeLineWidth} 0px 0px ${({ theme }) => theme.tab.background}; diff --git a/src/components/tabs/tabs.component.js b/src/components/tabs/tabs.component.js index 5de643413c..8be3e9888c 100644 --- a/src/components/tabs/tabs.component.js +++ b/src/components/tabs/tabs.component.js @@ -257,6 +257,7 @@ const Tabs = ({ noRightBorder={["no right side", "no sides"].includes(borders)} customLayout={customLayout} isInSidebar={isInSidebar} + align={align} onFocus={() => { if (!hasTabStop(tabId)) { setTabStopId(tabId); diff --git a/src/components/tabs/tabs.stories.mdx b/src/components/tabs/tabs.stories.mdx index 6b6a34e9b0..fcb6ff41cf 100644 --- a/src/components/tabs/tabs.stories.mdx +++ b/src/components/tabs/tabs.stories.mdx @@ -5,7 +5,6 @@ import { Tabs, Tab } from "./tabs.component"; import { Checkbox } from "../checkbox"; import Icon from "../icon"; import Pill from "../pill"; -import { ActionPopover, ActionPopoverItem } from "../action-popover"; import StyledSystemProps from "../../../.storybook/utils/styled-system-props"; @@ -1484,11 +1483,7 @@ prop to the `Tab` component.
- - {}}> - Edit - - +
Tab 1
@@ -1513,11 +1508,7 @@ prop to the `Tab` component.
- - {}}> - Edit - - +
Tab 2
@@ -1542,11 +1533,7 @@ prop to the `Tab` component.
- - {}}> - Edit - - +
Tab 3
@@ -1643,9 +1630,9 @@ The `headerWidth` prop works only if prop `position` is set to `left`. errorMessage="error" warningMessage="warning" infoMessage="info" - tabId="tab-1" + tabId="tabs-1-tab-1" title="Very long title for Tab 1 without with prop it would be not well aligned with the second Tabs group" - key="tab-1" + key="tabs-1-tab-1" > Content for tab 1 @@ -1653,9 +1640,9 @@ The `headerWidth` prop works only if prop `position` is set to `left`. errorMessage="error" warningMessage="warning" infoMessage="info" - tabId="tab-2" + tabId="tabs-1-tab-2" title="Tab 2" - key="tab-2" + key="tabs-1-tab-2" > Content for tab 2 @@ -1665,9 +1652,9 @@ The `headerWidth` prop works only if prop `position` is set to `left`. errorMessage="error" warningMessage="warning" infoMessage="info" - tabId="tab-1" + tabId="tabs-2-tab-1" title="Tab 1" - key="tab-1" + key="tabs-2-tab-1" > Content for tab 1 @@ -1675,9 +1662,9 @@ The `headerWidth` prop works only if prop `position` is set to `left`. errorMessage="error" warningMessage="warning" infoMessage="info" - tabId="tab-2" + tabId="tabs-2-tab-2" title="Tab 2" - key="tab-2" + key="tabs-2-tab-2" > Content for tab 2