Restore previous focus when modal dialogs close.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<p class="view-lede">{{ subheading }}</p>
|
||||
</header>
|
||||
|
||||
<section class="section-block">
|
||||
<section class="section-block" ref="userInfoSection">
|
||||
<div class="section-body">
|
||||
<UserBasicInfo
|
||||
v-if="user"
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-body host-actions">
|
||||
<div class="button-row">
|
||||
<div class="button-row" ref="buttonRow" @keydown="handleButtonRowKeydown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@@ -58,10 +58,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { getDirection, navigateButtonRow } from '@/utils/keynav'
|
||||
|
||||
defineProps({
|
||||
initializing: {
|
||||
@@ -73,6 +74,10 @@ defineProps({
|
||||
const authStore = useAuthStore()
|
||||
const currentHost = window.location.host
|
||||
|
||||
// Template refs for navigation
|
||||
const userInfoSection = ref(null)
|
||||
const buttonRow = ref(null)
|
||||
|
||||
const user = computed(() => authStore.userInfo?.user || null)
|
||||
const orgDisplayName = computed(() => authStore.userInfo?.org?.display_name || '')
|
||||
const roleDisplayName = computed(() => authStore.userInfo?.role?.display_name || '')
|
||||
@@ -105,6 +110,20 @@ const goToAuthSite = () => {
|
||||
const logout = async () => {
|
||||
await authStore.logout()
|
||||
}
|
||||
|
||||
// Keyboard navigation for button row
|
||||
const handleButtonRowKeydown = (event) => {
|
||||
const direction = getDirection(event)
|
||||
if (!direction) return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
navigateButtonRow(buttonRow.value, event.target, direction, { itemSelector: 'button' })
|
||||
}
|
||||
// Up does nothing (no elements above to navigate to)
|
||||
// Down does nothing (no elements below to navigate to)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -7,11 +7,73 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, nextTick } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { navigateButtonRow, getDirection, focusPreferred, focusDialogDefault } from '@/utils/keynav'
|
||||
|
||||
const props = defineProps({
|
||||
// Optional: provide a fallback element to focus if original element is gone
|
||||
focusFallback: { type: [HTMLElement, Object], default: null },
|
||||
// Optional: index to help find next sibling when item is deleted
|
||||
focusIndex: { type: Number, default: -1 },
|
||||
// Optional: selector for finding siblings when restoring focus
|
||||
focusSiblingSelector: { type: String, default: '' }
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
// Store the element that had focus before modal opened
|
||||
const previouslyFocusedElement = ref(null)
|
||||
|
||||
/**
|
||||
* Try to restore focus to the original element, or find a suitable fallback.
|
||||
* Called on unmount to restore focus when modal closes.
|
||||
*/
|
||||
const restoreFocus = () => {
|
||||
const prev = previouslyFocusedElement.value
|
||||
if (!prev) return
|
||||
|
||||
// Check if the original element still exists in DOM and is focusable
|
||||
if (document.body.contains(prev) && !prev.disabled) {
|
||||
prev.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// Original element is gone (deleted) - try to find a sibling
|
||||
if (props.focusSiblingSelector && props.focusIndex >= 0) {
|
||||
// Find container that has items matching the selector
|
||||
const containers = [
|
||||
props.focusFallback?.$el || props.focusFallback,
|
||||
prev.closest('[data-nav-group]'),
|
||||
prev.parentElement?.closest('section'),
|
||||
document.querySelector('.view-root')
|
||||
].filter(Boolean)
|
||||
|
||||
for (const container of containers) {
|
||||
if (!container) continue
|
||||
const siblings = container.querySelectorAll(props.focusSiblingSelector)
|
||||
if (siblings.length > 0) {
|
||||
// Try to focus the next item, or the previous if we were at the end
|
||||
const targetIndex = Math.min(props.focusIndex, siblings.length - 1)
|
||||
const target = siblings[targetIndex]
|
||||
if (target && !target.disabled) {
|
||||
target.focus()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the provided fallback element
|
||||
const fallback = props.focusFallback?.$el || props.focusFallback
|
||||
if (fallback && document.body.contains(fallback)) {
|
||||
const focusable = fallback.querySelector?.('button:not([disabled]), a, [tabindex="0"]') || fallback
|
||||
if (focusable?.focus) {
|
||||
focusable.focus()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleOverlayKeydown = (event) => {
|
||||
const direction = getDirection(event)
|
||||
if (!direction) return
|
||||
@@ -45,6 +107,9 @@ const handleOverlayKeydown = (event) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Save currently focused element before modal takes focus
|
||||
previouslyFocusedElement.value = document.activeElement
|
||||
|
||||
// Autofocus the most appropriate element:
|
||||
// - For form dialogs (rename, edit): focus first input and select text
|
||||
// - For other dialogs: focus primary button (or fallback)
|
||||
@@ -61,6 +126,11 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Restore focus when modal closes
|
||||
restoreFocus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -49,6 +49,8 @@ const emit = defineEmits(['close', 'copied'])
|
||||
const linkUrl = ref(null)
|
||||
const expiresAt = ref(null)
|
||||
const actionsRow = ref(null)
|
||||
// Store the element that had focus before modal opened
|
||||
const previouslyFocusedElement = ref(null)
|
||||
|
||||
async function generateLink() {
|
||||
try {
|
||||
@@ -102,6 +104,8 @@ const handleActionsKeydown = (event) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Save currently focused element before modal takes focus
|
||||
previouslyFocusedElement.value = document.activeElement
|
||||
// Hold backdrop before fetch to avoid gap if auth iframe shows
|
||||
holdGlobalBackdrop()
|
||||
generateLink()
|
||||
@@ -110,6 +114,11 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
// Release backdrop when modal closes
|
||||
releaseGlobalBackdrop()
|
||||
// Restore focus when modal closes
|
||||
const prev = previouslyFocusedElement.value
|
||||
if (prev && document.body.contains(prev) && !prev.disabled) {
|
||||
prev.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user