Skip to content

Commit 92c16ec

Browse files
authored
feat(in-page-navigation): add vertical (#3537)
* feat(in-page-navigation): add vertical * fix: lint error * fix: overflow story
1 parent c58b3bf commit 92c16ec

File tree

8 files changed

+346
-92
lines changed

8 files changed

+346
-92
lines changed

.changeset/slow-humans-peel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@twilio-paste/in-page-navigation": minor
3+
"@twilio-paste/core": minor
4+
---
5+
6+
[In Page Navigation] Add new `orientation` property with a vertical option.

packages/paste-core/components/in-page-navigation/__tests__/index.spec.tsx

Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,8 @@ import * as React from "react";
55
import { InPageNavigation, InPageNavigationItem } from "../src";
66

77
describe("InPageNavigation", () => {
8-
it("should render a nav with correct aria-label", () => {
9-
const { getByRole } = render(
10-
<InPageNavigation aria-label="my-nav">
11-
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
12-
<InPageNavigationItem href="#">page 2</InPageNavigationItem>
13-
</InPageNavigation>,
14-
);
15-
16-
expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav");
17-
});
18-
19-
it("should render a list with list items and links", () => {
20-
const { getAllByRole } = render(
21-
<InPageNavigation aria-label="my-nav">
22-
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
23-
<InPageNavigationItem href="#">page 2</InPageNavigationItem>
24-
</InPageNavigation>,
25-
);
26-
27-
expect(getAllByRole("list")).toHaveLength(1);
28-
expect(getAllByRole("listitem")).toHaveLength(2);
29-
expect(getAllByRole("link")).toHaveLength(2);
30-
});
31-
32-
it("should use the currentPage prop to apply aria-current", () => {
33-
const { getByText } = render(
34-
<InPageNavigation aria-label="my-nav">
35-
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
36-
<InPageNavigationItem currentPage href="#">
37-
page 2
38-
</InPageNavigationItem>
39-
</InPageNavigation>,
40-
);
41-
42-
expect(getByText("page 2")).toHaveAttribute("aria-current", "page");
43-
});
44-
45-
it("should pass props given to InPageNavigationItem onto its <a> child", () => {
46-
const { getByText } = render(
8+
it("should render semantically correct with aria properly", () => {
9+
const { getByRole, getAllByRole, getByText } = render(
4710
<InPageNavigation aria-label="my-nav">
4811
<InPageNavigationItem data-test-id="page-1" href="#">
4912
page 1
@@ -54,40 +17,17 @@ describe("InPageNavigation", () => {
5417
</InPageNavigation>,
5518
);
5619

20+
expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav");
21+
expect(getAllByRole("list")).toHaveLength(1);
22+
expect(getAllByRole("listitem")).toHaveLength(2);
23+
expect(getAllByRole("link")).toHaveLength(2);
24+
expect(getByText("page 2")).toHaveAttribute("aria-current", "page");
5725
expect(getByText("page 1")).toHaveAttribute("data-test-id", "page-1");
5826
});
5927
});
6028

6129
describe("Customization", () => {
62-
it("should set a default element name", () => {
63-
const { getByRole } = render(
64-
<InPageNavigation aria-label="my-nav">
65-
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
66-
</InPageNavigation>,
67-
);
68-
69-
expect(getByRole("navigation")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION");
70-
expect(getByRole("list")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS");
71-
expect(getByRole("listitem")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM");
72-
expect(getByRole("link")).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR");
73-
});
74-
75-
it("should set a custom element name when provided", () => {
76-
const { getByRole } = render(
77-
<InPageNavigation element="MY_IN_PAGE_NAVIGATION" aria-label="my-nav">
78-
<InPageNavigationItem element="MY_IN_PAGE_NAVIGATION_ITEM" href="#">
79-
page 1
80-
</InPageNavigationItem>
81-
</InPageNavigation>,
82-
);
83-
84-
expect(getByRole("navigation")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION");
85-
expect(getByRole("list")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS");
86-
expect(getByRole("listitem")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM");
87-
expect(getByRole("link")).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR");
88-
});
89-
90-
it("should add custom styles to default element names", () => {
30+
it("should add custom styles to the default element name", () => {
9131
const { getByRole } = render(
9232
<CustomizationProvider
9333
baseTheme="default"
@@ -102,13 +42,24 @@ describe("Customization", () => {
10242
<InPageNavigation aria-label="my-nav">
10343
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
10444
</InPageNavigation>
45+
,
10546
</CustomizationProvider>,
10647
);
10748

108-
expect(getByRole("navigation")).toHaveStyleRule("font-weight", "400");
109-
expect(getByRole("list")).toHaveStyleRule("padding", "0.75rem");
110-
expect(getByRole("listitem")).toHaveStyleRule("margin", "0.75rem");
111-
expect(getByRole("link")).toHaveStyleRule("font-size", "1rem");
49+
const nav = getByRole("navigation");
50+
const list = getByRole("list");
51+
const listitem = getByRole("listitem");
52+
const link = getByRole("link");
53+
54+
expect(nav).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION");
55+
expect(list).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS");
56+
expect(listitem).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM");
57+
expect(link).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR");
58+
59+
expect(nav).toHaveStyleRule("font-weight", "400");
60+
expect(list).toHaveStyleRule("padding", "0.75rem");
61+
expect(listitem).toHaveStyleRule("margin", "0.75rem");
62+
expect(link).toHaveStyleRule("font-size", "1rem");
11263
});
11364

11465
it("should add custom styles to custom element names", () => {
@@ -131,9 +82,19 @@ describe("Customization", () => {
13182
</CustomizationProvider>,
13283
);
13384

134-
expect(getByRole("navigation")).toHaveStyleRule("font-weight", "400");
135-
expect(getByRole("list")).toHaveStyleRule("padding", "0.75rem");
136-
expect(getByRole("listitem")).toHaveStyleRule("margin", "0.75rem");
137-
expect(getByRole("link")).toHaveStyleRule("font-size", "1rem");
85+
const nav = getByRole("navigation");
86+
const list = getByRole("list");
87+
const listitem = getByRole("listitem");
88+
const link = getByRole("link");
89+
90+
expect(nav).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION");
91+
expect(list).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS");
92+
expect(listitem).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM");
93+
expect(link).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR");
94+
95+
expect(nav).toHaveStyleRule("font-weight", "400");
96+
expect(list).toHaveStyleRule("padding", "0.75rem");
97+
expect(listitem).toHaveStyleRule("margin", "0.75rem");
98+
expect(link).toHaveStyleRule("font-size", "1rem");
13899
});
139100
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { render } from "@testing-library/react";
2+
import { CustomizationProvider } from "@twilio-paste/customization";
3+
import * as React from "react";
4+
5+
import { InPageNavigation, InPageNavigationItem } from "../src";
6+
7+
describe("InPageNavigation", () => {
8+
it("should render semantically correct with aria properly", () => {
9+
const { getByRole, getAllByRole, getByText } = render(
10+
<InPageNavigation aria-label="my-nav" orientation="vertical">
11+
<InPageNavigationItem data-test-id="page-1" href="#">
12+
page 1
13+
</InPageNavigationItem>
14+
<InPageNavigationItem currentPage href="#">
15+
page 2
16+
</InPageNavigationItem>
17+
</InPageNavigation>,
18+
);
19+
20+
expect(getByRole("navigation")).toHaveAttribute("aria-label", "my-nav");
21+
expect(getAllByRole("list")).toHaveLength(1);
22+
expect(getAllByRole("listitem")).toHaveLength(2);
23+
expect(getAllByRole("link")).toHaveLength(2);
24+
expect(getByText("page 2")).toHaveAttribute("aria-current", "page");
25+
expect(getByText("page 1")).toHaveAttribute("data-test-id", "page-1");
26+
});
27+
});
28+
29+
describe("Customization", () => {
30+
it("should set a default element name", () => {
31+
const { getByRole } = render(
32+
<CustomizationProvider
33+
baseTheme="default"
34+
theme={TestTheme}
35+
elements={{
36+
IN_PAGE_NAVIGATION: { fontWeight: "fontWeightLight" },
37+
IN_PAGE_NAVIGATION_ITEMS: { padding: "space40" },
38+
IN_PAGE_NAVIGATION_ITEM: { margin: "space40" },
39+
IN_PAGE_NAVIGATION_ITEM_ANCHOR: { fontSize: "fontSize40" },
40+
}}
41+
>
42+
<InPageNavigation aria-label="my-nav" orientation="vertical">
43+
<InPageNavigationItem href="#">page 1</InPageNavigationItem>
44+
</InPageNavigation>
45+
</CustomizationProvider>,
46+
);
47+
48+
const nav = getByRole("navigation");
49+
const list = getByRole("list");
50+
const listitem = getByRole("listitem");
51+
const link = getByRole("link");
52+
53+
expect(nav).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION");
54+
expect(list).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEMS");
55+
expect(listitem).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM");
56+
expect(link).toHaveAttribute("data-paste-element", "IN_PAGE_NAVIGATION_ITEM_ANCHOR");
57+
58+
expect(nav).toHaveStyleRule("font-weight", "400");
59+
expect(list).toHaveStyleRule("padding", "0.75rem");
60+
expect(listitem).toHaveStyleRule("margin", "0.75rem");
61+
expect(link).toHaveStyleRule("font-size", "1rem");
62+
});
63+
64+
it("should add custom styles to custom element names", () => {
65+
const { getByRole } = render(
66+
<CustomizationProvider
67+
baseTheme="default"
68+
theme={TestTheme}
69+
elements={{
70+
MY_IN_PAGE_NAVIGATION: { fontWeight: "fontWeightLight" },
71+
MY_IN_PAGE_NAVIGATION_ITEMS: { padding: "space40" },
72+
MY_IN_PAGE_NAVIGATION_ITEM: { margin: "space40" },
73+
MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR: { fontSize: "fontSize40" },
74+
}}
75+
>
76+
<InPageNavigation element="MY_IN_PAGE_NAVIGATION" aria-label="my-nav" orientation="vertical">
77+
<InPageNavigationItem element="MY_IN_PAGE_NAVIGATION_ITEM" href="#">
78+
page 1
79+
</InPageNavigationItem>
80+
</InPageNavigation>
81+
</CustomizationProvider>,
82+
);
83+
84+
const nav = getByRole("navigation");
85+
const list = getByRole("list");
86+
const listitem = getByRole("listitem");
87+
const link = getByRole("link");
88+
89+
expect(nav).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION");
90+
expect(list).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEMS");
91+
expect(listitem).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM");
92+
expect(link).toHaveAttribute("data-paste-element", "MY_IN_PAGE_NAVIGATION_ITEM_ANCHOR");
93+
94+
expect(nav).toHaveStyleRule("font-weight", "400");
95+
expect(list).toHaveStyleRule("padding", "0.75rem");
96+
expect(listitem).toHaveStyleRule("margin", "0.75rem");
97+
expect(link).toHaveStyleRule("font-size", "1rem");
98+
});
99+
});

packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,67 @@ import type { BoxProps } from "@twilio-paste/box";
33
import * as React from "react";
44

55
import { InPageNavigationContext } from "./InPageNavigationContext";
6-
import type { Variants } from "./types";
6+
import type { Orientation, Variants } from "./types";
77

88
export interface InPageNavigationProps extends Omit<React.ComponentPropsWithRef<"div">, "children"> {
99
children?: React.ReactNode;
1010
element?: BoxProps["element"];
1111
marginBottom?: "space0";
1212
"aria-label": string;
1313
variant?: Variants;
14+
orientation?: Orientation;
1415
}
1516

1617
const InPageNavigation = React.forwardRef<HTMLDivElement, InPageNavigationProps>(
17-
({ element = "IN_PAGE_NAVIGATION", variant = "default", marginBottom, children, ...props }, ref) => {
18+
(
19+
{
20+
element = "IN_PAGE_NAVIGATION",
21+
variant = "default",
22+
orientation = "horizontal",
23+
marginBottom,
24+
children,
25+
...props
26+
},
27+
ref,
28+
) => {
1829
const isFullWidth = variant === "fullWidth" || variant === "inverse_fullWidth";
1930

31+
if (orientation === "vertical") {
32+
return (
33+
<InPageNavigationContext.Provider value={{ variant, orientation }}>
34+
<Box {...safelySpreadBoxProps(props)} as="nav" ref={ref} element={element}>
35+
<Box
36+
as="ul"
37+
listStyleType="none"
38+
element={`${element}_ITEMS`}
39+
display="flex"
40+
flexDirection="column"
41+
margin="space0"
42+
padding="space0"
43+
minWidth="size20"
44+
maxWidth="size40"
45+
rowGap="space20"
46+
>
47+
{children}
48+
</Box>
49+
</Box>
50+
</InPageNavigationContext.Provider>
51+
);
52+
}
53+
2054
return (
21-
<InPageNavigationContext.Provider value={{ variant }}>
55+
<InPageNavigationContext.Provider value={{ variant, orientation }}>
2256
<Box {...safelySpreadBoxProps(props)} as="nav" ref={ref} element={element}>
2357
<Box
2458
as="ul"
2559
listStyleType="none"
2660
element={`${element}_ITEMS`}
2761
display="flex"
2862
justifyContent={isFullWidth ? "space-evenly" : "flex-start"}
63+
columnGap={!isFullWidth ? "space80" : "space0"}
64+
padding="space0"
2965
margin="space0"
3066
marginBottom={marginBottom || "space60"}
31-
padding="space0"
32-
columnGap={!isFullWidth ? "space80" : "space0"}
3367
>
3468
{children}
3569
</Box>

packages/paste-core/components/in-page-navigation/src/InPageNavigationContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import type { Variants } from "./types";
44

55
interface InPageNavigationContextValue {
66
variant?: Variants;
7+
orientation?: "horizontal" | "vertical";
78
}
89

910
const InPageNavigationContext = React.createContext<InPageNavigationContextValue>({
1011
variant: "default",
12+
orientation: "horizontal",
1113
});
1214

1315
export { InPageNavigationContext };

0 commit comments

Comments
 (0)