diff --git a/.changeset/slow-humans-peel.md b/.changeset/slow-humans-peel.md new file mode 100644 index 0000000000..8931c98c74 --- /dev/null +++ b/.changeset/slow-humans-peel.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/in-page-navigation": minor +"@twilio-paste/core": minor +--- + +[In Page Navigation] Add new `orientation` property with a vertical option. diff --git a/packages/paste-core/components/in-page-navigation/__tests__/index.spec.tsx b/packages/paste-core/components/in-page-navigation/__tests__/index.spec.tsx index af1a878194..8a8fa5be70 100644 --- a/packages/paste-core/components/in-page-navigation/__tests__/index.spec.tsx +++ b/packages/paste-core/components/in-page-navigation/__tests__/index.spec.tsx @@ -5,45 +5,8 @@ import * as React from "react"; import { InPageNavigation, InPageNavigationItem } from "../src"; describe("InPageNavigation", () => { - it("should render a nav with correct aria-label", () => { - const { getByRole } = render( - - page 1 - page 2 - , - ); - - expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav"); - }); - - it("should render a list with list items and links", () => { - const { getAllByRole } = render( - - page 1 - page 2 - , - ); - - expect(getAllByRole("list")).toHaveLength(1); - expect(getAllByRole("listitem")).toHaveLength(2); - expect(getAllByRole("link")).toHaveLength(2); - }); - - it("should use the currentPage prop to apply aria-current", () => { - const { getByText } = render( - - page 1 - - page 2 - - , - ); - - expect(getByText("page 2")).toHaveAttribute("aria-current", "page"); - }); - - it("should pass props given to InPageNavigationItem onto its child", () => { - const { getByText } = render( + it("should render semantically correct with aria properly", () => { + const { getByRole, getAllByRole, getByText } = render( page 1 @@ -54,40 +17,17 @@ describe("InPageNavigation", () => { , ); + expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav"); + expect(getAllByRole("list")).toHaveLength(1); + expect(getAllByRole("listitem")).toHaveLength(2); + expect(getAllByRole("link")).toHaveLength(2); + expect(getByText("page 2")).toHaveAttribute("aria-current", "page"); expect(getByText("page 1")).toHaveAttribute("data-test-id", "page-1"); }); }); describe("Customization", () => { - it("should set a default element name", () => { - const { getByRole } = render( - - page 1 - , - ); - - expect(getByRole("navigation")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION"); - expect(getByRole("list")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS"); - expect(getByRole("listitem")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM"); - expect(getByRole("link")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR"); - }); - - it("should set a custom element name when provided", () => { - const { getByRole } = render( - - - page 1 - - , - ); - - expect(getByRole("navigation")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION"); - expect(getByRole("list")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS"); - expect(getByRole("listitem")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM"); - expect(getByRole("link")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR"); - }); - - it("should add custom styles to default element names", () => { + it("should add custom styles to the default element name", () => { const { getByRole } = render( { page 1 + , , ); - expect(getByRole("navigation")).toHaveStyleRule("font-weight", "400"); - expect(getByRole("list")).toHaveStyleRule("padding", "0.75rem"); - expect(getByRole("listitem")).toHaveStyleRule("margin", "0.75rem"); - expect(getByRole("link")).toHaveStyleRule("font-size", "1rem"); + const nav = getByRole("navigation"); + const list = getByRole("list"); + const listitem = getByRole("listitem"); + const link = getByRole("link"); + + expect(nav).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION"); + expect(list).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS"); + expect(listitem).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM"); + expect(link).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR"); + + expect(nav).toHaveStyleRule("font-weight", "400"); + expect(list).toHaveStyleRule("padding", "0.75rem"); + expect(listitem).toHaveStyleRule("margin", "0.75rem"); + expect(link).toHaveStyleRule("font-size", "1rem"); }); it("should add custom styles to custom element names", () => { @@ -131,9 +82,19 @@ describe("Customization", () => { , ); - expect(getByRole("navigation")).toHaveStyleRule("font-weight", "400"); - expect(getByRole("list")).toHaveStyleRule("padding", "0.75rem"); - expect(getByRole("listitem")).toHaveStyleRule("margin", "0.75rem"); - expect(getByRole("link")).toHaveStyleRule("font-size", "1rem"); + const nav = getByRole("navigation"); + const list = getByRole("list"); + const listitem = getByRole("listitem"); + const link = getByRole("link"); + + expect(nav).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION"); + expect(list).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS"); + expect(listitem).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM"); + expect(link).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR"); + + expect(nav).toHaveStyleRule("font-weight", "400"); + expect(list).toHaveStyleRule("padding", "0.75rem"); + expect(listitem).toHaveStyleRule("margin", "0.75rem"); + expect(link).toHaveStyleRule("font-size", "1rem"); }); }); diff --git a/packages/paste-core/components/in-page-navigation/__tests__/vertical.spec.tsx b/packages/paste-core/components/in-page-navigation/__tests__/vertical.spec.tsx new file mode 100644 index 0000000000..cdafa57827 --- /dev/null +++ b/packages/paste-core/components/in-page-navigation/__tests__/vertical.spec.tsx @@ -0,0 +1,99 @@ +import { render } from "@testing-library/react"; +import { CustomizationProvider } from "@twilio-paste/customization"; +import * as React from "react"; + +import { InPageNavigation, InPageNavigationItem } from "../src"; + +describe("InPageNavigation", () => { + it("should render semantically correct with aria properly", () => { + const { getByRole, getAllByRole, getByText } = render( + + + page 1 + + + page 2 + + , + ); + + expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav"); + expect(getAllByRole("list")).toHaveLength(1); + expect(getAllByRole("listitem")).toHaveLength(2); + expect(getAllByRole("link")).toHaveLength(2); + expect(getByText("page 2")).toHaveAttribute("aria-current", "page"); + expect(getByText("page 1")).toHaveAttribute("data-test-id", "page-1"); + }); +}); + +describe("Customization", () => { + it("should set a default element name", () => { + const { getByRole } = render( + + + page 1 + + , + ); + + const nav = getByRole("navigation"); + const list = getByRole("list"); + const listitem = getByRole("listitem"); + const link = getByRole("link"); + + expect(nav).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION"); + expect(list).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS"); + expect(listitem).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM"); + expect(link).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR"); + + expect(nav).toHaveStyleRule("font-weight", "400"); + expect(list).toHaveStyleRule("padding", "0.75rem"); + expect(listitem).toHaveStyleRule("margin", "0.75rem"); + expect(link).toHaveStyleRule("font-size", "1rem"); + }); + + it("should add custom styles to custom element names", () => { + const { getByRole } = render( + + + + page 1 + + + , + ); + + const nav = getByRole("navigation"); + const list = getByRole("list"); + const listitem = getByRole("listitem"); + const link = getByRole("link"); + + expect(nav).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION"); + expect(list).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS"); + expect(listitem).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM"); + expect(link).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR"); + + expect(nav).toHaveStyleRule("font-weight", "400"); + expect(list).toHaveStyleRule("padding", "0.75rem"); + expect(listitem).toHaveStyleRule("margin", "0.75rem"); + expect(link).toHaveStyleRule("font-size", "1rem"); + }); +}); diff --git a/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx b/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx index 95ede5438d..23e39c1770 100644 --- a/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx +++ b/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx @@ -3,7 +3,7 @@ import type { BoxProps } from "@twilio-paste/box"; import * as React from "react"; import { InPageNavigationContext } from "./InPageNavigationContext"; -import type { Variants } from "./types"; +import type { Orientation, Variants } from "./types"; export interface InPageNavigationProps extends Omit, "children"> { children?: React.ReactNode; @@ -11,14 +11,48 @@ export interface InPageNavigationProps extends Omit( - ({ element = "IN_PAGE_NAVIGATION", variant = "default", marginBottom, children, ...props }, ref) => { + ( + { + element = "IN_PAGE_NAVIGATION", + variant = "default", + orientation = "horizontal", + marginBottom, + children, + ...props + }, + ref, + ) => { const isFullWidth = variant === "fullWidth" || variant === "inverse_fullWidth"; + if (orientation === "vertical") { + return ( + + + + {children} + + + + ); + } + return ( - + element={`${element}_ITEMS`} display="flex" justifyContent={isFullWidth ? "space-evenly" : "flex-start"} + columnGap={!isFullWidth ? "space80" : "space0"} + padding="space0" margin="space0" marginBottom={marginBottom || "space60"} - padding="space0" - columnGap={!isFullWidth ? "space80" : "space0"} > {children} diff --git a/packages/paste-core/components/in-page-navigation/src/InPageNavigationContext.tsx b/packages/paste-core/components/in-page-navigation/src/InPageNavigationContext.tsx index 77045358c3..7f1fc75484 100644 --- a/packages/paste-core/components/in-page-navigation/src/InPageNavigationContext.tsx +++ b/packages/paste-core/components/in-page-navigation/src/InPageNavigationContext.tsx @@ -4,10 +4,12 @@ import type { Variants } from "./types"; interface InPageNavigationContextValue { variant?: Variants; + orientation?: "horizontal" | "vertical"; } const InPageNavigationContext = React.createContext({ variant: "default", + orientation: "horizontal", }); export { InPageNavigationContext }; diff --git a/packages/paste-core/components/in-page-navigation/src/InPageNavigationItem.tsx b/packages/paste-core/components/in-page-navigation/src/InPageNavigationItem.tsx index 97d171f35b..d6790bcc20 100644 --- a/packages/paste-core/components/in-page-navigation/src/InPageNavigationItem.tsx +++ b/packages/paste-core/components/in-page-navigation/src/InPageNavigationItem.tsx @@ -7,15 +7,8 @@ import * as React from "react"; import { InPageNavigationContext } from "./InPageNavigationContext"; const BASE_STYLES: BoxStyleProps = { - borderBottomColor: "transparent", - borderBottomStyle: "solid", - borderBottomWidth: "borderWidth10", color: "colorTextWeak", minWidth: "sizeSquare130", - paddingBottom: "space40", - paddingLeft: "space20", - paddingRight: "space20", - paddingTop: "space40", textAlign: "center", fontSize: "fontSize30", fontWeight: "fontWeightMedium", @@ -24,8 +17,6 @@ const BASE_STYLES: BoxStyleProps = { textOverflow: "ellipsis", transition: "border-color 100ms ease, color 100ms ease", whiteSpace: "nowrap", - display: "block", - width: "100%", textDecoration: "none", _hover: { borderBottomColor: "colorBorderPrimaryStronger", @@ -38,8 +29,34 @@ const BASE_STYLES: BoxStyleProps = { }, }; +const HORIZONTAL_BASE_STYLES: BoxStyleProps = { + ...BASE_STYLES, + width: "100%", + display: "block", + borderBottomColor: "transparent", + borderBottomStyle: "solid", + borderBottomWidth: "borderWidth10", + paddingBottom: "space40", + paddingLeft: "space20", + paddingRight: "space20", + paddingTop: "space40", +}; +const VERTICAL_BASE_STYLES: BoxStyleProps = { + ...BASE_STYLES, + width: "auto", + display: "inline-block", + borderLeftColor: "transparent", + borderLeftStyle: "solid", + borderLeftWidth: "borderWidth10", + paddingBottom: "space30", + paddingTop: "space30", + paddingLeft: "space50", + paddingRight: "space50", +}; + const CURRENT_PAGE_STYLES: BoxStyleProps = { borderBottomColor: "colorBorderPrimary", + borderLeftColor: "colorBorderPrimary", color: "colorTextLink", }; @@ -58,6 +75,7 @@ const INVERSE_STYLES: BoxStyleProps = { const INVERSE_CURRENT_PAGE_STYLES: BoxStyleProps = { borderBottomColor: "colorBorderInverseStrong", + borderLeftColor: "colorBorderInverseStrong", color: "colorTextInverse", }; @@ -70,7 +88,7 @@ export interface InPageNavigationItemProps extends HTMLPasteProps<"a"> { const InPageNavigationItem = React.forwardRef( ({ element = "IN_PAGE_NAVIGATION_ITEM", currentPage = false, href, children, ...props }, ref) => { - const { variant } = React.useContext(InPageNavigationContext); + const { variant, orientation } = React.useContext(InPageNavigationContext); const isFullWidth = variant === "fullWidth" || variant === "inverse_fullWidth"; const isInverse = variant === "inverse" || variant === "inverse_fullWidth"; let currentPageStyles = {}; @@ -80,6 +98,27 @@ const InPageNavigationItem = React.forwardRef + + {children} + + + ); + } + return ( { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + Super SIM + + Programmable Wireless + + ); +}; + +export const FullWidth: StoryFn = () => { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + Home + + Detection + Settings + + ); +}; + +export const Inverse: StoryFn = () => { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + + Home + + Detection + Settings + + + ); +}; + +export const InverseFullWidth: StoryFn = () => { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + + Home + + Detection + Settings + + + ); +}; + +export const LinkOverflowExample: StoryFn = () => { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + Super SIMSuper SIMSuper SIM - Telephony Overflow Please Work + + Programmable Wireless Telephony Overflow Please Work + + ); +}; + +export const Customized: StoryFn = () => { + const theme = useTheme(); + return ( + + {/* using UID here to make unique labels for landmarks in Storybook for axe testing */} + + + Home + + Detection + Settings + + + ); +}; +Customized.parameters = { + a11y: { + // no need to a11y check customization + disable: true, + }, +};