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,
+ },
+};