Skip to content

Commit 0054df2

Browse files
[6.x] Improve focus on page load (#13357)
* When pages load through Inertia focus on the main content if the focus is not explicitly set * Fix keyboard focus on search-type pages like entry indexes * Try to fix * Fix focusing on the main element * Simplify focus management * Simplify focus management * Fix a few places where autofocus wasn't catching properly on an Inertia request such as "create" pages * Trim whitespace * Remove some focus outlines for elements that aren't focusable anyway --------- Co-authored-by: Jack McDade <jack@jackmcdade.com>
1 parent f828b11 commit 0054df2

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

resources/js/components/ui/Listing/Search.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ defineExpose({ focus });
1717
<div class="flex-1 max-w-sm" :class="{ 'max-w-60!': activeFilterBadgeCount > 2 }">
1818
<label for="listings-search" class="sr-only">{{ __('Search entries') }}</label>
1919
<Input
20-
autofocus
20+
:focus="true"
2121
ref="input"
2222
icon="magnifying-glass"
2323
id="listings-search"

resources/js/pages/layout/Layout.vue

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { ConfigProvider } from 'reka-ui';
55
import SessionExpiry from '@/components/SessionExpiry.vue';
66
import LicensingAlert from '@/components/LicensingAlert.vue';
77
import PortalTargets from '@/components/portals/PortalTargets.vue';
8-
import Tooltips from '@/components/Tooltips.vue';
9-
import { provide, watch, ref } from 'vue';
8+
import { provide, watch, ref, onMounted, onUnmounted, nextTick } from 'vue';
9+
import { router } from '@inertiajs/vue3';
1010
import useBodyClasses from './body-classes.js';
1111
import useStatamicPageProps from '@/composables/page-props.js';
1212
import useMaxWidthToggle from '@/composables/use-max-width-toggle.js';
@@ -27,6 +27,49 @@ provide('layout', {
2727
isMaxWidthEnabled,
2828
toggleMaxWidth: toggle,
2929
});
30+
31+
// Focus management: focus main element if no input has auto-focus
32+
let navigationListener = null;
33+
34+
function focusMain() {
35+
// Wait for components to mount and autofocus to process
36+
nextTick(() => {
37+
requestAnimationFrame(() => {
38+
setTimeout(() => {
39+
// If an input is already focused, we're done
40+
if (document.activeElement?.matches('input, textarea, select, [contenteditable]')) {
41+
return;
42+
}
43+
44+
// Find any input with autofocus attribute (including nested in UI components)
45+
const autofocusInput = document.querySelector('input[autofocus], textarea[autofocus], select[autofocus]') ||
46+
document.querySelector('[data-ui-input] input[autofocus]');
47+
48+
// If autofocus input exists but isn't focused, focus it manually
49+
if (autofocusInput && document.activeElement !== autofocusInput) {
50+
autofocusInput.focus();
51+
return;
52+
}
53+
54+
// Otherwise, focus the content card
55+
if (!autofocusInput) {
56+
document.querySelector('#content-card')?.focus();
57+
}
58+
}, 100);
59+
});
60+
});
61+
}
62+
63+
onMounted(() => {
64+
navigationListener = router.on('success', focusMain);
65+
focusMain();
66+
});
67+
68+
onUnmounted(() => {
69+
if (navigationListener) {
70+
navigationListener();
71+
}
72+
});
3073
</script>
3174
3275
<template>
@@ -38,8 +81,8 @@ provide('layout', {
3881
<main id="main" class="flex bg-body-bg dark:border-t dark:border-body-border rounded-t-2xl fixed top-14 inset-x-0 bottom-0 min-h-[calc(100vh-3.5rem)]">
3982
<Nav />
4083
<!-- The data attribute allows CSS to target elements when max-width is disabled. -->
41-
<div id="main-content" class="main-content sm:p-2 h-full flex-1 overflow-y-auto rounded-t-2xl" :data-max-width-enabled="isMaxWidthEnabled">
42-
<div id="content-card" class="relative content-card grid min-h-full mx-auto">
84+
<div id="main-content" class="main-content sm:p-2 h-full flex-1 overflow-y-auto focus:outline-none rounded-t-2xl" :data-max-width-enabled="isMaxWidthEnabled">
85+
<div id="content-card" tabindex="-1" class="focus:outline-none relative content-card grid min-h-full mx-auto">
4386
<!-- Data attribute used by the CSS style tag below to override max-width when disabled.-->
4487
<div class="w-full mx-auto max-w-page" data-max-width-wrapper>
4588
<slot />
@@ -74,9 +117,9 @@ provide('layout', {
74117
</template>
75118
76119
<style>
77-
/*
120+
/*
78121
Max-width override CSS:
79-
When max-width is disabled (data-max-width-enabled="false"),
122+
When max-width is disabled (data-max-width-enabled="false"),
80123
this rule removes the max-width constraint from elements tagged with data-max-width-wrapper.
81124
82125
This allows the content to expand to full width when the toggle is disabled,

0 commit comments

Comments
 (0)