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,260 +3,254 @@
<template>
<v-navigation-drawer v-model="model" class="py-1">
<v-sheet>
<v-list class="px-2" color="default" variant="flat">
<!-- Project -->
<v-list-item link class="pa-4 rounded-lg">
<v-menu activator="parent" location="end" transition="scale-transition">
<!-- 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')">
<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>
My Projects
</v-chip>
</v-list-item-title>
</v-list-item>
<v-sheet class="pa-2">
<!-- Project -->
<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>
<!-- Selected Project -->
<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'">
{{ project.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2" />
<!-- Project Menu -->
<v-list class="pa-2">
<!-- My Projects -->
<template v-if="ownProjects.length">
<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>
<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 -->
<template v-if="sharedProjects.length">
<v-list-item rounded="lg" link router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconProject />
</template>
<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>
Shared Projects
</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" />
<!-- Selected Project -->
<v-list-item
v-for="project in ownProjects"
: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>
<!-- Project Settings -->
<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 />
<v-divider class="my-2" />
</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">
<!-- Shared With Me -->
<template v-if="sharedProjects.length">
<v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconProject />
</template>
<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>
</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')">
<template #prepend>
<IconDashboard />
</template>
<v-list-item-title class="text-body-2 ml-3">
Overview
</v-list-item-title>
</v-list-item>
<!-- <v-divider class="my-2"></v-divider> -->
<v-list-item link router-link :to="`/projects/${selectedProject.urlId}/buckets`" class="my-1" rounded="lg" @click="() => registerLinkClick('/buckets')">
<template #prepend>
<IconBucket />
</template>
<v-list-item-title class="text-body-2 ml-3">
Buckets
</v-list-item-title>
</v-list-item>
<!-- View All Projects -->
<v-list-item router-link to="/projects" @click="() => registerLinkClick('/projects')">
<template #prepend>
<IconAllProjects />
</template>
<v-list-item-title class="ml-3">
View All Projects
</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')">
<template #prepend>
<IconAccess size="18" />
</template>
<v-list-item-title class="text-body-2 ml-3">
Access
</v-list-item-title>
</v-list-item>
<!-- Create New Project -->
<v-list-item link @click="isCreateProjectDialogShown = true">
<template #prepend>
<IconNew />
</template>
<v-list-item-title class="ml-3">
Create New Project
</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')">
<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" />
<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-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-divider class="my-2" />
<v-list-item
class="py-3"
rounded="lg"
href="https://forum.storj.io/"
target="_blank"
rel="noopener noreferrer"
>
<template #prepend>
<IconForum />
</template>
<v-list-item-title class="text-body-2 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
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="ml-3">
Overview
</v-list-item-title>
</v-list-item>
-->
<v-list-item
class="py-3"
rounded="lg"
href="https://supportdcs.storj.io/hc/en-us"
target="_blank"
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>
<navigation-item title="Overview" :to="`/projects/${selectedProject.urlId}/dashboard`">
<template #prepend>
<IconDashboard />
</template>
</navigation-item>
<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>
<navigation-item title="Buckets" :to="`/projects/${selectedProject.urlId}/buckets`">
<template #prepend>
<IconBucket />
</template>
</navigation-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">
<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-list>
<navigation-item title="Team" :to="`/projects/${selectedProject.urlId}/team`">
<template #prepend>
<IconTeam size="18" />
</template>
</navigation-item>
<v-divider class="my-2" />
<!-- 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-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();