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

feat(skip-nav): create skip nav components and story #529

Merged
merged 11 commits into from
Mar 24, 2023
Merged
123 changes: 123 additions & 0 deletions packages/chakra-ui-core/src/CSkipNav/CSkipNav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Hey! Welcome to @chakra-ui/vue SkipNavLink
*
* Renders a link that remains hidden until focused to skip to the main content.
*
* @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
*/

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
*/
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
*/
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
},
'data-testid': 'chakra-skip-nav'
}
},
this.$slots.default
)
}
}

export { CSkipNavLink, CSkipNavContent }
49 changes: 49 additions & 0 deletions packages/chakra-ui-core/src/CSkipNav/CSkipNav.stories.js
Original file line number Diff line number Diff line change
@@ -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: `
<div>
<CSkipNavLink>Skip to Content</CSkipNavLink>
<CSkipNavContent>
<main>
<CText>
To test the SkipNav Components:
<CList mb="4">
<CListItem>
<CListIcon icon="chevron-right" />
Place focus on the input
</CListItem>
<CListItem>
<CListIcon icon="chevron-right" />
Press "Shift + Tab" to make the SkipNavLink appear
</CListItem>
<CListItem>
<CListIcon icon="chevron-right" />
Hit "Enter". You might leave the page to load up the iFrame isolated
</CListItem>
<CListItem>
<CListIcon icon="chevron-right" />
You should now see a blue outline over all the content.
</CListItem>
</CList>
</CText>
<label>Example Form Search</label>
<CInput placeholder="Search" />
</main>
</CSkipNavContent>
</div>
`
}))
51 changes: 51 additions & 0 deletions packages/chakra-ui-core/src/CSkipNav/accessibility.md
Original file line number Diff line number Diff line change
@@ -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 `<a>` 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)
1 change: 1 addition & 0 deletions packages/chakra-ui-core/src/CSkipNav/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CSkipNav'
64 changes: 64 additions & 0 deletions packages/chakra-ui-core/src/CSkipNav/tests/CSkipNav.test.js
Original file line number Diff line number Diff line change
@@ -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: `
<div>
<CSkipNavLink>Skip to Content</CSkipNavLink>
<CSkipNavContent>
<main>
<form>
<input type="text" placeholder="Search" />
</form>
</main>
</CSkipNavContent>
</div>
`,
...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()
})
})
12 changes: 10 additions & 2 deletions packages/chakra-ui-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'