Skip to content

Commit

Permalink
feat(skip-link): add SkipLink component WD-15080
Browse files Browse the repository at this point in the history
  • Loading branch information
lorumic committed Oct 3, 2024
1 parent a021576 commit b03c168
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 1 deletion.
11 changes: 10 additions & 1 deletion src/components/ApplicationLayout/ApplicationLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import AppStatus from "./AppStatus";
import Application from "./Application";
import Button from "components/Button";
import Icon from "components/Icon";
import SkipLink from "components/SkipLink";

export type BaseProps<
NI = SideNavigationLinkDefaultElement,
Expand Down Expand Up @@ -86,6 +87,10 @@ export type BaseProps<
* Classes to apply to the status area.
*/
statusClassName?: string;
/**
* Id to apply to the main area. Used for the "Skip to main content" link.
*/
mainId?: string;
},
HTMLProps<HTMLDivElement>
>;
Expand Down Expand Up @@ -134,6 +139,7 @@ const ApplicationLayout = <
sideNavigation,
status,
statusClassName,
mainId = "main-content",
...props
}: Props<NI, PL>) => {
const [internalMenuPinned, setInternalMenuPinned] = useState(false);
Expand All @@ -145,6 +151,7 @@ const ApplicationLayout = <

return (
<Application {...props}>
<SkipLink mainId={mainId} />
{(navItems || sideNavigation) && (
<>
<AppNavigationBar className={navigationBarClassName}>
Expand Down Expand Up @@ -222,7 +229,9 @@ const ApplicationLayout = <
</AppNavigation>
</>
)}
<AppMain className={mainClassName}>{children}</AppMain>
<AppMain id={mainId} className={mainClassName}>
{children}
</AppMain>
{aside}
{status && <AppStatus className={statusClassName}>{status}</AppStatus>}
</Application>
Expand Down
27 changes: 27 additions & 0 deletions src/components/SkipLink/SkipLink.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { Meta, StoryObj } from "@storybook/react";

import SkipLink from "./SkipLink";

const meta: Meta<typeof SkipLink> = {
component: SkipLink,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof SkipLink>;

export const Default: Story = {
render: () => (
<div>
<SkipLink />
<p>
Click inside this example box, then hit the "Tab" key to make the skip
link focused and visible.
</p>
</div>
),

name: "Default",
};
43 changes: 43 additions & 0 deletions src/components/SkipLink/SkipLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";

import SkipLink from "./SkipLink";

describe("<SkipLink />", () => {
it("renders and is found in the DOM", () => {
render(<SkipLink />);

expect(screen.getByText("Skip to main content")).toBeInTheDocument();
});

it("gets focused only after a TAB press", async () => {
render(<SkipLink />);

const skipLink = screen.getByText("Skip to main content");
expect(skipLink).not.toHaveFocus();

await userEvent.tab();
expect(skipLink).toHaveFocus();
});

it("redirects focus to the main content", async () => {
render(
<div>
<SkipLink mainId="main-element" />
<main id="#main-element">
<input />
</main>
</div>,
);

const input = screen.getByRole("textbox");
expect(input).not.toHaveFocus();

await userEvent.click(screen.getByText("Skip to main content"));
expect(window.location.hash).toBe("#main-element");

await userEvent.tab();
expect(input).toHaveFocus();
});
});
21 changes: 21 additions & 0 deletions src/components/SkipLink/SkipLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { HTMLProps } from "react";

export type Props = {
/**
* Id of the main content area to skip to.
*/
mainId?: string;
} & HTMLProps<HTMLAnchorElement>;

/**
* This is a [React](https://reactjs.org/) component for the Vanilla [Skip link](https://vanillaframework.io/docs/patterns/links#skip-link) component.
*/
export const SkipLink = ({ mainId = "main-content" }: Props): JSX.Element => {
return (
<a className="p-link--skip" href={`#${mainId}`}>
Skip to main content
</a>
);
};

export default SkipLink;
2 changes: 2 additions & 0 deletions src/components/SkipLink/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./SkipLink";
export type { Props as SkipLinkProps } from "./SkipLink";
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export { default as SideNavigation } from "./components/SideNavigation";
export { default as SideNavigationItem } from "./components/SideNavigation/SideNavigationItem";
export { default as SideNavigationLink } from "./components/SideNavigation/SideNavigationLink";
export { default as SideNavigationText } from "./components/SideNavigation/SideNavigationText";
export { default as SkipLink } from "./components/SkipLink";
export { default as Slider } from "./components/Slider";
export { default as Switch } from "./components/Switch";
export { default as Spinner } from "./components/Spinner";
Expand Down Expand Up @@ -153,6 +154,7 @@ export type { SideNavigationProps } from "./components/SideNavigation";
export type { SideNavigationItemProps } from "./components/SideNavigation/SideNavigationItem";
export type { SideNavigationLinkProps } from "./components/SideNavigation/SideNavigationLink";
export type { SideNavigationTextProps } from "./components/SideNavigation/SideNavigationText";
export type { SkipLinkProps } from "./components/SkipLink";
export type { SliderProps } from "./components/Slider";
export type { SpinnerProps } from "./components/Spinner";
export type { StatusLabelProps } from "./components/StatusLabel";
Expand Down

0 comments on commit b03c168

Please sign in to comment.