From 460057c2beaf0c0f2bf19cd3db654eac73286e73 Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Mon, 25 Apr 2022 14:49:41 -0400 Subject: [PATCH 1/7] feat(skip-nav): create skip nav components and story --- .../chakra-ui-core/src/CSkipNav/CSkipNav.js | 121 ++++++++++++++++++ .../src/CSkipNav/CSkipNav.stories.js | 49 +++++++ 2 files changed, 170 insertions(+) create mode 100644 packages/chakra-ui-core/src/CSkipNav/CSkipNav.js create mode 100644 packages/chakra-ui-core/src/CSkipNav/CSkipNav.stories.js diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js new file mode 100644 index 00000000..bb36c760 --- /dev/null +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js @@ -0,0 +1,121 @@ +/** + * Hey! Welcome to @chakra-ui/vue Box + * + * Box is the most abstract component on top of which all + * other @chakra-ui/vue components are built. By default, it renders a `div` element + * + * @see Docs https://vue.chakra-ui.com/box + * @see Source https://github.com/chakra-ui/chakra-ui-vue/blob/master/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js + */ + +import { SNA } from '../config/props.types' +import { createStyledAttrsMixin, mode } from '../utils' +import CBox from '../CBox' + +const FALLBACK_ID = 'chakra-skip-nav' + +const createSkipNavLinkStyles = (props) => { + const baseStyles = { + userSelect: 'none', + border: '0', + borderRadius: 'md', + fontWeight: 'semibold', + height: '1px', + width: '1px', + margin: '-1px', + padding: '0', + outline: '0', + overflow: 'hidden', + position: 'absolute', + clip: 'rect(0 0 0 0)', + _focus: { + clip: 'auto', + width: 'auto', + height: 'auto', + boxShadow: 'outline', + padding: '1rem', + position: 'fixed', + top: '1.5rem', + insetStart: '1.5rem', + bg: mode('white', 'gray.700') + } + } + + return { ...baseStyles } +} + +/** + * CSkipNavLink component + * + * Renders a link that remains hidden until focused to skip to the main content. + * + * @see Docs https://vue.chakra-ui.com/skip-nav + */ +export const CSkipNavLink = { + name: 'CSkipNavLink', + mixins: [createStyledAttrsMixin('CSkipNavLink')], + props: { + id: { + type: String, + default: FALLBACK_ID + } + }, + computed: { + colorMode () { + return this.$chakraColorMode() + }, + theme () { + return this.$chakraTheme() + }, + componentStyles () { + return createSkipNavLinkStyles() + } + }, + render (h) { + return h( + 'a', + { + class: this.className, + attrs: { + href: `#${this.id}` + } + }, + this.$slots.default + ) + } +} + +/** + * CSkipNavLink component + * + * Renders a div as the target for the link. + * + * @see Docs https://vue.chakra-ui.com/skip-nav + */ +export const CSkipNavContent = { + name: 'CSkipNavContent', + mixins: [createStyledAttrsMixin('CSkipNavContent')], + props: { + id: { + type: String, + default: FALLBACK_ID + }, + to: SNA + }, + render (h) { + return h( + CBox, + { + class: this.className, + attrs: { + id: this.id, + tabIndex: '-1', + style: { + outline: 0 + } + } + }, + this.$slots.default + ) + } +} diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.stories.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.stories.js new file mode 100644 index 00000000..719a9bb7 --- /dev/null +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.stories.js @@ -0,0 +1,49 @@ +import { storiesOf } from '@storybook/vue' +import CInput from '../CInput' +import CText from '../CText' +import CList, { CListItem, CListIcon } from '../CList' +import { CSkipNavLink, CSkipNavContent } from './CSkipNav' + +storiesOf('UI | SkipNav', module).add('Default', () => ({ + components: { + CSkipNavLink, + CSkipNavContent, + CInput, + CText, + CList, + CListItem, + CListIcon + }, + template: ` +
+ Skip to Content + +
+ + To test the SkipNav Components: + + + + Place focus on the input + + + + Press "Shift + Tab" to make the SkipNavLink appear + + + + Hit "Enter". You might leave the page to load up the iFrame isolated + + + + You should now see a blue outline over all the content. + + + + + +
+
+
+ ` +})) From 4204c0e1ea6e4739dca112576e8ad22a1b05b9b3 Mon Sep 17 00:00:00 2001 From: Jonathan Bakebwa Date: Tue, 14 Jun 2022 00:17:12 +0800 Subject: [PATCH 2/7] docs(skip-nav): update jsdocs --- packages/chakra-ui-core/src/CSkipNav/CSkipNav.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js index bb36c760..d33661fa 100644 --- a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js @@ -1,10 +1,9 @@ /** - * Hey! Welcome to @chakra-ui/vue Box + * Hey! Welcome to @chakra-ui/vue SkipNavLink * - * Box is the most abstract component on top of which all - * other @chakra-ui/vue components are built. By default, it renders a `div` element + * Renders a link that remains hidden until focused to skip to the main content. * - * @see Docs https://vue.chakra-ui.com/box + * @see Docs https://vue.chakra-ui.com/skip-nav-link * @see Source https://github.com/chakra-ui/chakra-ui-vue/blob/master/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js */ From e9dc96bf095166b0507ae2c9d63123d044bbd483 Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Tue, 14 Jun 2022 09:58:16 -0400 Subject: [PATCH 3/7] fix(skip-nav): add index file --- packages/chakra-ui-core/src/CSkipNav/CSkipNav.js | 16 +++++++++------- packages/chakra-ui-core/src/CSkipNav/index.js | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 packages/chakra-ui-core/src/CSkipNav/index.js diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js index d33661fa..84f21675 100644 --- a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js @@ -50,7 +50,7 @@ const createSkipNavLinkStyles = (props) => { * * @see Docs https://vue.chakra-ui.com/skip-nav */ -export const CSkipNavLink = { +const CSkipNavLink = { name: 'CSkipNavLink', mixins: [createStyledAttrsMixin('CSkipNavLink')], props: { @@ -60,17 +60,17 @@ export const CSkipNavLink = { } }, computed: { - colorMode () { + colorMode() { return this.$chakraColorMode() }, - theme () { + theme() { return this.$chakraTheme() }, - componentStyles () { + componentStyles() { return createSkipNavLinkStyles() } }, - render (h) { + render(h) { return h( 'a', { @@ -91,7 +91,7 @@ export const CSkipNavLink = { * * @see Docs https://vue.chakra-ui.com/skip-nav */ -export const CSkipNavContent = { +const CSkipNavContent = { name: 'CSkipNavContent', mixins: [createStyledAttrsMixin('CSkipNavContent')], props: { @@ -101,7 +101,7 @@ export const CSkipNavContent = { }, to: SNA }, - render (h) { + render(h) { return h( CBox, { @@ -118,3 +118,5 @@ export const CSkipNavContent = { ) } } + +export { CSkipNavLink, CSkipNavContent } diff --git a/packages/chakra-ui-core/src/CSkipNav/index.js b/packages/chakra-ui-core/src/CSkipNav/index.js new file mode 100644 index 00000000..1fc03fec --- /dev/null +++ b/packages/chakra-ui-core/src/CSkipNav/index.js @@ -0,0 +1 @@ +export * from './CSkipNav' From 1a19a07a8235eeb2ad4b1f76593736a2a0c741dd Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Tue, 14 Jun 2022 11:30:38 -0400 Subject: [PATCH 4/7] test(skip-nav): add keyboard testing --- .../chakra-ui-core/src/CSkipNav/CSkipNav.js | 3 +- .../src/CSkipNav/tests/CSkipNav.test.js | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 packages/chakra-ui-core/src/CSkipNav/tests/CSkipNav.test.js diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js index 84f21675..52ac3d5e 100644 --- a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js @@ -111,7 +111,8 @@ const CSkipNavContent = { tabIndex: '-1', style: { outline: 0 - } + }, + 'data-testid': 'chakra-skip-nav' } }, this.$slots.default diff --git a/packages/chakra-ui-core/src/CSkipNav/tests/CSkipNav.test.js b/packages/chakra-ui-core/src/CSkipNav/tests/CSkipNav.test.js new file mode 100644 index 00000000..dc3432b1 --- /dev/null +++ b/packages/chakra-ui-core/src/CSkipNav/tests/CSkipNav.test.js @@ -0,0 +1,64 @@ +import { CSkipNavLink, CSkipNavContent } from '../CSkipNav' +import { fireEvent, render, userEvent, screen, wait } from '@/tests/test-utils' + +const renderComponent = (props) => { + const base = { + components: { CSkipNavLink, CSkipNavContent }, + template: ` +
+ Skip to Content + +
+
+ +
+
+
+
+ `, + ...props + } + return render(base) +} + +const getSkipLink = () => screen.getByText('Skip to Content') + +const getContentWrapper = () => screen.getByTestId('chakra-skip-nav') + +const triggerSkipLink = async () => { + const link = getSkipLink() + await fireEvent.keyDown(link, { + key: 'Enter', + code: 'Enter' + }) +} + +describe('CSkipNav', () => { + beforeEach(async () => { + renderComponent() + await userEvent.tab() + }) + + it('should be tabbed to link after initial render', () => { + const link = getSkipLink() + expect(link).toHaveAttribute('href', '#chakra-skip-nav') + }) + + it('should navigate to content wrapper on selecting skip link', async () => { + await triggerSkipLink() + const contentWrapper = getContentWrapper() + + wait(() => { + expect(contentWrapper).toHaveFocus() + }) + }) + + it('should tab to input after wrapper focus', async () => { + await triggerSkipLink() + await userEvent.tab() + + const input = screen.getByPlaceholderText('Search') + + expect(input).toHaveFocus() + }) +}) From a013a652f09a0b571b6c7139aeae66a509dce7f2 Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Tue, 14 Jun 2022 11:34:26 -0400 Subject: [PATCH 5/7] chore(core): export `CSkipNav` components in core package --- packages/chakra-ui-core/src/index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/chakra-ui-core/src/index.js b/packages/chakra-ui-core/src/index.js index 7afe8a8c..bff6995c 100644 --- a/packages/chakra-ui-core/src/index.js +++ b/packages/chakra-ui-core/src/index.js @@ -99,6 +99,7 @@ export { default as CRadioButtonGroup } from './CRadioButtonGroup' // S export { default as CSimpleGrid } from './CSimpleGrid' export { default as CSelect } from './CSelect' +export * from './CSkipNav' export { default as CSlider } from './CSlider' export * from './CSlider' export { default as CSpinner } from './CSpinner' @@ -124,8 +125,15 @@ export { default as defaultTheme } from '@chakra-ui/theme-vue' // Internal icons export { parsePackIcons } from './utils/icons' -export { mode, colorModeObserver as localColorModeObserver, defineColorModeObserver } from './utils/color-mode-observer' +export { + mode, + colorModeObserver as localColorModeObserver, + defineColorModeObserver +} from './utils/color-mode-observer' export { default as internalIcons } from './lib/internal-icons' // Directives -export { createServerDirective, createClientDirective } from './directives/chakra.directive' +export { + createServerDirective, + createClientDirective +} from './directives/chakra.directive' From 7199dfd309dceb730a2a51560d1d766ea06b809d Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Tue, 14 Jun 2022 12:13:21 -0400 Subject: [PATCH 6/7] docs(skip-nav): add an accessibility report --- .../src/CSkipNav/accessibility.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/chakra-ui-core/src/CSkipNav/accessibility.md diff --git a/packages/chakra-ui-core/src/CSkipNav/accessibility.md b/packages/chakra-ui-core/src/CSkipNav/accessibility.md new file mode 100644 index 00000000..79655054 --- /dev/null +++ b/packages/chakra-ui-core/src/CSkipNav/accessibility.md @@ -0,0 +1,51 @@ +# Skip Nav | Accessibility ♿️ + +This report is adapted to the outline from the [WAI-ARIA Authoring Patterns practices](https://www.w3.org/WAI/ARIA/apg/patterns/) and technique information from [WebAIM](https://webaim.org/techniques/skipnav/), supported by Chakra UI for the `CSkipNav` components. + +### Description + +The Skip Navigation components are a tandem used to provide interaction for the keyboard user in skipping navigation content (or redundant content used at the top of multiple pages) to the main body of the page. + +#### Components + +`@chakra-ui/vue` exports 2 Skip Nav related components: + +- `CSkipNavLink` +- `CSkipNavContent` + +### `CSkipNav` Keyboard Interaction + +- **`Tab`**: + - On initial load of the page, moves focus to the `CSkipNavLink` element, provided that this component is the first focusable element in the page. + - On focus of the `CSkipNavContent` component, moves to the next focusable element inside the wrapper. +- **`Enter`**: + - Moves focus from the `CSkipNavLink` element to the `CSkipNavContent` element. + +### `CDrawer` WAI-ARIA Roles, States, and Properties: + +- The `CSkipNavLink` contains an href linking to the `id` of the `CSkipNavContent` component. +- The `CSkipNavContent` component renders `tabindex="-1"` to show visible change of focus to the main content. A screen reader is expected to immediately read out the first of this content. + +### Consideration of Multiple `CSkipNavLinks` Components + +In most cases, a single component is sufficient. + +However, a very complex page with several repeated elements may neeeded additional skip links, either by providing the whole set at the very beginning of the page to navigate through, or added as in-page links to allow the user to quickly bypass content, including confusing or inaccessible content such as ASCII art, complex tables, or complex social media feeds. + +Remember, the purpose of skip navigation links is to make keyboard navigation more efficient. Adding more links increases link-clutter. At what point will you need to add a "Skip the skip links" link?! + +### Concerns with Aestheic Impact + +The `CSkipNavLink` component is designed to be hidden until a user navigtes to it with a keyboard. The address concerns of the link being unattractive or confusing to users who do not need it. + +Techniques like `display: none` or the `hidden` attribute will remove the component from keyboard interaction. Therefore, the component is styled in such a way that it positioned out of the visible browser window, and then on focus with CSS it transitions into view. + +If there is concern with a user potentially tabbing quickly away from the component, it can be styled or scripted to remain visible for an extended period of time. + +### `CLink` WAI-ARIA compliance + +- [WCAG 2.4.1 (Bypass Blocks - Level A)](https://www.w3.org/TR/WCAG21/#bypass-blocks): This component tandem is a mechanism bypassing blocks of content that are repeated on multiple pages. +- The `CSkipNavLink` component renders an `` element with rendered visibility as hidden off the screen. When tabbing to the link, it becomes visible for sighted keyboard users, and read out by a screen reader. +- With the `CSkipNavContent` component containing `tabindex="-1"`, the component renders a focus ring on focus for the visual keyboard user to indicate arrival to the main content. + +Noticed a bug or inconsistency with this component? [Open an issue](https://github.com/chakra-ui/chakra-ui-vue/issues/new/choose) From 13fed1055a8a1f7ca965e63d4e2e41a019391691 Mon Sep 17 00:00:00 2001 From: Tyler Pfledderer Date: Tue, 28 Jun 2022 11:16:40 -0400 Subject: [PATCH 7/7] fix(skip-nav): fix format --- packages/chakra-ui-core/src/CSkipNav/CSkipNav.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js index 52ac3d5e..6a25ff52 100644 --- a/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js +++ b/packages/chakra-ui-core/src/CSkipNav/CSkipNav.js @@ -60,17 +60,17 @@ const CSkipNavLink = { } }, computed: { - colorMode() { + colorMode () { return this.$chakraColorMode() }, - theme() { + theme () { return this.$chakraTheme() }, - componentStyles() { + componentStyles () { return createSkipNavLinkStyles() } }, - render(h) { + render (h) { return h( 'a', { @@ -101,7 +101,7 @@ const CSkipNavContent = { }, to: SNA }, - render(h) { + render (h) { return h( CBox, {