Skip to content
This repository was archived by the owner on Sep 20, 2024. It is now read-only.

Commit 2011d38

Browse files
Merge pull request #170 from TylerAPfledderer/feat/c-skip-nav
Create Skip Nav Components
2 parents 1b7eaac + f7d820f commit 2011d38

File tree

11 files changed

+366
-0
lines changed

11 files changed

+366
-0
lines changed

packages/c-skip-nav/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# @chakra-ui/c-skip-nav
2+
3+
`CSkipNavLink` and `CSkipNavContent` are components in tandem to allow a user to skip over navigation content to some main content.
4+
5+
## Installation
6+
7+
```sh
8+
yarn add @chakra-ui/c-skip-nav
9+
# or
10+
npm i @chakra-ui/c-skip-nav
11+
```
12+
13+
Per WebAim.org on
14+
[WCAG 2.4.1 (Bypass Blocks - Level A)](https://webaim.org/standards/wcag/checklist#sc2.4.1),
15+
because the main content is not always the first section that the user
16+
encounters on a page, it is strongly recommended to include a skip link for
17+
users to be able to bypass content that is present on multiple pages. Navigation
18+
links are the most common skipped.
19+
20+
> A user with a screen reader or specialized software could skip content via the
21+
> headings or `main` region, but this is not sufficiant enough for sighted users
22+
> who primarily use a keyboard.
23+
24+
## Imports
25+
26+
```sh
27+
import { CSkipNavLink, CSkipNavContent } from '@chakra-ui/c-skip-nav'
28+
```
29+
30+
## Usage
31+
32+
The `CSkipNavLink` component ideally needs to be one of the first items a user
33+
encounters when they begin navigating a page after load. Therefore, it is
34+
recommended to place it as high as possible in the app.
35+
36+
It renders an `a` tag and automatically creates a link with an `href` attribute
37+
that will point to `CSkipNavContent`.
38+
39+
The `CSkipNavContent` component is used as a target for the link to place
40+
keyboard focus on the first piece on main content. It renders a `div` and can
41+
either be a self-closing component, or wrap the main content. (Visually, it
42+
might be better to just wrap the main content so the visual focus outline is
43+
very clear)
44+
45+
> You can supply a custom id value using the `id` prop but you will have to declare
46+
> this prop and value in both components, or they will not match. For Example:
47+
> `id="custom-id"` renders `href="#custom-id"` in `CSkipNavLink` and
48+
> `id="custom-id"` in `CSkipNavContent`.
49+
50+
```tsx
51+
<template>
52+
<c-skip-nav-link>Skip Navigation</c-skip-nav-link>
53+
// * Later in the page after the navigation...
54+
<c-skip-nav-content /> // Or it wraps the main content // Main content below
55+
</template>
56+
```
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<c-box position="relative">
3+
<c-skip-nav-link> HELLO CSkipNav </c-skip-nav-link>
4+
<c-skip-nav-content>
5+
<main>
6+
<c-text>
7+
To test the SkipNav Components:
8+
<c-list mb="4">
9+
<c-list-item>
10+
<c-list-icon name="chevron-right" />
11+
Place focus on the below input
12+
</c-list-item>
13+
<c-list-item>
14+
<c-list-icon name="chevron-right" />
15+
Press <c-kbd>Shift + Tab</c-kbd> to make the
16+
<c-code>SkipNavLink</c-code> appear
17+
</c-list-item>
18+
<c-list-item>
19+
<c-list-icon name="chevron-right" />
20+
Hit <c-kbd>Enter</c-kbd>.
21+
</c-list-item>
22+
<c-list-item>
23+
<c-list-icon name="chevron-right" />
24+
You should now see the focus over all the content with a blue
25+
outline.
26+
</c-list-item>
27+
</c-list>
28+
</c-text>
29+
<label>Example Form Search</label>
30+
<CInput placeholder="Search" />
31+
</main>
32+
</c-skip-nav-content>
33+
</c-box>
34+
</template>
35+
<script lang="ts" setup>
36+
import {
37+
CBox,
38+
CCode,
39+
CInput,
40+
CKbd,
41+
CList,
42+
CListItem,
43+
CSkipNavContent,
44+
CSkipNavLink,
45+
CText,
46+
} from "@chakra-ui/vue-next"
47+
</script>

packages/c-skip-nav/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './src'

packages/c-skip-nav/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@chakra-ui/c-skip-nav",
3+
"description": "Chakra UI Vue | My component description component",
4+
"version": "0.0.0-alpha.0",
5+
"main": "dist/chakra-ui-c-skip-nav.cjs.js",
6+
"module": "dist/chakra-ui-c-skip-nav.esm.js",
7+
"author": "Jonathan Bakebwa <[email protected]>",
8+
"homepage": "https://github.com/chakra-ui/chakra-ui-vue-next#readme",
9+
"license": "MIT",
10+
"files": [
11+
"dist"
12+
],
13+
"exports": {
14+
".": {
15+
"require": "./dist/chakra-ui-c-skip-nav.cjs.js",
16+
"default": "./dist/chakra-ui-c-skip-nav.esm.js"
17+
}
18+
},
19+
"repository": "https://github.com/chakra-ui/chakra-ui-vue-next/tree/master/packages/c-skip-nav",
20+
"bugs": {
21+
"url": "https://github.com/chakra-ui/chakra-ui-vue-next/issues"
22+
},
23+
"sideEffects": false,
24+
"scripts": {
25+
"clean": "rimraf dist"
26+
},
27+
"dependencies": {
28+
"@chakra-ui/vue-next": "1.0.0-alpha.13",
29+
"@chakra-ui/vue-system": "0.1.0-alpha.10",
30+
"@chakra-ui/vue-utils": "0.1.0-alpha.10"
31+
},
32+
"devDependencies": {
33+
"vue": "^3.2.37"
34+
},
35+
"peerDependencies": {
36+
"vue": "^3.1.4"
37+
},
38+
"publishConfig": {
39+
"access": "public"
40+
}
41+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Hey! Welcome to @chakra-ui/vue-next CSkipNav
3+
*
4+
* Component tandem to allow users to skip navigation content
5+
*
6+
* @see Docs https://next.vue.chakra-ui.com/c-skip-nav
7+
* @see Source https://github.com/chakra-ui/chakra-ui-vue-next/blob/master/packages/c-skip-nav/src/c-skip-nav.tsx
8+
* @see WebAIM https://webaim.org/techniques/skipnav/
9+
*/
10+
11+
import { defineComponent, computed, h } from "vue"
12+
import {
13+
chakra,
14+
ComponentWithProps,
15+
HTMLChakraProps,
16+
SystemStyleObject,
17+
useStyleConfig,
18+
} from "@chakra-ui/vue-system"
19+
import { CBox } from "@chakra-ui/vue-next"
20+
import { getValidChildren } from "@chakra-ui/vue-utils"
21+
22+
const FALLBACK_ID = "chakra-skip-nav"
23+
24+
export interface CSkipNavLinkProps extends HTMLChakraProps<"a"> {}
25+
26+
export const CSkipNavLink: ComponentWithProps<CSkipNavLinkProps> =
27+
defineComponent({
28+
name: "CSkipNavLink",
29+
props: {
30+
id: {
31+
type: String,
32+
default: FALLBACK_ID,
33+
},
34+
},
35+
setup(props, { slots, attrs }) {
36+
const styles = useStyleConfig("SkipLink", props)
37+
38+
const skipLinkStyles = computed<SystemStyleObject>(() => {
39+
return {
40+
userSelect: "none",
41+
border: "0",
42+
borderRadius: "md",
43+
fontWeight: "semibold",
44+
height: "1px",
45+
width: "1px",
46+
margin: "-1px",
47+
padding: "0",
48+
outline: "0",
49+
overflow: "hidden",
50+
position: "absolute",
51+
clip: "rect(0 0 0 0)",
52+
...styles.value,
53+
_focus: {
54+
clip: "auto",
55+
width: "auto",
56+
height: "auto",
57+
boxShadow: "outline",
58+
padding: "1rem",
59+
position: "fixed",
60+
top: "1.5rem",
61+
insetStart: "1.5rem",
62+
},
63+
}
64+
})
65+
66+
return () => {
67+
return (
68+
<chakra.a
69+
href={`#${props.id}`}
70+
__css={skipLinkStyles.value}
71+
{...attrs}
72+
>
73+
{() => getValidChildren(slots)}
74+
</chakra.a>
75+
)
76+
}
77+
},
78+
})
79+
80+
export interface CSkipNavContentProps extends HTMLChakraProps<"div"> {}
81+
82+
export const CSkipNavContent: ComponentWithProps<CSkipNavContentProps> =
83+
defineComponent({
84+
name: "CSkipNavContent",
85+
props: {
86+
id: {
87+
type: String,
88+
default: FALLBACK_ID,
89+
},
90+
},
91+
setup(props, { attrs, slots }) {
92+
return () => {
93+
return (
94+
<CBox
95+
tabIndex="-1"
96+
id={props.id}
97+
data-testid={FALLBACK_ID}
98+
{...attrs}
99+
>
100+
{() => getValidChildren(slots)}
101+
</CBox>
102+
)
103+
}
104+
},
105+
})

packages/c-skip-nav/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './c-skip-nav'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`CSkipNavContent should render properly 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="chakra-box css-0"
7+
data-testid="chakra-skip-nav"
8+
id="chakra-skip-nav"
9+
tabindex="-1"
10+
/>
11+
</DocumentFragment>
12+
`;
13+
14+
exports[`CSkipNavLink should render properly 1`] = `
15+
<DocumentFragment>
16+
<a
17+
class="css-cyzxyu"
18+
href="#chakra-skip-nav"
19+
/>
20+
</DocumentFragment>
21+
`;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
fireEvent,
3+
render,
4+
screen,
5+
userEvent,
6+
} from "@chakra-ui/vue-test-utils/src"
7+
import { wait } from "@testing-library/user-event/dist/utils"
8+
import { Component } from "vue"
9+
import { CSkipNavContent, CSkipNavLink } from "../src"
10+
11+
const MOCK_INPUT_PLACEHOLDER_TEXT = "Search"
12+
13+
const renderBaseTemplate = (options: Component = {}) => {
14+
return render({
15+
components: {
16+
CSkipNavLink,
17+
CSkipNavContent,
18+
},
19+
template: `
20+
<div>
21+
<c-skip-nav-link>Skip to Content</c-skip-nav-link>
22+
<c-skip-nav-content>
23+
<main>
24+
<form>
25+
<input type="text" placeholder=${MOCK_INPUT_PLACEHOLDER_TEXT} />
26+
</form>
27+
</main>
28+
</c-skip-nav-content>
29+
</div>
30+
`,
31+
...options,
32+
})
33+
}
34+
35+
const getSkipLink = () => screen.getByText("Skip to Content")
36+
37+
const getContentWrapper = () => screen.getByTestId("chakra-skip-nav")
38+
39+
const triggerSkipLink = async () => {
40+
const link = getSkipLink()
41+
await fireEvent.keyDown(link, {
42+
key: "Enter",
43+
code: "Enter",
44+
})
45+
}
46+
47+
describe("CSkipNavLink", () => {
48+
it("should render properly", () => {
49+
const { asFragment } = render(CSkipNavLink)
50+
expect(asFragment()).toMatchSnapshot()
51+
})
52+
})
53+
54+
describe("CSkipNavContent", () => {
55+
it("should render properly", () => {
56+
const { asFragment } = render(CSkipNavContent)
57+
expect(asFragment()).toMatchSnapshot()
58+
})
59+
})
60+
61+
describe("CSkipNav Pair", () => {
62+
beforeEach(async () => {
63+
renderBaseTemplate()
64+
userEvent.tab()
65+
})
66+
it("should have the user tab to `CSkipNavLink` after initial render", () => {
67+
const link = getSkipLink()
68+
expect(link).toHaveAttribute("href", "#chakra-skip-nav")
69+
})
70+
71+
it("should have the user navigate from `CSkipNavLink` to `CSkipNavContent` upon selection", async () => {
72+
await triggerSkipLink()
73+
const contentWrapper = getContentWrapper()
74+
75+
wait().then(() => {
76+
expect(contentWrapper).toHaveFocus()
77+
})
78+
})
79+
80+
it("should tab to input after wrapper focus", async () => {
81+
await triggerSkipLink()
82+
userEvent.tab()
83+
84+
const input = screen.getByPlaceholderText(MOCK_INPUT_PLACEHOLDER_TEXT)
85+
86+
expect(input).toHaveFocus()
87+
})
88+
})

packages/c-skip-nav/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"include": ["src", "./index.tsx", "./index.ts"]
4+
}

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@chakra-ui/c-portal": "0.1.0-alpha.10",
4141
"@chakra-ui/c-reset": "1.0.0-alpha.10",
4242
"@chakra-ui/c-scroll-lock": "0.1.0-alpha.8",
43+
"@chakra-ui/c-skip-nav": "0.0.0-alpha.0",
4344
"@chakra-ui/c-spinner": "1.0.0-alpha.10",
4445
"@chakra-ui/c-tag": "0.0.0-alpha.0",
4546
"@chakra-ui/c-theme-provider": "1.0.0-alpha.10",

0 commit comments

Comments
 (0)