web/satellite/vuetify-poc: improve keyboard navigation for sidebars

This change improves keyboard navigation for the Vuetify UI's
navigation sidebars. Navigation items can now be focused with the tab
key and selected with the enter or space key.

Resolves #6411

Change-Id: I6416efbee74209e089abbccd0e1a7f1c0f4b80cb
This commit is contained in:
Jeremy Wharton 2023-10-15 20:23:02 -05:00 committed by Storj Robot
parent 6ae28e2306
commit 4e0ffd1a11
5 changed files with 297 additions and 275 deletions

View File

@ -45,7 +45,7 @@ module.exports = {
},
{
'group': 'internal',
'pattern': '@?(poc)/{components,views}/**',
'pattern': '@?(poc)/{components,views,layouts}/**',
'position': 'after',
},
{

View File

@ -10,6 +10,7 @@
import { onMounted } from 'vue';
import { useConfigStore } from '@/store/modules/configStore';
import Notifications from '@poc/layouts/default/Notifications.vue';
const configStore = useConfigStore();

View File

@ -6,47 +6,35 @@
<v-sheet>
<v-list class="px-2" color="default" variant="flat">
<template v-if="pathBeforeAccountPage">
<v-list-item class="pa-4 rounded-lg" link router-link :to="pathBeforeAccountPage" @click="() => registerLinkClick(pathBeforeAccountPage)">
<navigation-item class="pa-4" title="Go back" :to="pathBeforeAccountPage">
<template #prepend>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 10C1 5.02944 5.02944 0.999999 10 0.999999C14.9706 0.999999 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10ZM1.99213 10C1.99213 14.4226 5.57737 18.0079 10 18.0079C14.4226 18.0079 18.0079 14.4226 18.0079 10C18.0079 5.57737 14.4226 1.99213 10 1.99213C5.57737 1.99213 1.99213 5.57737 1.99213 10ZM5.48501 9.73986L5.50374 9.7201L9.01144 6.2124C9.20516 6.01868 9.51925 6.01868 9.71297 6.2124C9.90024 6.39967 9.90648 6.69941 9.7317 6.89418L9.71297 6.91394L7.05211 9.5748L14.4646 9.5748C14.7385 9.5748 14.9606 9.7969 14.9606 10.0709C14.9606 10.3357 14.7531 10.5521 14.4918 10.5662L14.4646 10.5669L7.05211 10.5669L9.71297 13.2278C9.90024 13.4151 9.90648 13.7148 9.7317 13.9096L9.71297 13.9293C9.52571 14.1166 9.22597 14.1228 9.0312 13.9481L9.01144 13.9293L5.50374 10.4216C5.31647 10.2344 5.31023 9.93463 5.48501 9.73986Z" fill="currentColor" />
</svg>
</template>
<v-list-item-title link class="text-body-2 ml-3">
Go back
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-divider class="my-2" />
</template>
<!-- All Projects -->
<v-list-item class="pa-4 rounded-lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')">
<navigation-item title="All Projects" to="/projects">
<template #prepend>
<icon-all-projects />
</template>
<v-list-item-title link class="text-body-2 ml-3">
All Projects
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-list-item link router-link to="settings" class="my-1 py-3" rounded="lg" @click="() => registerLinkClick('/settings')">
<navigation-item title="Account Settings" to="settings">
<template #prepend>
<icon-settings />
</template>
<v-list-item-title class="text-body-2 ml-3">
Account Settings
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-list-item link router-link to="billing" class="my-1" rounded="lg" @click="() => registerLinkClick('/billing')">
<navigation-item title="Account Billing" to="billing">
<template #prepend>
<icon-card />
</template>
<v-list-item-title class="text-body-2 ml-3">
Account Billing
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-divider class="my-2" />
</v-list>
@ -60,8 +48,6 @@ import {
VNavigationDrawer,
VSheet,
VList,
VListItem,
VListItemTitle,
VDivider,
} from 'vuetify/components';
import { useDisplay } from 'vuetify';
@ -72,6 +58,7 @@ import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import IconCard from '@poc/components/icons/IconCard.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconAllProjects from '@poc/components/icons/IconAllProjects.vue';
import NavigationItem from '@poc/layouts/default/NavigationItem.vue';
const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
@ -92,23 +79,6 @@ const pathBeforeAccountPage = computed((): string | null => {
return path;
});
/**
* Conditionally closes the navigation drawer and tracks page visit.
*/
function registerLinkClick(page: string | null): void {
if (mdAndDown.value) {
model.value = false;
}
trackPageVisitEvent(page);
}
/**
* Sends "Page Visit" event to segment and opens link.
*/
function trackPageVisitEvent(page: string | null): void {
if (page) analyticsStore.pageVisit(page);
}
onBeforeMount(() => {
if (mdAndDown.value) {
model.value = false;

View File

@ -0,0 +1,56 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-list-item link lines="one" :to="to" class="my-1" tabindex="0" @click="onClick" @keydown.space.prevent="onClick">
<template #prepend>
<slot name="prepend" />
</template>
<v-list-item-title class="ml-3">{{ title }}</v-list-item-title>
<v-list-item-subtitle v-if="subtitle" class="ml-3">{{ subtitle }}</v-list-item-subtitle>
<template #append>
<slot name="append" />
</template>
</v-list-item>
</template>
<script setup lang="ts">
import { VListItem, VListItemTitle, VListItemSubtitle } from 'vuetify/components';
import { useDisplay } from 'vuetify';
import { useRouter } from 'vue-router';
import { useAppStore } from '@poc/store/appStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
const { mdAndDown } = useDisplay();
const router = useRouter();
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const analyticsStore = useAnalyticsStore();
const props = defineProps<{
title: string;
subtitle?: string;
to?: string;
}>();
/**
* Conditionally closes the navigation drawer and tracks page visit.
*/
function onClick(e: MouseEvent | KeyboardEvent): void {
if (!props.to) return;
const next = router.resolve(props.to).path;
if (next === router.currentRoute.value.path) return;
if (mdAndDown.value) appStore.toggleNavigationDrawer(false);
analyticsStore.pageVisit(next);
// Vuetify handles navigation via click or pressing the Enter key.
// We must handle the space key ourselves.
if ('key' in e && e.key === ' ') router.push(props.to);
}
</script>

View File

@ -3,22 +3,31 @@
<template>
<v-navigation-drawer v-model="model" class="py-1">
<v-sheet>
<v-list class="px-2" color="default" variant="flat">
<v-sheet class="pa-2">
<!-- Project -->
<v-list-item link class="pa-4 rounded-lg">
<v-menu activator="parent" location="end" transition="scale-transition">
<v-menu location="end" transition="scale-transition">
<template #activator="{ props: activatorProps }">
<navigation-item title="Project" :subtitle="selectedProject.name" class="pa-4" v-bind="activatorProps">
<template #prepend>
<IconProject />
</template>
<template #append>
<img src="@poc/assets/icon-right.svg" class="ml-3" alt="Project" width="10">
</template>
</navigation-item>
</template>
<!-- Project Menu -->
<v-list class="pa-2">
<!-- My Projects -->
<template v-if="ownProjects.length">
<v-list-item rounded="lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')">
<v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<!-- <img src="@poc/assets/icon-project.svg" alt="Projects"> -->
<IconProject />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-chip color="purple2" variant="tonal" size="small" rounded="xl" class="font-weight-bold" link>
<v-list-item-title>
<v-chip color="purple2" variant="tonal" size="small" rounded="xl" class="font-weight-bold">
My Projects
</v-chip>
</v-list-item-title>
@ -28,14 +37,13 @@
<v-list-item
v-for="project in ownProjects"
:key="project.id"
rounded="lg"
:active="project.isSelected"
@click="() => onProjectSelected(project)"
>
<template v-if="project.isSelected" #prepend>
<img src="@poc/assets/icon-check-color.svg" alt="Selected Project">
</template>
<v-list-item-title class="text-body-2" :class="project.isSelected ? 'ml-3' : 'ml-7'">
<v-list-item-title :class="project.isSelected ? 'ml-3' : 'ml-7'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
@ -45,11 +53,11 @@
<!-- Shared With Me -->
<template v-if="sharedProjects.length">
<v-list-item rounded="lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')">
<v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconProject />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
<v-chip color="green" variant="tonal" size="small" rounded="xl" class="font-weight-bold" link>
Shared Projects
</v-chip>
@ -60,14 +68,13 @@
<v-list-item
v-for="project in sharedProjects"
:key="project.id"
rounded="lg"
:active="project.isSelected"
@click="() => onProjectSelected(project)"
>
<template v-if="project.isSelected" #prepend>
<img src="@poc/assets/icon-check-color.svg" alt="Selected Project">
</template>
<v-list-item-title class="text-body-2" :class="project.isSelected ? 'ml-3' : 'ml-7'">
<v-list-item-title :class="project.isSelected ? 'ml-3' : 'ml-7'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
@ -76,11 +83,11 @@
</template>
<!-- Project Settings -->
<v-list-item link rounded="lg" :to="`/projects/${selectedProject.urlId}/settings`">
<v-list-item :to="`/projects/${selectedProject.urlId}/settings`">
<template #prepend>
<IconSettings />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
@ -88,21 +95,21 @@
<!-- <v-divider class="my-2"></v-divider> -->
<!-- View All Projects -->
<v-list-item link rounded="lg" router-link to="/projects" @click="() => registerLinkClick('/projects')">
<v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconAllProjects />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
View All Projects
</v-list-item-title>
</v-list-item>
<!-- Create New Project -->
<v-list-item link rounded="lg" @click="isCreateProjectDialogShown = true">
<v-list-item link @click="isCreateProjectDialogShown = true">
<template #prepend>
<IconNew />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
Create New Project
</v-list-item-title>
</v-list-item>
@ -110,77 +117,78 @@
<v-divider class="my-2" />
<!-- Manage Passphrase -->
<v-list-item link class="mt-1" rounded="lg" @click="isManagePassphraseDialogShown = true">
<v-list-item link class="mt-1" @click="isManagePassphraseDialogShown = true">
<template #prepend>
<IconPassphrase />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
Manage Passphrase
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<template #prepend>
<IconProject />
</template>
<v-list-item-title link class="text-body-2 ml-3">
Project
</v-list-item-title>
<v-list-item-subtitle class="ml-3">
{{ selectedProject.name }}
</v-list-item-subtitle>
<template #append>
<img src="@poc/assets/icon-right.svg" class="ml-3" alt="Project" width="10">
</template>
</v-list-item>
<v-divider class="my-2" />
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/dashboard`" class="my-1 py-3" rounded="lg" @click="() => registerLinkClick('/dashboard')">
<!--
<v-list-item
router-link
:to="`/projects/${selectedProject.urlId}/dashboard`"
class="my-1 py-3"
tabindex="0"
@click="() => registerLinkClick('/dashboard')"
>
<template #prepend>
<IconDashboard />
</template>
<v-list-item-title class="text-body-2 ml-3">
<v-list-item-title class="ml-3">
Overview
</v-list-item-title>
</v-list-item>
-->
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/buckets`" class="my-1" rounded="lg" @click="() => registerLinkClick('/buckets')">
<navigation-item title="Overview" :to="`/projects/${selectedProject.urlId}/dashboard`">
<template #prepend>
<IconDashboard />
</template>
</navigation-item>
<navigation-item title="Buckets" :to="`/projects/${selectedProject.urlId}/buckets`">
<template #prepend>
<IconBucket />
</template>
<v-list-item-title class="text-body-2 ml-3">
Buckets
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/access`" class="my-1" rounded="lg" @click="() => registerLinkClick('/access')">
<navigation-item title="Access" :to="`/projects/${selectedProject.urlId}/access`">
<template #prepend>
<IconAccess size="18" />
</template>
<v-list-item-title class="text-body-2 ml-3">
Access
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/team`" class="my-1" rounded="lg" @click="() => registerLinkClick('/team')">
<navigation-item title="Team" :to="`/projects/${selectedProject.urlId}/team`">
<template #prepend>
<IconTeam size="18" />
</template>
<v-list-item-title class="text-body-2 ml-3">
Team
</v-list-item-title>
</v-list-item>
</navigation-item>
<v-divider class="my-2" />
<!-- Resources Menu -->
<v-list-item link class="rounded-lg">
<v-menu activator="parent" location="end" transition="scale-transition">
<v-menu location="end" transition="scale-transition">
<template #activator="{ props: activatorProps }">
<navigation-item title="Resources" v-bind="activatorProps">
<template #prepend>
<IconResources />
</template>
<template #append>
<img src="@poc/assets/icon-right.svg" alt="Resources" width="10">
</template>
</navigation-item>
</template>
<v-list class="pa-2">
<v-list-item
class="py-3"
rounded="lg"
href="https://docs.storj.io/"
target="_blank"
rel="noopener noreferrer"
@ -189,7 +197,7 @@
<!-- <img src="@poc/assets/icon-docs.svg" alt="Docs"> -->
<IconDocs />
</template>
<v-list-item-title class="text-body-2 mx-3">
<v-list-item-title class="mx-3">
Documentation
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
@ -199,7 +207,6 @@
<v-list-item
class="py-3"
rounded="lg"
href="https://forum.storj.io/"
target="_blank"
rel="noopener noreferrer"
@ -207,7 +214,7 @@
<template #prepend>
<IconForum />
</template>
<v-list-item-title class="text-body-2 mx-3">
<v-list-item-title class="mx-3">
Community Forum
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
@ -217,7 +224,6 @@
<v-list-item
class="py-3"
rounded="lg"
href="https://supportdcs.storj.io/hc/en-us"
target="_blank"
rel="noopener noreferrer"
@ -225,7 +231,7 @@
<template #prepend>
<IconSupport />
</template>
<v-list-item-title class="text-body-2 mx-3">
<v-list-item-title class="mx-3">
Storj Support
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
@ -235,17 +241,6 @@
</v-list>
</v-menu>
<template #prepend>
<IconResources />
</template>
<v-list-item-title class="text-body-2 ml-3">
Resources
</v-list-item-title>
<template #append>
<img src="@poc/assets/icon-right.svg" alt="Resources" width="10">
</template>
</v-list-item>
<v-divider class="my-2" />
<!-- <v-list-item link class="my-1" router-link to="/design-library" rounded="lg">
@ -256,7 +251,6 @@
Design Library
</v-list-item-title>
</v-list-item> -->
</v-list>
</v-sheet>
</v-navigation-drawer>
@ -304,6 +298,7 @@ import IconSupport from '@poc/components/icons/IconSupport.vue';
import IconResources from '@poc/components/icons/IconResources.vue';
import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue';
import ManagePassphraseDialog from '@poc/components/dialogs/ManagePassphraseDialog.vue';
import NavigationItem from '@poc/layouts/default/NavigationItem.vue';
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();