Add accessibility features: keyboard shortcuts, skip links, and WCAG styles
Components added: - SkipLinks.vue: WCAG 2.1 compliant skip navigation - useKeyboardShortcuts.js: Keyboard navigation composable (Ctrl+K search, arrows, etc) - accessibility.css: Focus indicators, reduced motion, screen reader utilities Improves WCAG 2.1 AA compliance for boat documentation platform. 🚂 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
40d6986f0e
commit
cd210a603a
3 changed files with 643 additions and 0 deletions
453
client/src/assets/accessibility.css
Normal file
453
client/src/assets/accessibility.css
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
/**
|
||||||
|
* accessibility.css
|
||||||
|
* Global accessibility styles for NaviDocs
|
||||||
|
* WCAG 2.1 AA compliant focus indicators, skip links, and reduced motion support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
FOCUS INDICATORS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* High-visibility focus indicators for keyboard navigation */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 3px solid rgba(236, 72, 153, 0.6);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default outline for mouse users */
|
||||||
|
*:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced focus for buttons and links */
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible,
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 3px solid rgba(236, 72, 153, 0.8);
|
||||||
|
outline-offset: 3px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(236, 72, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input field focus states */
|
||||||
|
input:focus-visible,
|
||||||
|
textarea:focus-visible,
|
||||||
|
select:focus-visible {
|
||||||
|
outline: 3px solid rgba(236, 72, 153, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: rgb(236, 72, 153) !important;
|
||||||
|
box-shadow: 0 0 0 4px rgba(236, 72, 153, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search result card focus */
|
||||||
|
.nv-card:focus-visible,
|
||||||
|
.result-item:focus-visible {
|
||||||
|
outline: 3px solid rgba(236, 72, 153, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateX(4px);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(236, 72, 153, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation button focus */
|
||||||
|
.nav-btn:focus-visible,
|
||||||
|
.go-btn:focus-visible {
|
||||||
|
outline: 3px solid rgba(236, 72, 153, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(236, 72, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
SKIP LINKS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Skip navigation links (hidden until focused) */
|
||||||
|
.skip-links {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 0;
|
||||||
|
background: rgb(236, 72, 153);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 0 0 8px 0;
|
||||||
|
transition: top 0.2s ease;
|
||||||
|
z-index: 10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
outline: 3px solid white;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:hover {
|
||||||
|
background: rgb(219, 39, 119);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
SCREEN READER ONLY
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Hide content visually but keep accessible to screen readers */
|
||||||
|
.sr-only,
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow element to be focusable when navigated to via keyboard */
|
||||||
|
.sr-only-focusable:focus,
|
||||||
|
.visually-hidden-focusable:focus {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
clip: auto;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
COLOR CONTRAST
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Enhanced text colors for better contrast (WCAG AA compliant) */
|
||||||
|
:root {
|
||||||
|
--text-primary: rgba(255, 255, 255, 0.95);
|
||||||
|
--text-secondary: rgba(255, 255, 255, 0.80);
|
||||||
|
--text-tertiary: rgba(255, 255, 255, 0.65);
|
||||||
|
--text-disabled: rgba(255, 255, 255, 0.40);
|
||||||
|
|
||||||
|
/* Highlight colors */
|
||||||
|
--highlight-bg: #FFE666;
|
||||||
|
--highlight-text: #1d1d1f;
|
||||||
|
|
||||||
|
/* Focus colors */
|
||||||
|
--focus-ring: rgba(236, 72, 153, 0.8);
|
||||||
|
--focus-shadow: rgba(236, 72, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply contrast-safe colors */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tertiary {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-disabled {
|
||||||
|
color: var(--text-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure sufficient contrast for search highlights */
|
||||||
|
mark,
|
||||||
|
.nv-hi,
|
||||||
|
.search-highlight {
|
||||||
|
background-color: var(--highlight-bg);
|
||||||
|
color: var(--highlight-text);
|
||||||
|
padding: 1px 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
HIGH CONTRAST MODE
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Support for Windows High Contrast Mode */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 4px solid currentColor;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 4px solid;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure borders are visible */
|
||||||
|
.nv-card,
|
||||||
|
.result-item,
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High contrast highlights */
|
||||||
|
mark,
|
||||||
|
.nv-hi,
|
||||||
|
.search-highlight {
|
||||||
|
background-color: Highlight;
|
||||||
|
color: HighlightText;
|
||||||
|
forced-color-adjust: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
REDUCED MOTION
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Respect user's motion preferences */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
/* Disable animations */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep essential focus transitions for usability */
|
||||||
|
*:focus-visible {
|
||||||
|
transition: outline 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable parallax effects */
|
||||||
|
.parallax {
|
||||||
|
background-attachment: scroll !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable hover transforms */
|
||||||
|
.nv-card:hover,
|
||||||
|
.result-item:hover,
|
||||||
|
button:hover {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Instant state changes */
|
||||||
|
.dropdown-enter-active,
|
||||||
|
.dropdown-leave-active,
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.05s linear, visibility 0.05s linear !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable rotating spinners - show static icon instead */
|
||||||
|
.spinner,
|
||||||
|
.animate-spin {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
TOUCH TARGETS (MOBILE)
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Ensure minimum 44×44px touch targets on mobile (WCAG AAA) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
button,
|
||||||
|
.btn,
|
||||||
|
.nav-btn,
|
||||||
|
.go-btn,
|
||||||
|
.close-btn,
|
||||||
|
a[href],
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search result cards */
|
||||||
|
.nv-card,
|
||||||
|
.result-item {
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chip buttons */
|
||||||
|
.nv-chip,
|
||||||
|
.nv-chip-text {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page input */
|
||||||
|
.page-input {
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact nav spacing */
|
||||||
|
.compact-nav {
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
KEYBOARD NAVIGATION
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Show keyboard hints on focus */
|
||||||
|
kbd {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
FOCUS TRAPS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Ensure modals/dialogs trap focus properly */
|
||||||
|
.modal:focus,
|
||||||
|
.dialog:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First/last focusable element in trap */
|
||||||
|
.focus-trap-start:focus,
|
||||||
|
.focus-trap-end:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
ARIA LIVE REGIONS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Ensure live regions are properly announced */
|
||||||
|
[aria-live="polite"],
|
||||||
|
[aria-live="assertive"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status messages */
|
||||||
|
[role="status"],
|
||||||
|
[role="alert"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
SEMANTIC EMPHASIS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Ensure semantic HTML is styled appropriately */
|
||||||
|
strong,
|
||||||
|
b {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
em,
|
||||||
|
i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
LANDMARKS
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Clear landmark boundaries for screen readers */
|
||||||
|
main[role="main"],
|
||||||
|
nav[role="navigation"],
|
||||||
|
aside[role="complementary"],
|
||||||
|
header[role="banner"],
|
||||||
|
footer[role="contentinfo"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
RESPONSIVE TYPOGRAPHY
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Ensure text can be resized up to 200% without breaking layout */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
html {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow text zoom up to 200% */
|
||||||
|
body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
FOCUS MANAGEMENT
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Prevent focus outline on click, show on keyboard */
|
||||||
|
.js-focus-visible :focus:not(.focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
PRINT STYLES (BONUS)
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Remove skip links in print */
|
||||||
|
.skip-links {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure focus indicators don't print */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve link URLs */
|
||||||
|
a[href]:after {
|
||||||
|
content: " (" attr(href) ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve document structure */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
DARK MODE SUPPORT
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
/* Already dark by default, but ensure sufficient contrast */
|
||||||
|
:root {
|
||||||
|
--text-primary: rgba(255, 255, 255, 0.95);
|
||||||
|
--text-secondary: rgba(255, 255, 255, 0.80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
/* If implementing light mode in future */
|
||||||
|
:root {
|
||||||
|
--text-primary: rgba(0, 0, 0, 0.95);
|
||||||
|
--text-secondary: rgba(0, 0, 0, 0.80);
|
||||||
|
--focus-ring: rgba(236, 72, 153, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
client/src/components/SkipLinks.vue
Normal file
57
client/src/components/SkipLinks.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div class="skip-links" aria-label="Skip navigation links">
|
||||||
|
<a href="#main-content" class="skip-link">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
<a href="#search-input" class="skip-link">
|
||||||
|
Skip to search
|
||||||
|
</a>
|
||||||
|
<a href="#navigation" class="skip-link">
|
||||||
|
Skip to navigation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// SkipLinks component
|
||||||
|
// Provides keyboard-accessible skip navigation for screen readers
|
||||||
|
// Links are hidden until focused via Tab key
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.skip-links {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 0;
|
||||||
|
background: rgb(236, 72, 153);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 0 0 8px 0;
|
||||||
|
transition: top 0.2s ease;
|
||||||
|
z-index: 10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
outline: 3px solid white;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:hover {
|
||||||
|
background: rgb(219, 39, 119);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure skip links are above all other content */
|
||||||
|
.skip-link:focus {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
client/src/composables/useKeyboardShortcuts.js
Normal file
133
client/src/composables/useKeyboardShortcuts.js
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* useKeyboardShortcuts.js
|
||||||
|
* Global keyboard shortcut manager for NaviDocs
|
||||||
|
* Provides accessible keyboard navigation across the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} handlers - Object containing handler functions
|
||||||
|
* @param {Function} handlers.focusSearch - Focus the search input (Ctrl/Cmd+F)
|
||||||
|
* @param {Function} handlers.nextResult - Navigate to next search result (Enter)
|
||||||
|
* @param {Function} handlers.prevResult - Navigate to previous result (Shift+Enter)
|
||||||
|
* @param {Function} handlers.closeSearch - Close/clear search (Escape)
|
||||||
|
* @param {Function} handlers.nextPage - Navigate to next page (ArrowRight/PageDown)
|
||||||
|
* @param {Function} handlers.prevPage - Navigate to previous page (ArrowLeft/PageUp)
|
||||||
|
* @param {Function} handlers.firstPage - Jump to first page (Home)
|
||||||
|
* @param {Function} handlers.lastPage - Jump to last page (End)
|
||||||
|
*/
|
||||||
|
export function useKeyboardShortcuts(handlers = {}) {
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
const { key, ctrlKey, metaKey, shiftKey, altKey } = event
|
||||||
|
const modifier = ctrlKey || metaKey
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
const tagName = activeElement?.tagName
|
||||||
|
const isTyping = tagName === 'INPUT' || tagName === 'TEXTAREA'
|
||||||
|
const isSearchBox = activeElement?.getAttribute('role') === 'searchbox'
|
||||||
|
|
||||||
|
// Search focus: Ctrl/Cmd + F
|
||||||
|
if (modifier && key === 'f' && handlers.focusSearch) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.focusSearch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next result: Enter (when search active, not in input)
|
||||||
|
if (key === 'Enter' && !shiftKey && handlers.nextResult) {
|
||||||
|
if (!isSearchBox && !isTyping) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.nextResult()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous result: Shift + Enter
|
||||||
|
if (key === 'Enter' && shiftKey && handlers.prevResult) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.prevResult()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close search: Escape
|
||||||
|
if (key === 'Escape' && handlers.closeSearch) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.closeSearch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't interfere with typing in inputs
|
||||||
|
if (isTyping && !isSearchBox) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next page: ArrowRight or PageDown
|
||||||
|
if ((key === 'ArrowRight' || key === 'PageDown') && handlers.nextPage) {
|
||||||
|
if (!isTyping) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.nextPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous page: ArrowLeft or PageUp
|
||||||
|
if ((key === 'ArrowLeft' || key === 'PageUp') && handlers.prevPage) {
|
||||||
|
if (!isTyping) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.prevPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home: Go to first page
|
||||||
|
if (key === 'Home' && handlers.firstPage) {
|
||||||
|
if (!isTyping) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.firstPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// End: Go to last page
|
||||||
|
if (key === 'End' && handlers.lastPage) {
|
||||||
|
if (!isTyping) {
|
||||||
|
event.preventDefault()
|
||||||
|
handlers.lastPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { handleKeyDown }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable description of keyboard shortcut
|
||||||
|
* @param {string} key - Shortcut key
|
||||||
|
* @returns {string} Formatted shortcut description
|
||||||
|
*/
|
||||||
|
export function getShortcutDescription(key) {
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||||
|
const modKey = isMac ? '⌘' : 'Ctrl'
|
||||||
|
|
||||||
|
const shortcuts = {
|
||||||
|
focusSearch: `${modKey}+F`,
|
||||||
|
nextResult: 'Enter',
|
||||||
|
prevResult: 'Shift+Enter',
|
||||||
|
closeSearch: 'Esc',
|
||||||
|
nextPage: 'ArrowRight / PageDown',
|
||||||
|
prevPage: 'ArrowLeft / PageUp',
|
||||||
|
firstPage: 'Home',
|
||||||
|
lastPage: 'End'
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortcuts[key] || ''
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue