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', 'group': 'internal',
'pattern': '@?(poc)/{components,views}/**', 'pattern': '@?(poc)/{components,views,layouts}/**',
'position': 'after', 'position': 'after',
}, },
{ {

View File

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

View File

@ -6,47 +6,35 @@
<v-sheet> <v-sheet>
<v-list class="px-2" color="default" variant="flat"> <v-list class="px-2" color="default" variant="flat">
<template v-if="pathBeforeAccountPage"> <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> <template #prepend>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <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> </svg>
</template> </template>
<v-list-item-title link class="text-body-2 ml-3"> </navigation-item>
Go back
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" /> <v-divider class="my-2" />
</template> </template>
<!-- All Projects --> <!-- 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> <template #prepend>
<icon-all-projects /> <icon-all-projects />
</template> </template>
<v-list-item-title link class="text-body-2 ml-3"> </navigation-item>
All Projects
</v-list-item-title>
</v-list-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> <template #prepend>
<icon-settings /> <icon-settings />
</template> </template>
<v-list-item-title class="text-body-2 ml-3"> </navigation-item>
Account Settings
</v-list-item-title>
</v-list-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> <template #prepend>
<icon-card /> <icon-card />
</template> </template>
<v-list-item-title class="text-body-2 ml-3"> </navigation-item>
Account Billing
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" /> <v-divider class="my-2" />
</v-list> </v-list>
@ -60,8 +48,6 @@ import {
VNavigationDrawer, VNavigationDrawer,
VSheet, VSheet,
VList, VList,
VListItem,
VListItemTitle,
VDivider, VDivider,
} from 'vuetify/components'; } from 'vuetify/components';
import { useDisplay } from 'vuetify'; import { useDisplay } from 'vuetify';
@ -72,6 +58,7 @@ import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import IconCard from '@poc/components/icons/IconCard.vue'; import IconCard from '@poc/components/icons/IconCard.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue'; import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconAllProjects from '@poc/components/icons/IconAllProjects.vue'; import IconAllProjects from '@poc/components/icons/IconAllProjects.vue';
import NavigationItem from '@poc/layouts/default/NavigationItem.vue';
const analyticsStore = useAnalyticsStore(); const analyticsStore = useAnalyticsStore();
const appStore = useAppStore(); const appStore = useAppStore();
@ -92,23 +79,6 @@ const pathBeforeAccountPage = computed((): string | null => {
return path; 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(() => { onBeforeMount(() => {
if (mdAndDown.value) { if (mdAndDown.value) {
model.value = false; 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,260 +3,254 @@
<template> <template>
<v-navigation-drawer v-model="model" class="py-1"> <v-navigation-drawer v-model="model" class="py-1">
<v-sheet> <v-sheet class="pa-2">
<v-list class="px-2" color="default" variant="flat"> <!-- Project -->
<!-- Project --> <v-menu location="end" transition="scale-transition">
<v-list-item link class="pa-4 rounded-lg"> <template #activator="{ props: activatorProps }">
<v-menu activator="parent" location="end" transition="scale-transition"> <navigation-item title="Project" :subtitle="selectedProject.name" class="pa-4" v-bind="activatorProps">
<!-- Project Menu --> <template #prepend>
<v-list class="pa-2"> <IconProject />
<!-- My Projects --> </template>
<template v-if="ownProjects.length"> <template #append>
<v-list-item rounded="lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')"> <img src="@poc/assets/icon-right.svg" class="ml-3" alt="Project" width="10">
<template #prepend> </template>
<!-- <img src="@poc/assets/icon-project.svg" alt="Projects"> --> </navigation-item>
<IconProject /> </template>
</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>
My Projects
</v-chip>
</v-list-item-title>
</v-list-item>
<!-- Selected Project --> <!-- Project Menu -->
<v-list-item <v-list class="pa-2">
v-for="project in ownProjects" <!-- My Projects -->
:key="project.id" <template v-if="ownProjects.length">
rounded="lg" <v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
:active="project.isSelected" <template #prepend>
@click="() => onProjectSelected(project)" <!-- <img src="@poc/assets/icon-project.svg" alt="Projects"> -->
> <IconProject />
<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'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
</template> </template>
<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>
</v-list-item>
<!-- Shared With Me --> <!-- Selected Project -->
<template v-if="sharedProjects.length"> <v-list-item
<v-list-item rounded="lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')"> v-for="project in ownProjects"
<template #prepend> :key="project.id"
<IconProject /> :active="project.isSelected"
</template> @click="() => onProjectSelected(project)"
<v-list-item-title class="text-body-2 ml-3"> >
<v-chip color="green" variant="tonal" size="small" rounded="xl" class="font-weight-bold" link> <template v-if="project.isSelected" #prepend>
Shared Projects <img src="@poc/assets/icon-check-color.svg" alt="Selected Project">
</v-chip>
</v-list-item-title>
</v-list-item>
<!-- Other Project -->
<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'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
</template> </template>
<v-list-item-title :class="project.isSelected ? 'ml-3' : 'ml-7'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
<!-- Project Settings --> <v-divider class="my-2" />
<v-list-item link rounded="lg" :to="`/projects/${selectedProject.urlId}/settings`">
<template #prepend>
<IconSettings />
</template>
<v-list-item-title class="text-body-2 ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
<!-- <v-divider class="my-2"></v-divider> -->
<!-- View All Projects -->
<v-list-item link rounded="lg" router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconAllProjects />
</template>
<v-list-item-title class="text-body-2 ml-3">
View All Projects
</v-list-item-title>
</v-list-item>
<!-- Create New Project -->
<v-list-item link rounded="lg" @click="isCreateProjectDialogShown = true">
<template #prepend>
<IconNew />
</template>
<v-list-item-title class="text-body-2 ml-3">
Create New Project
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
<!-- Manage Passphrase -->
<v-list-item link class="mt-1" rounded="lg" @click="isManagePassphraseDialogShown = true">
<template #prepend>
<IconPassphrase />
</template>
<v-list-item-title class="text-body-2 ml-3">
Manage Passphrase
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<template #prepend>
<IconProject />
</template> </template>
<v-list-item-title link class="text-body-2 ml-3">
Project <!-- Shared With Me -->
</v-list-item-title> <template v-if="sharedProjects.length">
<v-list-item-subtitle class="ml-3"> <v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
{{ selectedProject.name }} <template #prepend>
</v-list-item-subtitle> <IconProject />
<template #append> </template>
<img src="@poc/assets/icon-right.svg" class="ml-3" alt="Project" width="10"> <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>
</v-list-item-title>
</v-list-item>
<!-- Other Project -->
<v-list-item
v-for="project in sharedProjects"
:key="project.id"
: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="project.isSelected ? 'ml-3' : 'ml-7'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
</template> </template>
</v-list-item>
<v-divider class="my-2" /> <!-- Project Settings -->
<v-list-item :to="`/projects/${selectedProject.urlId}/settings`">
<template #prepend>
<IconSettings />
</template>
<v-list-item-title class="ml-3">
Project Settings
</v-list-item-title>
</v-list-item>
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/dashboard`" class="my-1 py-3" rounded="lg" @click="() => registerLinkClick('/dashboard')"> <!-- <v-divider class="my-2"></v-divider> -->
<template #prepend>
<IconDashboard />
</template>
<v-list-item-title class="text-body-2 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')"> <!-- View All Projects -->
<template #prepend> <v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<IconBucket /> <template #prepend>
</template> <IconAllProjects />
<v-list-item-title class="text-body-2 ml-3"> </template>
Buckets <v-list-item-title class="ml-3">
</v-list-item-title> View All Projects
</v-list-item> </v-list-item-title>
</v-list-item>
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/access`" class="my-1" rounded="lg" @click="() => registerLinkClick('/access')"> <!-- Create New Project -->
<template #prepend> <v-list-item link @click="isCreateProjectDialogShown = true">
<IconAccess size="18" /> <template #prepend>
</template> <IconNew />
<v-list-item-title class="text-body-2 ml-3"> </template>
Access <v-list-item-title class="ml-3">
</v-list-item-title> Create New Project
</v-list-item> </v-list-item-title>
</v-list-item>
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/team`" class="my-1" rounded="lg" @click="() => registerLinkClick('/team')"> <v-divider class="my-2" />
<template #prepend>
<IconTeam size="18" />
</template>
<v-list-item-title class="text-body-2 ml-3">
Team
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" /> <!-- Manage Passphrase -->
<v-list-item link class="mt-1" @click="isManagePassphraseDialogShown = true">
<template #prepend>
<IconPassphrase />
</template>
<v-list-item-title class="ml-3">
Manage Passphrase
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Resources Menu --> <v-divider class="my-2" />
<v-list-item link class="rounded-lg">
<v-menu activator="parent" location="end" transition="scale-transition">
<v-list class="pa-2">
<v-list-item
class="py-3"
rounded="lg"
href="https://docs.storj.io/"
target="_blank"
rel="noopener noreferrer"
>
<template #prepend>
<!-- <img src="@poc/assets/icon-docs.svg" alt="Docs"> -->
<IconDocs />
</template>
<v-list-item-title class="text-body-2 mx-3">
Documentation
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
<small>Go to the Storj docs.</small>
</v-list-item-subtitle>
</v-list-item>
<v-list-item <!--
class="py-3" <v-list-item
rounded="lg" router-link
href="https://forum.storj.io/" :to="`/projects/${selectedProject.urlId}/dashboard`"
target="_blank" class="my-1 py-3"
rel="noopener noreferrer" tabindex="0"
> @click="() => registerLinkClick('/dashboard')"
<template #prepend> >
<IconForum /> <template #prepend>
</template> <IconDashboard />
<v-list-item-title class="text-body-2 mx-3"> </template>
Community Forum <v-list-item-title class="ml-3">
</v-list-item-title> Overview
<v-list-item-subtitle class="mx-3"> </v-list-item-title>
<small>Join our global community.</small> </v-list-item>
</v-list-item-subtitle> -->
</v-list-item>
<v-list-item <navigation-item title="Overview" :to="`/projects/${selectedProject.urlId}/dashboard`">
class="py-3" <template #prepend>
rounded="lg" <IconDashboard />
href="https://supportdcs.storj.io/hc/en-us" </template>
target="_blank" </navigation-item>
rel="noopener noreferrer"
>
<template #prepend>
<IconSupport />
</template>
<v-list-item-title class="text-body-2 mx-3">
Storj Support
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
<small>Need help? Get support.</small>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
<template #prepend> <navigation-item title="Buckets" :to="`/projects/${selectedProject.urlId}/buckets`">
<IconResources /> <template #prepend>
</template> <IconBucket />
<v-list-item-title class="text-body-2 ml-3"> </template>
Resources </navigation-item>
</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" /> <navigation-item title="Access" :to="`/projects/${selectedProject.urlId}/access`">
<template #prepend>
<IconAccess size="18" />
</template>
</navigation-item>
<!-- <v-list-item link class="my-1" router-link to="/design-library" rounded="lg"> <navigation-item title="Team" :to="`/projects/${selectedProject.urlId}/team`">
<template v-slot:prepend> <template #prepend>
<img src="@poc/assets/icon-bookmark.svg" alt="Design Library" class="mr-3"> <IconTeam size="18" />
</template> </template>
<v-list-item-title class="text-body-2"> </navigation-item>
Design Library
</v-list-item-title> <v-divider class="my-2" />
</v-list-item> -->
</v-list> <!-- Resources Menu -->
<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"
href="https://docs.storj.io/"
target="_blank"
rel="noopener noreferrer"
>
<template #prepend>
<!-- <img src="@poc/assets/icon-docs.svg" alt="Docs"> -->
<IconDocs />
</template>
<v-list-item-title class="mx-3">
Documentation
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
<small>Go to the Storj docs.</small>
</v-list-item-subtitle>
</v-list-item>
<v-list-item
class="py-3"
href="https://forum.storj.io/"
target="_blank"
rel="noopener noreferrer"
>
<template #prepend>
<IconForum />
</template>
<v-list-item-title class="mx-3">
Community Forum
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
<small>Join our global community.</small>
</v-list-item-subtitle>
</v-list-item>
<v-list-item
class="py-3"
href="https://supportdcs.storj.io/hc/en-us"
target="_blank"
rel="noopener noreferrer"
>
<template #prepend>
<IconSupport />
</template>
<v-list-item-title class="mx-3">
Storj Support
</v-list-item-title>
<v-list-item-subtitle class="mx-3">
<small>Need help? Get support.</small>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
<v-divider class="my-2" />
<!-- <v-list-item link class="my-1" router-link to="/design-library" rounded="lg">
<template v-slot:prepend>
<img src="@poc/assets/icon-bookmark.svg" alt="Design Library" class="mr-3">
</template>
<v-list-item-title class="text-body-2">
Design Library
</v-list-item-title>
</v-list-item> -->
</v-sheet> </v-sheet>
</v-navigation-drawer> </v-navigation-drawer>
@ -304,6 +298,7 @@ import IconSupport from '@poc/components/icons/IconSupport.vue';
import IconResources from '@poc/components/icons/IconResources.vue'; import IconResources from '@poc/components/icons/IconResources.vue';
import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue'; import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue';
import ManagePassphraseDialog from '@poc/components/dialogs/ManagePassphraseDialog.vue'; import ManagePassphraseDialog from '@poc/components/dialogs/ManagePassphraseDialog.vue';
import NavigationItem from '@poc/layouts/default/NavigationItem.vue';
const analyticsStore = useAnalyticsStore(); const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();