Implement keyboard navigation using arrow keys in the whole application. #2
@@ -3,7 +3,9 @@ import '@/assets/style.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import AdminApp from './AdminApp.vue'
|
||||
import { initKeyboardNavigation } from '@/utils/keynav'
|
||||
|
||||
const app = createApp(AdminApp)
|
||||
app.use(createPinia())
|
||||
app.mount('#admin-app')
|
||||
initKeyboardNavigation()
|
||||
|
||||
@@ -3,9 +3,11 @@ import '@/assets/style.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import { initKeyboardNavigation } from '@/utils/keynav'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
|
||||
app.mount('#app')
|
||||
initKeyboardNavigation()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import RestrictedApi from './RestrictedApi.vue'
|
||||
import '@/assets/style.css'
|
||||
import { initKeyboardNavigation } from '@/utils/keynav'
|
||||
|
||||
createApp(RestrictedApi).mount('#app')
|
||||
initKeyboardNavigation()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './RestrictedForward.vue'
|
||||
import '@/assets/style.css'
|
||||
import { initKeyboardNavigation } from '@/utils/keynav'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
initKeyboardNavigation()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<section class="section-block" v-else-if="!canRegister">
|
||||
<div class="section-body center">
|
||||
<div class="button-row center" style="justify-content: center;" @keydown="handleButtonKeydown">
|
||||
<div class="button-row center" style="justify-content: center;">
|
||||
<button class="btn-secondary" @click="goHome">Return to sign-in</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,6 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import passkey from '@/utils/passkey'
|
||||
import { getSettings, uiBasePath } from '@/utils/settings'
|
||||
import { apiJson, ApiError, getUserFriendlyErrorMessage } from '@/utils/api'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
const status = reactive({
|
||||
show: false,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import ResetApp from './ResetApp.vue'
|
||||
import '@/assets/style.css'
|
||||
import { initKeyboardNavigation } from '@/utils/keynav'
|
||||
|
||||
createApp(ResetApp).mount('#app')
|
||||
initKeyboardNavigation()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import NameEditForm from '@/components/NameEditForm.vue'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
const props = defineProps({
|
||||
dialog: Object,
|
||||
@@ -82,7 +81,7 @@ const NAME_EDIT_TYPES = new Set(['org-update', 'role-update', 'user-update-name'
|
||||
<p>{{ dialog.data.message }}</p>
|
||||
</template>
|
||||
<div v-if="dialog.error && !NAME_EDIT_TYPES.has(dialog.type)" class="error small">{{ dialog.error }}</div>
|
||||
<div v-if="!NAME_EDIT_TYPES.has(dialog.type)" class="modal-actions" @keydown="handleButtonKeydown">
|
||||
<div v-if="!NAME_EDIT_TYPES.has(dialog.type)" class="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="message-container">
|
||||
<div class="message-content">
|
||||
<h2>🔒 Access Denied</h2>
|
||||
<div class="button-row" @keydown="handleButtonKeydown">
|
||||
<div class="button-row">
|
||||
<button class="btn-secondary" @click="goBack">Back</button>
|
||||
<button class="btn-primary" @click="$emit('reload')">Reload Page</button>
|
||||
</div>
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
<script setup>
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
defineEmits(['reload'])
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h1>📱 Add Another Device</h1>
|
||||
<p class="view-lede">Generate a one-time link to set up passkeys on a new device.</p>
|
||||
</header>
|
||||
<div class="button-row" style="margin-top:1rem;" @keydown="handleButtonKeydown">
|
||||
<div class="button-row" style="margin-top:1rem;">
|
||||
<button @click="showModal = true" class="btn-primary">Generate Registration Link</button>
|
||||
<button @click="authStore.currentView = 'profile'" class="btn-secondary">Back to Profile</button>
|
||||
</div>
|
||||
@@ -22,7 +22,6 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userName = ref(null)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-body host-actions">
|
||||
<div class="button-row" @keydown="handleButtonKeydown">
|
||||
<div class="button-row">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@@ -62,7 +62,6 @@ import { computed } from 'vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
defineProps({
|
||||
initializing: {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { handleButtonKeydown, getDirection } from '@/utils/keynav'
|
||||
import { getDirection } from '@/utils/keynav'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
@@ -69,7 +69,7 @@ const handleActionsKeydown = (event) => {
|
||||
inputRef.value?.focus()
|
||||
return
|
||||
}
|
||||
handleButtonKeydown(event)
|
||||
// Left/right navigation is handled globally by keynav
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<p v-if="error" class="error-message" style="margin-top: 0.5rem;">{{ error }}</p>
|
||||
|
||||
<div class="button-row" style="margin-top: 0.75rem; display: flex; gap: 0.5rem;" @keydown="handleButtonKeydown">
|
||||
<div class="button-row" style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@@ -91,7 +91,6 @@ import { getSettings } from '@/utils/settings'
|
||||
import { getUniqueMatch, isValidWord, isValidPrefix } from '@/utils/wordlist'
|
||||
import { solvePoW } from '@/utils/pow'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { handleButtonKeydown } from '@/utils/keynav'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: 'Help Another Device Sign In' },
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="section-body center">
|
||||
<!-- Local passkey authentication view -->
|
||||
<div v-if="authView === 'local'" class="auth-view">
|
||||
<div class="button-row center" ref="buttonRow" @keydown="handleButtonKeydown">
|
||||
<div class="button-row center" ref="buttonRow">
|
||||
<slot name="actions"
|
||||
:loading="loading"
|
||||
:can-authenticate="canAuthenticate"
|
||||
@@ -60,7 +60,7 @@ import passkey from '@/utils/passkey'
|
||||
import { getSettings, uiBasePath } from '@/utils/settings'
|
||||
import { fetchJson, getUserFriendlyErrorMessage } from '@/utils/api'
|
||||
import RemoteAuthRequest from '@/components/RemoteAuthRequest.vue'
|
||||
import { handleButtonKeydown, focusDialogButton } from '@/utils/keynav'
|
||||
import { focusDialogButton } from '@/utils/keynav'
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
* - data-nav-group: Marks a navigation group container
|
||||
* - data-nav-primary: Marks the preferred element to focus when entering a group
|
||||
* - data-nav-items: CSS selector for focusable items within the group (default: 'button, a, [tabindex="0"], [tabindex="-1"]:not([disabled])')
|
||||
*
|
||||
* Automatic navigation:
|
||||
* - Buttons/links inside .button-row or .modal-actions get automatic left/right arrow navigation
|
||||
* - No need for explicit @keydown handlers on elements
|
||||
* - Call initKeyboardNavigation() once at app startup to enable global navigation
|
||||
*/
|
||||
|
||||
// Direction mapping from key events
|
||||
@@ -406,3 +411,64 @@ export const installKeyboardNav = (container, options = {}) => {
|
||||
|
||||
return () => container.removeEventListener('keydown', handler)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global Automatic Keyboard Navigation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Selector for containers that should have automatic button row navigation
|
||||
*/
|
||||
const BUTTON_ROW_SELECTOR = '.button-row, .modal-actions, .actions, .role-actions, .ancillary-actions'
|
||||
|
||||
/**
|
||||
* Global keydown handler for automatic button row navigation.
|
||||
* Handles arrow key navigation for buttons/links within .button-row or .modal-actions containers.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const globalKeydownHandler = (event) => {
|
||||
const direction = getDirection(event)
|
||||
if (!direction) return
|
||||
|
||||
// Only handle buttons and links
|
||||
const target = event.target
|
||||
if (target.tagName !== 'BUTTON' && target.tagName !== 'A') return
|
||||
|
||||
// Find the button row container
|
||||
const container = target.closest(BUTTON_ROW_SELECTOR)
|
||||
if (!container) return
|
||||
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
event.preventDefault()
|
||||
navigateButtonRow(container, target, direction, { itemSelector: 'button:not([disabled]), a' })
|
||||
}
|
||||
// Note: up/down navigation is intentionally not handled globally
|
||||
// Components can add their own handlers for vertical navigation between groups
|
||||
}
|
||||
|
||||
let globalNavInitialized = false
|
||||
|
||||
/**
|
||||
* Initialize global keyboard navigation.
|
||||
* Call this once at app startup to enable automatic arrow key navigation
|
||||
* for buttons within .button-row and .modal-actions containers.
|
||||
* Safe to call multiple times (only initializes once).
|
||||
*/
|
||||
export const initKeyboardNavigation = () => {
|
||||
if (globalNavInitialized) return
|
||||
if (typeof document === 'undefined') return // SSR safety
|
||||
|
||||
document.addEventListener('keydown', globalKeydownHandler)
|
||||
globalNavInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup global keyboard navigation (useful for testing).
|
||||
*/
|
||||
export const destroyKeyboardNavigation = () => {
|
||||
if (!globalNavInitialized) return
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
document.removeEventListener('keydown', globalKeydownHandler)
|
||||
globalNavInitialized = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user