Skip to content

Commit

Permalink
feat(a11y/QMenu/QDialog/QTabs): Add KeyGroupNavigation directive; Wra…
Browse files Browse the repository at this point in the history
…p arround keyboard navigation quasarframework#5266, quasarframework#4068, quasarframework#6736, quasarframework#6562, quasarframework#6560

- allow unique TAB target point in a group
- allow key navigation in group
- improve initial focusing on QMenu and QDialog
- tab goes from the end of the menu/dialog to the start
- shift+tab goes from the start of the menu/dialog to the end
- key navigation in tabs
  • Loading branch information
pdanpdan committed Apr 26, 2021
1 parent 013183b commit bc9f8e0
Show file tree
Hide file tree
Showing 39 changed files with 1,408 additions and 128 deletions.
5 changes: 5 additions & 0 deletions docs/src/assets/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,11 @@ const directives = [
name: 'Go Back (Handling Back Button)',
path: 'go-back'
},
{
name: 'Key Group Navigation',
badge: 'new',
path: 'key-group-navigation'
},
{
name: 'Intersection',
path: 'intersection'
Expand Down
2 changes: 1 addition & 1 deletion docs/src/components/DocExample.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ q-card.doc-example.q-my-lg(:class="classes", flat, bordered)

q-space

div.col-auto
div.col-auto(v-key-group-navigation)
q-btn(dense, flat, round, :icon="fabGithub", @click="openGitHub")
q-tooltip View on GitHub
q-btn.q-ml-sm(v-if="noEdit === false", dense, flat, round, :icon="fabCodepen", @click="openCodepen")
Expand Down
3 changes: 3 additions & 0 deletions docs/src/components/DocLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export default {
&:hover
opacity: .8
&:focus-visible
border-bottom: 2px solid currentColor
.q-icon
margin-top: -3px
margin-left: 4px
Expand Down
13 changes: 10 additions & 3 deletions docs/src/components/DocPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ q-page.doc-page

slot

.doc-page-nav.text-primary.q-pb-lg(v-if="related !== void 0")
.doc-page-nav.text-primary.q-pb-lg(v-if="related !== void 0", v-key-group-navigation)
.text-h6.q-pb-md Related
.q-gutter-md.flex
router-link.q-link.doc-page-related.rounded-borders.q-pa-md.cursor-pointer.column.justify-center.bg-grey-4(
Expand All @@ -32,7 +32,7 @@ q-page.doc-page

q-icon.q-ml-lg(:name="mdiLaunch")

.doc-page-nav.text-primary.q-pb-xl(v-if="nav !== void 0")
.doc-page-nav.text-primary.q-pb-xl(v-if="nav !== void 0", v-key-group-navigation)
.text-h6.q-pb-md Ready for more?
.q-gutter-md.flex
router-link.q-link.doc-page-related.doc-page-related-bordered.rounded-borders.q-pa-md.cursor-pointer.column.justify-center.bg-white(
Expand All @@ -57,7 +57,7 @@ q-page.doc-page
.q-mb-md(v-if="noEdit === false")
| Caught a mistake? <doc-link :to="editHref">Suggest an edit on GitHub</doc-link>

.doc-page-footer__icons.row.items-center.q-gutter-sm
.doc-page-footer__icons.row.items-center.q-gutter-sm(v-key-group-navigation)
a(href="https://github.com/pdanpdan/quasar", target="_blank", rel="noopener")
q-icon(:name="fabGithub")

Expand Down Expand Up @@ -164,6 +164,9 @@ export default {
text-decoration: none
outline: 0
&:focus-visible
border-bottom: 2px solid currentColor
&__badge
vertical-align: super
Expand Down Expand Up @@ -193,11 +196,15 @@ export default {
text-decoration: none
outline: 0
color: $primary
border-bottom: 2px solid transparent
transition: color .28s
&:hover
color: $grey-8
&:focus-visible
border-bottom-color: currentColor
.doc-page-nav
margin: 68px 0 0
margin-bottom: 0 !important
Expand Down
91 changes: 91 additions & 0 deletions docs/src/examples/KeyGroupNavigation/Bar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<div class="q-pa-md column no-wrap q-gutter-y-md" style="max-width: 500px">
<q-btn color="black" label="First focusable element" />

<q-bar v-key-group-navigation>
<q-btn flat dense color="black" no-caps label="File" class="q-px-sm">
<q-menu>
<q-list dense style="min-width: 100px" v-key-group-navigation>
<q-item clickable v-close-popup>
<q-item-section>Open...</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section>New</q-item-section>
</q-item>

<q-separator />

<q-item clickable>
<q-item-section>Preferences</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>

<q-menu anchor="top right" self="top left">
<q-list v-key-group-navigation>
<q-item
v-for="n in 3"
:key="n"
dense
clickable
>
<q-item-section>Submenu Label</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top right" self="top left">
<q-list v-key-group-navigation>
<q-item
v-for="n in 3"
:key="n"
dense
clickable
>
<q-item-section>3rd level Label</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
</q-list>
</q-menu>
</q-item>

<q-separator />

<q-item clickable v-close-popup>
<q-item-section>Quit</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>

<q-btn flat dense color="black" no-caps label="Edit" class="q-px-sm q-ml-md">
<q-menu auto-close>
<q-list dense style="min-width: 100px" v-key-group-navigation>
<q-item clickable>
<q-item-section>Cut</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>Copy</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>Paste</q-item-section>
</q-item>
<q-separator />
<q-item clickable>
<q-item-section>Select All</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>

<q-space />

<q-btn dense flat icon="minimize" />
<q-btn dense flat icon="crop_square" />
<q-btn dense flat icon="close" />
</q-bar>

<q-btn color="black" label="Last focusable element" />
</div>
</template>
24 changes: 24 additions & 0 deletions docs/src/examples/KeyGroupNavigation/FormControls.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<template>
<div class="q-pa-md column no-wrap q-gutter-y-md" style="max-width: 500px">
<q-btn color="black" label="First focusable element" />

<!-- QEditor already uses the keyboard navigation inside -->
<q-editor v-model="editor" min-height="5rem" />

<!-- QDate already uses the keyboard navigation inside -->
<q-date v-model="date" />

<q-btn color="black" label="Last focusable element" />
</div>
</template>

<script>
export default {
data () {
return {
editor: 'What you see is <b>what</b> you get.',
date: '2019/02/01'
}
}
}
</script>
71 changes: 71 additions & 0 deletions docs/src/examples/KeyGroupNavigation/List.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<div class="q-pa-md column no-wrap q-gutter-y-md" style="max-width: 500px">
<q-btn color="black" label="First focusable element" />

<q-list bordered padding v-key-group-navigation.vertical>
<q-item-label header>List with controls</q-item-label>

<q-item clickable v-ripple>
<q-item-section>
<q-item-label>Vertical navigation</q-item-label>
<q-item-label caption>
Navigate using <kbd>PG_UP</kbd>, <kbd>ARROW_UP</kbd>, <kbd>ARROW_DOWN</kbd>, <kbd>PG_DOWN</kbd>
</q-item-label>
</q-item-section>
</q-item>

<q-item clickable v-ripple>
<q-item-section>
<q-item-label>Horizontal navigation</q-item-label>
<q-item-label caption>
Navigate using <kbd>HOME</kbd>, <kbd>ARROW_LEFT</kbd>, <kbd>ARROW_RIGHT</kbd>, <kbd>END</kbd>
</q-item-label>
</q-item-section>
</q-item>

<q-item clickable v-ripple>
<q-item-section>
<q-item-label>Default</q-item-label>
<q-item-label caption>
Navigate using any of the above keys
</q-item-label>
</q-item-section>
</q-item>

<q-item tag="label" v-ripple>
<q-item-section side top>
<q-checkbox v-model="check" />
</q-item-section>

<q-item-section>
<q-item-label>Notifications</q-item-label>
<q-item-label caption>
Notify me about updates to apps or games that I downloaded
</q-item-label>
</q-item-section>
</q-item>

<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Battery too low</q-item-label>
</q-item-section>
<q-item-section side >
<q-toggle color="blue" v-model="notif" val="battery" />
</q-item-section>
</q-item>
</q-list>

<q-btn color="black" label="Last focusable element" />
</div>
</template>

<script>
export default {
data () {
return {
check: true,
notif: true
}
}
}
</script>
72 changes: 72 additions & 0 deletions docs/src/examples/KeyGroupNavigation/Toolbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="q-pa-md column no-wrap q-gutter-y-md" style="max-width: 500px">
<q-btn color="black" label="First focusable element" />

<q-toolbar class="bg-primary text-white q-my-md shadow-2" v-key-group-navigation>
<q-btn flat round dense icon="menu" class="q-mr-sm" />
<q-separator dark vertical inset />
<q-btn stretch flat label="Link" />

<q-space />

<q-btn-dropdown stretch flat label="Dropdown">
<q-list v-key-group-navigation>
<q-item-label header>Folders</q-item-label>
<q-item v-for="n in 3" :key="`x.${n}`" clickable v-close-popup tabindex="0">
<q-item-section avatar>
<q-avatar icon="folder" color="secondary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>Photos</q-item-label>
<q-item-label caption>February 22, 2016</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="info" />
</q-item-section>
</q-item>
<q-separator inset spaced />
<q-item-label header>Files</q-item-label>
<q-item v-for="n in 3" :key="`y.${n}`" clickable v-close-popup tabindex="0">
<q-item-section avatar>
<q-avatar icon="assignment" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>Vacation</q-item-label>
<q-item-label caption>February 22, 2016</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="info" />
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-separator dark vertical />
<q-btn stretch flat label="Link" />
<q-separator dark vertical />
<q-btn stretch flat label="Link" />
</q-toolbar>

<q-toolbar class="bg-purple text-white shadow-2 rounded-borders" v-key-group-navigation>
<q-btn stretch flat label="Homepage" />
<q-space />

<q-tabs v-model="tab" shrink>
<q-tab name="tab1" label="Tab 1" />
<q-tab name="tab2" label="Tab 2" />
<q-tab name="tab3" label="Tab 3" />
</q-tabs>
</q-toolbar>

<q-btn color="black" label="Last focusable element" />
</div>
</template>

<script>
export default {
data () {
return {
tab: null
}
}
}
</script>
2 changes: 1 addition & 1 deletion docs/src/pages/vue-components/uploader.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ cors = CORS()
cors.init_app(app, resource={r"/api/*": {"origins": "*"}})
@app.route('/upload', methods=['POST'])
def upload():
def upload():
for fname in request.files:
f = request.files.get(fname)
print(f)
Expand Down
39 changes: 39 additions & 0 deletions docs/src/pages/vue-directives/key-group-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Handling Keyboard Navigation in Groups of Controls
desc: How to improve keyboard accessibility when using groups of controls in a Quasar app.
---
Quasar offers a simple way to improve keyboard accessibility when using a large number of controls that can be grouped.

## Installation
<doc-installation directives="KeyGroupNavigation" />

## Usage
Attach the directive on a group wrapping component or DOM element (like QList, QBar, QToolbar).
Keyboard navigation using `TAB` or `SHIFT` + `TAB` keys will only select one tabbable element inside the group:
- the first / last tabbable element depending on navigation direction when first entering the group
- the last selected tabbable element when the group was visited before
- pressing the `TAB` or `SHIFT` + `TAB` keys when an element is focused inside the group will focus the next tabbable element after the group or the previous une before the group
Keyboard navigation inside the group can be performed using:
- `HOME`, `ARROW_LEFT`, `ARROW_RIGHT` and `END` keys when `horizontal` modifier is used
- `PG_UP`, `ARROW_UP`, `ARROW_DOWN` and `PG_DOWN` keys when `vertical` modifier is used
- any of the above keys when neither `horizontal` nor `vertical` modifiers are used (default)
The navigation wraps at the start / end, moving to the last / first tabbable element.

::: tip
To skip processing key events for some elements set a `q-key-group-navigation--ignore-key` class on them or on a parent of them.
:::

::: warning
Try not to mix keyboard controlled components (like QKnob, QRange, QSlider, QRating, QDate, QTime) in key navigation groups as it might get confusing to the user.
:::

<doc-example title="List navigation" file="KeyGroupNavigation/List" />

<doc-example title="Bar navigation" file="KeyGroupNavigation/Bar" />

<doc-example title="Toolbar navigation" file="KeyGroupNavigation/Toolbar" />

<doc-example title="Form controls navigation" file="KeyGroupNavigation/FormControls" />

## KeyGroupNavigation API
<doc-api file="KeyGroupNavigation" />
Loading

0 comments on commit bc9f8e0

Please sign in to comment.