Global Search Accessibility

Improved the accessibility of the global search field in the header.
- When focus is lost, the dropdown disappears
- The tab key and up/down arrows can be used to cycle through results
- When using the keyboard to cycle through results, focus loops back to the top result
- Pressing the esc key will clear the search text and remove the focus lock on the search

Resolves #25
This commit is contained in:
Refringe 2024-09-17 11:59:28 -04:00
parent 9a900bbece
commit df779135c1
Signed by: Refringe
SSH Key Fingerprint: SHA256:t865XsQpfTeqPRBMN2G6+N8wlDjkgUCZF3WGW6O9N/k
4 changed files with 34 additions and 26 deletions

View File

@ -51,8 +51,7 @@ class GlobalSearch extends Component
$results['total'] = $this->countTotalResults($results['data']);
}
$this->showDropdown = Str::length($query) > 0;
$this->noResults = $results['total'] === 0 && $this->showDropdown;
$this->noResults = $results['total'] === 0;
return $results;
}
@ -94,14 +93,4 @@ class GlobalSearch extends Component
return $carry + $result->count();
}, 0);
}
/**
* Clear the search query and hide the dropdown.
*/
public function clearSearch(): void
{
$this->query = '';
$this->showDropdown = false;
$this->noResults = false;
}
}

View File

@ -1,5 +1,11 @@
<div id="search-results" aria-live="polite" class="{{ $showDropdown ? 'block' : 'hidden' }} absolute z-10 top-11 w-full mx-auto max-w-2xl transform overflow-hidden rounded-md bg-white dark:bg-gray-900 shadow-2xl border border-gray-300 dark:border-gray-700 transition-all">
@if($showDropdown)
<div id="search-results"
x-cloak
x-show="showDropdown && query.length"
x-transition
aria-live="polite"
class="{{ $showDropdown ? 'block' : 'hidden' }} absolute z-10 top-11 w-full mx-auto max-w-2xl transform overflow-hidden rounded-md bg-white dark:bg-gray-900 shadow-2xl border border-gray-300 dark:border-gray-700 transition-all"
>
@if ($showDropdown)
<h2 class="sr-only">{{ __('Search Results') }}</h2>
<div class="max-h-96 scroll-py-2 overflow-y-auto" role="list">
@foreach($results['data'] as $typeName => $typeResults)

View File

@ -1,5 +1,13 @@
<div class="flex flex-1 justify-center px-2 lg:ml-6 lg:justify-end">
<div class="w-full max-w-lg lg:max-w-md">
<div x-data="{ query: $wire.entangle('query'), showDropdown: $wire.entangle('showDropdown'), noResults: $wire.entangle('noResults') }"
@keydown.esc.window="showDropdown = false"
class="flex flex-1 justify-center px-2 lg:ml-6 lg:justify-end"
>
<div class="w-full max-w-lg lg:max-w-md"
x-trap="showDropdown && query.length"
@click.away="showDropdown = false"
@keydown.down.prevent="$focus.wrap().next()"
@keydown.up.prevent="$focus.wrap().previous()"
>
<label for="search" class="sr-only">{{ __('Search') }}</label>
<search class="relative group" role="search">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@ -10,10 +18,11 @@
<input id="global-search"
type="search"
wire:model.live="query"
@keydown.escape.window="$wire.clearSearch()"
@focus="showDropdown = true"
@keydown.escape.window="$wire.query = ''; showDropdown = false; $wire.$refresh()"
placeholder="{{ __('Search') }}"
aria-controls="search-results"
aria-expanded="{{ $showDropdown ? 'true' : 'false' }}"
:aria-expanded="showDropdown"
aria-label="{{ __('Search') }}"
class="block w-full rounded-md border-0 bg-white dark:bg-gray-700 py-1.5 pl-10 pr-3 text-gray-900 dark:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 dark:placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 dark:focus:bg-gray-200 dark:focus:text-black dark:focus:ring-0 sm:text-sm sm:leading-6"
/>

View File

@ -46,7 +46,10 @@
@auth
{{-- Profile Dropdown --}}
<div x-data="{ profileDropdownOpen: false, openedWithKeyboard: false }" @keydown.esc.window="profileDropdownOpen = false, openedWithKeyboard = false" class="relative">
<div x-data="{ profileDropdownOpen: false, openedWithKeyboard: false }"
@keydown.esc.window="profileDropdownOpen = false, openedWithKeyboard = false"
class="relative"
>
<button id="user-menu-button" type="button" @click="profileDropdownOpen = ! profileDropdownOpen" @keydown.space.prevent="openedWithKeyboard = true" @keydown.enter.prevent="openedWithKeyboard = true" @keydown.down.prevent="openedWithKeyboard = true" class="relative flex rounded-full bg-gray-800 text-sm text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" :class="profileDropdownOpen || openedWithKeyboard ? 'text-black dark:text-white' : 'text-slate-700 dark:text-slate-300'" :aria-expanded="profileDropdownOpen || openedWithKeyboard" aria-haspopup="true">
<span class="absolute -inset-1.5"></span>
<span class="sr-only">{{ __('Open user menu') }}</span>
@ -60,7 +63,8 @@
@keydown.down.prevent="$focus.wrap().next()"
@keydown.up.prevent="$focus.wrap().previous()"
class="absolute top-11 right-0 z-10 flex w-full min-w-[12rem] flex-col divide-y divide-slate-300 overflow-hidden rounded-xl border border-gray-300 bg-gray-100 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800"
role="menu">
role="menu"
>
<div class="flex flex-col py-1.5">
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">