diff --git a/lib/.storybook/components/ExampleContainer.tsx b/lib/.storybook/components/ExampleContainer.tsx index 06327682a..ed873a6df 100644 --- a/lib/.storybook/components/ExampleContainer.tsx +++ b/lib/.storybook/components/ExampleContainer.tsx @@ -1,9 +1,19 @@ import React from "react"; import styled from "styled-components"; +type PseudoStates = + | "pseudo-active" + | "pseudo-focus" + | "pseudo-focus-visible" + | "pseudo-focus-within" + | "pseudo-hover" + | "pseudo-link" + | "pseudo-target" + | "pseudo-visited"; + type Props = { children?: React.ReactNode; - pseudoState?: string; + pseudoState?: PseudoStates; expanded?: boolean; }; diff --git a/lib/package-lock.json b/lib/package-lock.json index d60c670ed..4a06dc308 100644 --- a/lib/package-lock.json +++ b/lib/package-lock.json @@ -36,6 +36,8 @@ "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.0.0", "@types/color": "^3.0.3", + "@types/jest": "^29.5.12", + "@types/jest-axe": "^3.5.9", "@types/react": "^18.0.18", "@types/styled-components": "5.1.29", "@types/uuid": "^9.0.6", @@ -9364,6 +9366,61 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest-axe": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@types/jest-axe/-/jest-axe-3.5.9.tgz", + "integrity": "sha512-z98CzR0yVDalCEuhGXXO4/zN4HHuSebAukXDjTLJyjEAgoUf1H1i+sr7SUB/mz8CRS/03/XChsx0dcLjHkndoQ==", + "dev": true, + "dependencies": { + "@types/jest": "*", + "axe-core": "^3.5.5" + } + }, + "node_modules/@types/jest-axe/node_modules/axe-core": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.6.tgz", + "integrity": "sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", diff --git a/lib/package.json b/lib/package.json index 371263a22..ae93c9645 100644 --- a/lib/package.json +++ b/lib/package.json @@ -56,6 +56,8 @@ "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.0.0", "@types/color": "^3.0.3", + "@types/jest": "^29.5.12", + "@types/jest-axe": "^3.5.9", "@types/react": "^18.0.18", "@types/styled-components": "5.1.29", "@types/uuid": "^9.0.6", diff --git a/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx b/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx new file mode 100644 index 000000000..98d82bb63 --- /dev/null +++ b/lib/src/breadcrumbs/Breadcrumbs.accessibility.test.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { axe } from "../../test/accessibility/axe-helper.js"; +import DxcBreadcrumbs from "./Breadcrumbs"; +import { disabledRules as rules } from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules.js"; + +const disabledRules = { + rules: rules.reduce((rulesObj, rule) => { + rulesObj[rule] = { enabled: false }; + return rulesObj; + }, {}), +}; + +const items = [ + { + label: "Home", + href: "/", + }, + { + label: "User Menu", + href: "", + }, + { + label: "Preferences", + href: "", + }, + { + label: "Dark Mode", + href: "", + }, +]; + +describe("Breadcrumbs component accessibility tests", () => { + it("Should not have basic accessibility issues", async () => { + const { container } = render(); + const results = await axe(container, disabledRules); + expect(results).toHaveNoViolations(); + }); + it("Should not have basic accessibility issues when collapsed", async () => { + const { container } = render(); + const results = await axe(container, disabledRules); + expect(results).toHaveNoViolations(); + }); + it("Should not have basic accessibility issues without root", async () => { + const { container } = render( + + ); + const results = await axe(container, disabledRules); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/lib/src/breadcrumbs/Breadcrumbs.stories.tsx b/lib/src/breadcrumbs/Breadcrumbs.stories.tsx new file mode 100644 index 000000000..31187d644 --- /dev/null +++ b/lib/src/breadcrumbs/Breadcrumbs.stories.tsx @@ -0,0 +1,194 @@ +import React from "react"; +import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import DxcBreadcrumbs from "./Breadcrumbs"; +import DxcContainer from "../container/Container"; +import { HalstackProvider } from "../HalstackContext"; +import { userEvent, within } from "@storybook/testing-library"; +import { disabledRules } from "../../test/accessibility/rules/specific/breadcrumbs/disabledRules"; +import preview from "../../.storybook/preview"; + +export default { + title: "Breadcrumbs", + component: DxcBreadcrumbs, + parameters: { + a11y: { + config: { + rules: [ + ...disabledRules.map((ruleId) => ({ id: ruleId, enabled: false })), + ...preview?.parameters?.a11y?.config?.rules, + ], + }, + }, + }, +}; + +const items = [ + { + label: "Home", + href: "/", + }, + { + label: "User Menu", + href: "", + }, + { + label: "Preferences", + href: "", + }, + { + label: "Customization", + href: "", + }, + { + label: "Dark Mode", + href: "", + }, +]; + +const Breadcrumbs = () => ( + <> + + <ExampleContainer> + <DxcBreadcrumbs + items={[ + { + label: "Home", + href: "/", + }, + { + label: "User Menu", + href: "", + }, + { + label: "Preferences", + href: "", + }, + { + label: "Dark Mode", + href: "", + }, + ]} + /> + </ExampleContainer> + <Title title="Collapsed variant" theme="light" level={3} /> + <ExampleContainer> + <DxcBreadcrumbs items={items} /> + </ExampleContainer> + <Title title="Collapsed variant without root" theme="light" level={3} /> + <ExampleContainer> + <DxcBreadcrumbs items={items} showRoot={false} /> + </ExampleContainer> + <Title title="Collapsed variant with dropdown menu opened" theme="light" level={3} /> + <ExampleContainer> + <DxcContainer height="150px"> + <DxcBreadcrumbs items={items} /> + </DxcContainer> + </ExampleContainer> + <Title title="Focus state" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-focus"> + <DxcBreadcrumbs items={items} /> + </ExampleContainer> + <Title title="Hover state" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-hover"> + <DxcBreadcrumbs items={items} /> + </ExampleContainer> + <Title title="Active state" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-active"> + <DxcBreadcrumbs items={items} /> + </ExampleContainer> + <Title title="Truncation and text ellipsis with tooltip (only when collapsed)" theme="light" level={3} /> + <ExampleContainer> + <DxcContainer width="200px"> + <DxcBreadcrumbs + items={[ + { + label: "Root", + href: "/", + }, + { + label: "Main folder", + href: "", + }, + { + label: "User", + href: "", + }, + { + label: "Very long label for the link", + href: "", + }, + ]} + itemsBeforeCollapse={3} + /> + </DxcContainer> + </ExampleContainer> + <Title title="Truncation, text ellipsis with tooltip and without root" theme="light" level={3} /> + <ExampleContainer> + <DxcContainer width="200px"> + <DxcBreadcrumbs + items={[ + { + label: "Root", + href: "/", + }, + { + label: "Main folder", + href: "", + }, + { + label: "User", + href: "", + }, + { + label: "Very long label for the link", + href: "", + }, + ]} + itemsBeforeCollapse={3} + showRoot={false} + /> + </DxcContainer> + </ExampleContainer> + <Title title="Dropdown theming doesn't affect the collapsed trigger" theme="light" level={3} /> + <ExampleContainer> + <Title title="Opinionated theming" theme="light" level={4} /> + <ExampleContainer> + <HalstackProvider + theme={{ + dropdown: { + baseColor: "#fabada", + fontColor: "#999", + optionFontColor: "#4d4d4d", + }, + }} + > + <DxcBreadcrumbs items={items} itemsBeforeCollapse={3} /> + </HalstackProvider> + </ExampleContainer> + <Title title="Advanced theming" theme="light" level={4} /> + <ExampleContainer> + <HalstackProvider + advancedTheme={{ + dropdown: { + buttonBackgroundColor: "#fabada", + buttonHeight: "100px", + buttonBorderThickness: "2px", + buttonBorderStyle: "solid", + buttonBorderColor: "#000", + }, + }} + > + <DxcBreadcrumbs items={items} itemsBeforeCollapse={3} /> + </HalstackProvider> + </ExampleContainer> + </ExampleContainer> + </> +); + +export const Chromatic = Breadcrumbs.bind({}); +Chromatic.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + const dropdowns = canvas.getAllByRole("button"); + await userEvent.click(dropdowns[2]); +}; diff --git a/lib/src/breadcrumbs/Breadcrumbs.test.tsx b/lib/src/breadcrumbs/Breadcrumbs.test.tsx new file mode 100644 index 000000000..199a62069 --- /dev/null +++ b/lib/src/breadcrumbs/Breadcrumbs.test.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import DxcBreadcrumbs from "./Breadcrumbs"; +import userEvent from "@testing-library/user-event"; + +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + +const items = [ + { + label: "Home", + href: "/", + }, + { + label: "User Menu", + href: "", + }, + { + label: "Preferences", + href: "", + }, + { + label: "Dark Mode", + href: "", + }, +]; + +describe("Breadcrumbs component tests", () => { + test("Renders with correct aria accessibility attributes", () => { + const { getByText, getByRole } = render(<DxcBreadcrumbs items={items} ariaLabel="example" />); + const breadcrumbs = getByRole("navigation"); + expect(breadcrumbs.getAttribute("aria-label")).toBe("example"); + expect(getByText("Dark Mode").parentElement.getAttribute("aria-current")).toBe("page"); + }); + test("Collapsed variant renders all the items inside the dropdown menu except the root and the current page", async () => { + const { queryByText, getByText, getByRole } = render(<DxcBreadcrumbs items={items} itemsBeforeCollapse={3} />); + const dropdown = getByRole("button"); + expect(queryByText("User Menu")).toBeFalsy(); + expect(queryByText("Preferences")).toBeFalsy(); + await userEvent.click(dropdown); + expect(getByText("User Menu")).toBeTruthy(); + expect(getByText("Preferences")).toBeTruthy(); + }); + test("Collapsed variant, with show root set to false, renders all the items inside the dropdown menu except the current page", async () => { + const { queryByText, getByText, getByRole } = render( + <DxcBreadcrumbs items={items} itemsBeforeCollapse={3} showRoot={false} /> + ); + const dropdown = getByRole("button"); + expect(queryByText("Home")).toBeFalsy(); + expect(queryByText("User Menu")).toBeFalsy(); + expect(queryByText("Preferences")).toBeFalsy(); + await userEvent.click(dropdown); + expect(getByText("Home")).toBeTruthy(); + expect(getByText("User Menu")).toBeTruthy(); + expect(getByText("Preferences")).toBeTruthy(); + }); + test("If itemsBeforeCollapse value is below two, ignores it and renders a collapsed variant", async () => { + const { getByText, getByRole } = render(<DxcBreadcrumbs items={items} itemsBeforeCollapse={-1} />); + expect(getByText("Home")).toBeTruthy(); + expect(getByRole("button")).toBeTruthy(); + expect(getByText("Dark Mode")).toBeTruthy(); + }); + test("The onClick prop from an item is properly called", () => { + const onItemClick = jest.fn(); + const { getByText } = render( + <DxcBreadcrumbs + onItemClick={onItemClick} + items={[ + { label: "Home", href: "/home" }, + { label: "Preferences", href: "/preferences" }, + ]} + /> + ); + userEvent.click(getByText("Home")); + expect(onItemClick).toHaveBeenCalledWith("/home"); + }); + test("The onClick prop from an item is properly called (collapsed)", async () => { + const onItemClick = jest.fn(); + const { getByText, getByRole } = render( + <DxcBreadcrumbs + onItemClick={onItemClick} + items={[ + { label: "Home", href: "/" }, + { label: "Preferences", href: "/" }, + { label: "Dark Mode", href: "/" }, + ]} + itemsBeforeCollapse={2} + /> + ); + await userEvent.click(getByRole("button")); + await userEvent.click(getByText("Preferences")); + expect(onItemClick).toHaveBeenCalledWith("/"); + }); +}); diff --git a/lib/src/breadcrumbs/Breadcrumbs.tsx b/lib/src/breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 000000000..82eea9eac --- /dev/null +++ b/lib/src/breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; +import BreadcrumbsProps from "./types"; +import DxcDropdown from "../dropdown/Dropdown"; +import { HalstackProvider } from "../HalstackContext"; +import dropdownTheme from "./dropdownTheme"; +import CoreTokens from "../common/coreTokens"; +import DxcIcon from "../icon/Icon"; +import Item from "./Item"; +import DxcFlex from "../flex/Flex"; + +const DxcBreadcrumbs = ({ + ariaLabel = "Breadcrumbs", + items, + itemsBeforeCollapse = 4, + onItemClick, + showRoot = true, +}: BreadcrumbsProps) => { + const handleOnSelectOption = useCallback( + (href: string) => { + if (onItemClick) onItemClick(href); + else window.location.href = href; + }, + [items] + ); + + return ( + <nav aria-label={ariaLabel}> + <OrderedList> + {items && items.length > Math.max(itemsBeforeCollapse, 2) ? ( + <> + {showRoot && <Item href={items[0].href} key={0} label={items[0].label} />} + <DxcFlex alignItems="center" as="li" key={1}> + <HalstackProvider advancedTheme={dropdownTheme}> + <DxcDropdown + caretHidden + icon={<DxcIcon icon="more_horiz" />} + margin={showRoot && { left: "small" }} + onSelectOption={handleOnSelectOption} + options={items.slice(showRoot ? 1 : 0, -1).map(({ label, href }) => ({ label, value: href }))} + /> + </HalstackProvider> + </DxcFlex> + <Item isCurrentPage key={2} label={items[items.length - 1].label} /> + </> + ) : ( + items.map((item, index, { length }) => ( + <Item + href={item.href} + isCurrentPage={index === length - 1} + key={index} + label={item.label} + onClick={onItemClick} + /> + )) + )} + </OrderedList> + </nav> + ); +}; + +const OrderedList = styled.ol` + margin: ${CoreTokens.spacing_0}; + padding-left: ${CoreTokens.spacing_0}; + display: flex; + align-items: center; + gap: ${CoreTokens.spacing_12}; + list-style-type: none; + + > li:not(:first-child) { + > a, + > span { + margin-left: ${CoreTokens.spacing_12}; + } + &::before { + margin: ${CoreTokens.spacing_0} ${CoreTokens.spacing_2}; + transform: rotate(15deg); + border-right: ${CoreTokens.border_width_1} solid ${CoreTokens.color_grey_500}; + height: 1rem; + content: ""; + } + } +`; + +export default DxcBreadcrumbs; diff --git a/lib/src/breadcrumbs/Item.tsx b/lib/src/breadcrumbs/Item.tsx new file mode 100644 index 000000000..f608967fa --- /dev/null +++ b/lib/src/breadcrumbs/Item.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import styled from "styled-components"; +import CoreTokens from "../common/coreTokens"; +import { ItemPropsType } from "./types"; +import { useRef } from "react"; + +const Item = ({ isCurrentPage = false, href, label, onClick }: ItemPropsType) => { + const currentItemRef = useRef<HTMLSpanElement>(null); + + const handleOnMouseEnter = (event: React.MouseEvent<HTMLAnchorElement>) => { + const labelContainer = event.currentTarget; + const optionElement = currentItemRef?.current; + if (optionElement.title === "" && labelContainer.scrollWidth > labelContainer.clientWidth) + optionElement.title = label; + }; + + const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement>) => { + if (onClick) { + event.preventDefault(); + onClick(href); + } + }; + + return ( + <ListItem aria-current={isCurrentPage ? "page" : undefined} isCurrentPage={isCurrentPage}> + {isCurrentPage ? ( + <CurrentPage ref={currentItemRef} onMouseEnter={handleOnMouseEnter}> + {label} + </CurrentPage> + ) : ( + <Link href={href} onClick={handleOnClick}> + <Text>{label}</Text> + </Link> + )} + </ListItem> + ); +}; + +const ListItem = styled.li<{ isCurrentPage?: ItemPropsType["isCurrentPage"] }>` + display: flex; + align-items: center; + font-family: ${CoreTokens.type_sans}; + font-size: ${CoreTokens.type_scale_02}; + color: ${CoreTokens.color_black}; + ${({ isCurrentPage }) => isCurrentPage && "overflow: hidden;"} +`; + +const CurrentPage = styled.span` + font-weight: ${CoreTokens.type_semibold}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; +`; + +const Link = styled.a` + border-radius: ${CoreTokens.border_radius_small}; + padding: ${CoreTokens.spacing_0} ${CoreTokens.spacing_2}; + display: inline-flex; + align-items: center; + height: 24px; + color: ${CoreTokens.color_black}; + text-decoration: ${CoreTokens.type_no_line}; + cursor: pointer; + + &:focus { + outline: ${CoreTokens.border_width_2} solid ${CoreTokens.color_blue_600}; + } +`; + +const Text = styled.span` + border: ${CoreTokens.border_width_1} solid ${CoreTokens.color_transparent}; + &:hover { + border-bottom-color: ${CoreTokens.color_black}; + } +`; + +export default Item; diff --git a/lib/src/breadcrumbs/dropdownTheme.ts b/lib/src/breadcrumbs/dropdownTheme.ts new file mode 100644 index 000000000..5fe908d28 --- /dev/null +++ b/lib/src/breadcrumbs/dropdownTheme.ts @@ -0,0 +1,57 @@ +import CoreTokens from "../common/coreTokens"; + +export default { + dropdown: { + // Breadcrumbs tokens + buttonIconSize: CoreTokens.spacing_16, + buttonPaddingTop: CoreTokens.spacing_4, + buttonPaddingBottom: CoreTokens.spacing_4, + buttonPaddingLeft: CoreTokens.spacing_4, + buttonPaddingRight: CoreTokens.spacing_4, + buttonHeight: "24px", + buttonBorderRadius: "2px", + buttonBorderColor: CoreTokens.color_transparent, + optionFontSize: "14px", + optionPaddingTop: CoreTokens.spacing_0, + optionPaddingBottom: CoreTokens.spacing_0, + optionPaddingLeft: CoreTokens.spacing_16, + optionPaddingRight: CoreTokens.spacing_16, + + // Dropdown tokens + buttonBackgroundColor: CoreTokens.color_white, + hoverButtonBackgroundColor: CoreTokens.color_grey_100, + activeButtonBackgroundColor: CoreTokens.color_grey_300, + buttonFontFamily: CoreTokens.type_sans, + buttonFontSize: CoreTokens.type_scale_03, + buttonFontStyle: CoreTokens.type_normal, + buttonFontWeight: CoreTokens.type_regular, + buttonFontColor: CoreTokens.color_black, + buttonIconSpacing: "10px", + buttonIconColor: CoreTokens.color_black, + buttonBorderStyle: CoreTokens.border_none, + buttonBorderThickness: CoreTokens.border_width_0, + disabledColor: CoreTokens.color_grey_500, + disabledButtonBackgroundColor: CoreTokens.color_transparent, + disabledButtonBorderColor: CoreTokens.color_transparent, + optionBackgroundColor: CoreTokens.color_white, + hoverOptionBackgroundColor: CoreTokens.color_grey_100, + activeOptionBackgroundColor: CoreTokens.color_grey_300, + optionFontFamily: CoreTokens.type_sans, + optionFontStyle: CoreTokens.type_normal, + optionFontWeight: CoreTokens.type_regular, + optionFontColor: CoreTokens.color_black, + optionIconSize: "20px", + optionIconSpacing: "10px", + optionIconColor: CoreTokens.color_black, + caretIconSize: "24px", + caretIconColor: CoreTokens.color_black, + caretIconSpacing: "12px", + borderRadius: "4px", + borderStyle: CoreTokens.border_none, + borderThickness: CoreTokens.border_width_0, + borderColor: CoreTokens.color_transparent, + scrollBarThumbColor: CoreTokens.color_grey_700, + scrollBarTrackColor: CoreTokens.color_grey_300, + focusColor: CoreTokens.color_blue_600, + }, +}; diff --git a/lib/src/breadcrumbs/types.ts b/lib/src/breadcrumbs/types.ts new file mode 100644 index 000000000..5748cc0ac --- /dev/null +++ b/lib/src/breadcrumbs/types.ts @@ -0,0 +1,18 @@ +type Item = { + href?: string; + label: string; +}; +type Props = { + ariaLabel?: string; + items: Array<Item>; + itemsBeforeCollapse?: number; + onItemClick?: (href: string) => void; + showRoot?: boolean; +}; + +export type ItemPropsType = Item & { + isCurrentPage?: boolean; + onClick?: (href: string) => void; +}; + +export default Props; diff --git a/lib/src/common/coreTokens.ts b/lib/src/common/coreTokens.ts index b145250d9..750720805 100644 --- a/lib/src/common/coreTokens.ts +++ b/lib/src/common/coreTokens.ts @@ -109,10 +109,10 @@ export const getCoreColorToken = (key: CoreColorTokens) => CoreColorTokens[key]; export type CoreColorTokens = keyof typeof CoreColorTokens; /** - * Halstack Spacing Principles + * Halstack Spacing Values * @link https://developer.dxc.com/halstack/next/principles/spacing/ */ -const SpacingTokens = { +const CoreSpacingTokens = { spacing_0: "0rem", spacing_2: "0.125rem", spacing_4: "0.25rem", @@ -132,7 +132,7 @@ const SpacingTokens = { const CoreTokens = { ...CoreColorTokens, - ...SpacingTokens, + ...CoreSpacingTokens, inherit: "inherit", diff --git a/lib/src/common/variables.ts b/lib/src/common/variables.ts index 231d87804..fd745a7d3 100644 --- a/lib/src/common/variables.ts +++ b/lib/src/common/variables.ts @@ -14,18 +14,18 @@ export const componentTokens = { assistiveTextFontColor: CoreTokens.color_grey_700, disabledAssistiveTextFontColor: CoreTokens.color_grey_500, assistiveTextMinWidth: "100px", - assistiveTextPaddingRight: "24px", - assistiveTextPaddingLeft: "0px", + assistiveTextPaddingRight: CoreTokens.spacing_24, + assistiveTextPaddingLeft: CoreTokens.spacing_0, titleLabelFontFamily: CoreTokens.type_sans, titleLabelFontSize: CoreTokens.type_scale_03, titleLabelFontWeight: CoreTokens.type_regular, titleLabelFontStyle: CoreTokens.type_normal, titleLabelFontColor: CoreTokens.color_black, disabledTitleLabelFontColor: CoreTokens.color_grey_500, - titleLabelPaddingTop: "0px", - titleLabelPaddingBottom: "0px", - titleLabelPaddingLeft: "0px", - titleLabelPaddingRight: "16px", + titleLabelPaddingTop: CoreTokens.spacing_0, + titleLabelPaddingBottom: CoreTokens.spacing_0, + titleLabelPaddingLeft: CoreTokens.spacing_0, + titleLabelPaddingRight: CoreTokens.spacing_16, focusBorderColor: CoreTokens.color_blue_600, focusBorderStyle: CoreTokens.border_solid, focusBorderThickness: "2px", @@ -37,8 +37,8 @@ export const componentTokens = { iconColor: CoreTokens.color_purple_700, disabledIconColor: CoreTokens.color_grey_500, iconSize: "24px", - iconMarginLeft: "0px", - iconMarginRight: "12px", + iconMarginLeft: CoreTokens.spacing_0, + iconMarginRight: CoreTokens.spacing_12, accordionGroupSeparatorBorderColor: CoreTokens.color_grey_200_a, accordionGroupSeparatorBorderThickness: "1px", accordionGroupSeparatorBorderRadius: "0px", @@ -51,17 +51,17 @@ export const componentTokens = { titleFontStyle: CoreTokens.type_normal, titleFontWeight: CoreTokens.type_bold, titleTextTransform: CoreTokens.type_uppercase, - titlePaddingRight: "0px", - titlePaddingLeft: "0px", + titlePaddingRight: CoreTokens.spacing_0, + titlePaddingLeft: CoreTokens.spacing_0, inlineTextFontFamily: CoreTokens.type_sans, inlineTextFontColor: CoreTokens.color_black, inlineTextFontSize: CoreTokens.type_scale_01, inlineTextFontStyle: CoreTokens.type_normal, inlineTextFontWeight: CoreTokens.type_regular, - inlineTextPaddingLeft: "0px", - inlineTextPaddingRight: "0px", - contentPaddingLeft: "0px", - contentPaddingRight: "0px", + inlineTextPaddingLeft: CoreTokens.spacing_0, + inlineTextPaddingRight: CoreTokens.spacing_0, + contentPaddingLeft: CoreTokens.spacing_0, + contentPaddingRight: CoreTokens.spacing_0, contentPaddingTop: "20px", contentPaddingBottom: "30px", borderRadius: "4px", @@ -72,8 +72,8 @@ export const componentTokens = { warningBorderColor: CoreTokens.color_yellow_700, errorBorderColor: CoreTokens.color_red_700, iconSize: "24px", - iconPaddingLeft: "0px", - iconPaddingRight: "0px", + iconPaddingLeft: CoreTokens.spacing_0, + iconPaddingRight: CoreTokens.spacing_0, infoIconColor: CoreTokens.color_blue_800, successIconColor: CoreTokens.color_green_700, warningIconColor: CoreTokens.color_yellow_700, @@ -115,7 +115,7 @@ export const componentTokens = { bulletIconWidth: "1.5rem", bulletHeight: "5px", bulletWidth: "5px", - bulletMarginRight: "0.5rem", + bulletMarginRight: CoreTokens.spacing_8, }, button: { labelFontLineHeight: CoreTokens.type_leading_normal, @@ -189,7 +189,7 @@ export const componentTokens = { fontColor: CoreTokens.color_black, disabledFontColor: CoreTokens.color_grey_500, focusColor: CoreTokens.color_blue_600, - checkLabelSpacing: "8px", + checkLabelSpacing: CoreTokens.spacing_8, }, chip: { backgroundColor: CoreTokens.color_grey_200, @@ -204,12 +204,12 @@ export const componentTokens = { borderRadius: "80px", borderThickness: CoreTokens.border_width_0, borderStyle: CoreTokens.border_solid, - contentPaddingLeft: "16px", - contentPaddingRight: "16px", - contentPaddingTop: "0px", - contentPaddingBottom: "0px", + contentPaddingLeft: CoreTokens.spacing_16, + contentPaddingRight: CoreTokens.spacing_16, + contentPaddingTop: CoreTokens.spacing_0, + contentPaddingBottom: CoreTokens.spacing_0, iconSize: "24px", - iconSpacing: "8px", + iconSpacing: CoreTokens.spacing_8, iconColor: CoreTokens.color_grey_800, hoverIconColor: CoreTokens.color_grey_900, activeIconColor: CoreTokens.color_black, @@ -279,10 +279,10 @@ export const componentTokens = { buttonIconSize: "20px", buttonIconSpacing: "10px", buttonIconColor: CoreTokens.color_black, - buttonPaddingTop: "0px", - buttonPaddingBottom: "0px", - buttonPaddingLeft: "16px", - buttonPaddingRight: "16px", + buttonPaddingTop: CoreTokens.spacing_0, + buttonPaddingBottom: CoreTokens.spacing_0, + buttonPaddingLeft: CoreTokens.spacing_16, + buttonPaddingRight: CoreTokens.spacing_16, buttonHeight: "40px", buttonBorderRadius: "4px", buttonBorderStyle: CoreTokens.border_none, @@ -291,7 +291,6 @@ export const componentTokens = { disabledColor: CoreTokens.color_grey_500, disabledButtonBackgroundColor: CoreTokens.color_transparent, disabledButtonBorderColor: CoreTokens.color_transparent, - disabledBorderColor: CoreTokens.color_transparent, optionBackgroundColor: CoreTokens.color_white, hoverOptionBackgroundColor: CoreTokens.color_grey_100, activeOptionBackgroundColor: CoreTokens.color_grey_300, @@ -305,11 +304,11 @@ export const componentTokens = { optionIconColor: CoreTokens.color_black, optionPaddingTop: "6px", optionPaddingBottom: "6px", - optionPaddingLeft: "16px", - optionPaddingRight: "16px", + optionPaddingLeft: CoreTokens.spacing_16, + optionPaddingRight: CoreTokens.spacing_16, caretIconSize: "24px", caretIconColor: CoreTokens.color_black, - caretIconSpacing: "12px", + caretIconSpacing: CoreTokens.spacing_12, borderRadius: "4px", borderStyle: CoreTokens.border_none, borderThickness: CoreTokens.border_width_0, @@ -331,7 +330,6 @@ export const componentTokens = { focusDropBorderColor: CoreTokens.color_blue_600, disabledDropBorderColor: CoreTokens.color_grey_500, dragoverDropBackgroundColor: CoreTokens.color_blue_50, - activeFileItemIconBackgrounColor: CoreTokens.color_grey_300, errorFileItemBorderColor: CoreTokens.color_red_700, errorFileItemBackgroundColor: CoreTokens.color_red_50, errorFilePreviewBackgroundColor: CoreTokens.color_red_200, @@ -377,7 +375,7 @@ export const componentTokens = { bottomLinksDividerColor: CoreTokens.color_blue_600, bottomLinksDividerThickness: "1px", bottomLinksDividerStyle: CoreTokens.border_solid, - bottomLinksDividerSpacing: "8px", + bottomLinksDividerSpacing: CoreTokens.spacing_8, bottomLinksFontFamily: CoreTokens.type_sans, bottomLinksFontSize: CoreTokens.type_scale_01, bottomLinksFontStyle: CoreTokens.type_normal, @@ -393,7 +391,7 @@ export const componentTokens = { logoHeight: "32px", logoWidth: "auto", socialLinksSize: "24px", - socialLinksGutter: "16px", + socialLinksGutter: CoreTokens.spacing_16, socialLinksColor: CoreTokens.color_white, }, header: { @@ -419,10 +417,10 @@ export const componentTokens = { overlayColor: CoreTokens.color_grey_800_a, overlayOpacity: "0.7", overlayZindex: "1600", - paddingTop: "0px", - paddingBottom: "0px", - paddingRight: "24px", - paddingLeft: "24px", + paddingTop: CoreTokens.spacing_0, + paddingBottom: CoreTokens.spacing_0, + paddingRight: CoreTokens.spacing_24, + paddingLeft: CoreTokens.spacing_24, underlinedColor: CoreTokens.color_black, underlinedThickness: "2px", underlinedStyle: CoreTokens.border_solid, @@ -480,8 +478,8 @@ export const componentTokens = { fontStyle: CoreTokens.type_normal, fontWeight: CoreTokens.type_regular, iconSize: "16px", - iconSpacing: "4px", - underlineSpacing: "0px", + iconSpacing: CoreTokens.spacing_4, + underlineSpacing: CoreTokens.spacing_0, underlineStyle: CoreTokens.border_solid, underlineThickness: "1px", disabledFontColor: CoreTokens.color_grey_500, @@ -520,20 +518,20 @@ export const componentTokens = { fontStyle: CoreTokens.type_normal, fontWeight: CoreTokens.type_regular, fontTextTransform: "none", - verticalPadding: "0.75rem", - horizontalPadding: "2rem", - marginRight: "40px", + verticalPadding: CoreTokens.spacing_12, + horizontalPadding: CoreTokens.spacing_32, + marginRight: CoreTokens.spacing_40, marginLeft: "20px", - itemsPerPageSelectorMarginLeft: "0px", - itemsPerPageSelectorMarginRight: "0.5rem", + itemsPerPageSelectorMarginLeft: CoreTokens.spacing_0, + itemsPerPageSelectorMarginRight: CoreTokens.spacing_8, pageSelectorMarginRight: "30px", - pageSelectorMarginLeft: "0px", - totalItemsContainerMarginRight: "2.5rem", - totalItemsContainerMarginLeft: "0px", + pageSelectorMarginLeft: CoreTokens.spacing_0, + totalItemsContainerMarginRight: CoreTokens.spacing_40, + totalItemsContainerMarginLeft: CoreTokens.spacing_0, }, paragraph: { - fontColor: CoreTokens.color_black, display: "block", + fontColor: CoreTokens.color_black, fontSize: CoreTokens.type_scale_03, fontWeight: CoreTokens.type_regular, }, @@ -719,10 +717,10 @@ export const componentTokens = { linkFontTextTransform: "none", linkFontLetterSpacing: CoreTokens.type_spacing_wide_01, linkTextDecoration: CoreTokens.type_no_line, - linkMarginTop: "4px", - linkMarginBottom: "4px", - linkMarginRight: "16px", - linkMarginLeft: "16px", + linkMarginTop: CoreTokens.spacing_4, + linkMarginBottom: CoreTokens.spacing_4, + linkMarginRight: CoreTokens.spacing_16, + linkMarginLeft: CoreTokens.spacing_16, linkFocusColor: CoreTokens.color_blue_600, scrollBarThumbColor: CoreTokens.color_grey_200_a, scrollBarTrackColor: CoreTokens.color_transparent, @@ -836,7 +834,7 @@ export const componentTokens = { thumbShift: "1.25rem", trackHeight: "12px", trackWidth: "36px", - spaceBetweenLabelSwitch: "8px", + spaceBetweenLabelSwitch: CoreTokens.spacing_8, }, table: { rowSeparatorThickness: "1px", @@ -849,8 +847,8 @@ export const componentTokens = { dataFontWeight: CoreTokens.type_regular, dataFontColor: CoreTokens.color_black, dataFontTextTransform: "none", - dataPaddingTop: "16px", - dataPaddingBottom: "16px", + dataPaddingTop: CoreTokens.spacing_16, + dataPaddingBottom: CoreTokens.spacing_16, dataPaddingRight: "20px", dataPaddingLeft: "20px", dataPaddingTopReduced: CoreTokens.spacing_8, @@ -871,8 +869,8 @@ export const componentTokens = { headerFontWeight: CoreTokens.type_regular, headerFontColor: CoreTokens.color_white, headerFontTextTransform: "none", - headerPaddingTop: "16px", - headerPaddingBottom: "16px", + headerPaddingTop: CoreTokens.spacing_16, + headerPaddingBottom: CoreTokens.spacing_16, headerPaddingRight: "20px", headerPaddingLeft: "20px", headerPaddingTopReduced: CoreTokens.spacing_8, @@ -926,10 +924,10 @@ export const componentTokens = { fontSize: CoreTokens.type_scale_02, fontStyle: CoreTokens.type_normal, fontWeight: CoreTokens.type_regular, - labelPaddingTop: "0px", - labelPaddingBottom: "0px", - labelPaddingLeft: "16px", - labelPaddingRight: "16px", + labelPaddingTop: CoreTokens.spacing_0, + labelPaddingBottom: CoreTokens.spacing_0, + labelPaddingLeft: CoreTokens.spacing_16, + labelPaddingRight: CoreTokens.spacing_16, height: "40px", iconColor: CoreTokens.color_white, iconSectionWidth: "40px", @@ -1297,13 +1295,13 @@ export type OpinionatedTheme = { }; export const spaces = { - xxsmall: "6px", - xsmall: "16px", - small: "24px", - medium: "36px", - large: "48px", - xlarge: "64px", - xxlarge: "100px", + xxsmall: CoreTokens.spacing_4, + xsmall: CoreTokens.spacing_8, + small: CoreTokens.spacing_12, + medium: CoreTokens.spacing_16, + large: CoreTokens.spacing_24, + xlarge: CoreTokens.spacing_32, + xxlarge: CoreTokens.spacing_48, }; export const responsiveSizes = { diff --git a/lib/src/main.ts b/lib/src/main.ts index 39f9a446e..53e8538d3 100644 --- a/lib/src/main.ts +++ b/lib/src/main.ts @@ -45,6 +45,7 @@ import DxcBadge from "./badge/Badge"; import DxcStatusLight from "./status-light/StatusLight"; import DxcContextualMenu from "./contextual-menu/ContextualMenu"; import DxcDivider from "./divider/Divider"; +import DxcBreadcrumbs from "./breadcrumbs/Breadcrumbs"; import HalstackContext, { HalstackProvider, HalstackLanguageContext } from "./HalstackContext"; @@ -99,4 +100,5 @@ export { DxcStatusLight, DxcContextualMenu, DxcDivider, + DxcBreadcrumbs, }; diff --git a/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.js b/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.js new file mode 100644 index 000000000..c37ae007f --- /dev/null +++ b/lib/test/accessibility/rules/specific/breadcrumbs/disabledRules.js @@ -0,0 +1,8 @@ +/** + * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the breadcrumbs component. + * + */ +export const disabledRules = [ + // Disable landmark unique valid rule to prevent errors from having multiple nav in the same page (that can happen in testing environments) + "landmark-unique", +]; diff --git a/website/screens/common/themes/advanced-theme.json b/website/screens/common/themes/advanced-theme.json index e98de9124..e78c54a09 100644 --- a/website/screens/common/themes/advanced-theme.json +++ b/website/screens/common/themes/advanced-theme.json @@ -323,7 +323,6 @@ "focusDropBorderColor": "#0095ff", "disabledDropBorderColor": "#999999", "dragoverDropBackgroundColor": "#f5fbff", - "activeFileItemIconBackgrounColor": "#cccccc", "errorFileItemBorderColor": "#d0011b", "errorFileItemBackgroundColor": "#fff5f6", "errorFilePreviewBackgroundColor": "#ffccd3", diff --git a/website/screens/components/link/code/examples/nextLink.ts b/website/screens/components/link/code/examples/nextLink.ts index b67f514f8..48b41b6a5 100644 --- a/website/screens/components/link/code/examples/nextLink.ts +++ b/website/screens/components/link/code/examples/nextLink.ts @@ -3,18 +3,20 @@ import Link from "next/link"; import React from "react"; const code = `() => { - const CustomLink = React.forwardRef(({ onClick, href, children, ...other }, ref) => { - return ( - <DxcLink {...other} href={href} onClick={onClick} ref={ref}> - {children} - </DxcLink> - ); - }); + const CustomLink = React.forwardRef( + ({ onClick, href, children, ...other }, ref) => { + return ( + <DxcLink {...other} href={href} onClick={onClick} ref={ref}> + {children} + </DxcLink> + ); + } + ); return ( <DxcInset space="2rem"> This is a text with a <Link href="/components/link" passHref legacyBehavior> - <CustomLink> next link</CustomLink> + <CustomLink>next</CustomLink> </Link>{" "} link. </DxcInset> diff --git a/website/screens/theme-generator/themes/schemas/advanced.schema.json b/website/screens/theme-generator/themes/schemas/advanced.schema.json index c9c297488..559b518f0 100644 --- a/website/screens/theme-generator/themes/schemas/advanced.schema.json +++ b/website/screens/theme-generator/themes/schemas/advanced.schema.json @@ -323,7 +323,6 @@ "focusDropBorderColor": "color", "disabledDropBorderColor": "color", "dragoverDropBackgroundColor": "color", - "activeFileItemIconBackgrounColor": "color", "errorFileItemBorderColor": "color", "errorFileItemBackgroundColor": "color", "errorFilePreviewBackgroundColor": "color",