Overlays
Drawer code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/drawer.blade.php
@props([
'name',
'title' => null,
'description' => null,
'side' => 'right',
'maxWidth' => 'md',
'show' => false,
'dismissible' => true,
'closeOnBackdrop' => true,
'closeOnEscape' => true,
])
@php
/*
| Slide-over panel. Open/close by dispatching window events:
| $dispatch('open-drawer', 'filters') / $dispatch('close-drawer', 'filters')
|
| side: left, right
| maxWidth: sm, md, lg, xl, 2xl
|
| Usage:
| <x-ui.button @click="$dispatch('open-drawer', 'filters')">Filters</x-ui.button>
| <x-ui.drawer name="filters" title="Filters">
| Body...
| <x-slot:footer><x-ui.button block>Apply</x-ui.button></x-slot:footer>
| </x-ui.drawer>
*/
$isLeft = $side === 'left';
$maxWidthClass = [
'sm' => 'max-w-sm', 'md' => 'max-w-md', 'lg' => 'max-w-lg',
'xl' => 'max-w-xl', '2xl' => 'max-w-2xl',
][$maxWidth] ?? 'max-w-md';
$sideClass = $isLeft ? 'start-0' : 'end-0';
$enterStart = $isLeft ? '-translate-x-full' : 'translate-x-full';
$dialogId = 'drawer-'.md5($name);
$titleId = $dialogId.'-title';
$descriptionId = $dialogId.'-description';
@endphp
<div
x-data="{
show: @js($show),
previouslyFocused: null,
open() {
this.previouslyFocused = document.activeElement;
this.show = true;
this.$nextTick(() => this.focusFirst());
},
close() {
this.show = false;
this.$nextTick(() => this.previouslyFocused?.focus?.());
},
focusable() {
return Array.from(this.$refs.panel?.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\'-1\'])') ?? [])
.filter((el) => ! el.hasAttribute('disabled') && ! el.getAttribute('aria-hidden'));
},
focusFirst() {
(this.$refs.initial ?? this.focusable()[0] ?? this.$refs.panel)?.focus?.();
},
trap(event) {
if (! this.show || event.key !== 'Tab') { return; }
const items = this.focusable();
if (items.length === 0) { event.preventDefault(); return; }
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (! event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
},
}"
x-on:open-drawer.window="$event.detail === @js($name) ? open() : null"
x-on:close-drawer.window="$event.detail === @js($name) ? close() : null"
x-on:keydown.escape.window="@js($dismissible && $closeOnEscape) ? close() : null"
x-on:keydown.tab.capture="trap($event)"
x-effect="document.body.classList.toggle('overflow-hidden', show)"
x-show="show"
x-cloak
class="fixed inset-0 z-50 overflow-hidden"
>
<div
x-show="show"
x-transition:enter="ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
@if ($dismissible && $closeOnBackdrop) @click="close()" @endif
class="absolute inset-0 bg-gray-950/60 backdrop-blur-sm"
></div>
<div
x-ref="panel"
tabindex="-1"
x-show="show"
x-transition:enter="transform transition ease-out duration-300" x-transition:enter-start="{{ $enterStart }}" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in duration-200" x-transition:leave-start="translate-x-0" x-transition:leave-end="{{ $enterStart }}"
class="absolute inset-y-0 {{ $sideClass }} flex w-full {{ $maxWidthClass }} flex-col bg-white shadow-2xl dark:bg-gray-900"
role="dialog"
aria-modal="true"
@if ($title) aria-labelledby="{{ $titleId }}" @endif
@if ($description) aria-describedby="{{ $descriptionId }}" @endif
>
@if ($title || $description || $dismissible)
<div class="flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<div>
@if ($title)
<h3 id="{{ $titleId }}" class="text-lg font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
@endif
@if ($description)
<p id="{{ $descriptionId }}" class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $description }}</p>
@endif
</div>
@if ($dismissible)
<button x-ref="initial" type="button" @click="close()" class="-m-1.5 rounded-lg p-1.5 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800" aria-label="Close">
<x-ui.icon name="x" weight="bold" size="lg" />
</button>
@endif
</div>
@endif
<div class="flex-1 overflow-y-auto px-6 py-5 text-sm text-gray-600 dark:text-gray-300">
{{ $slot }}
</div>
@isset($footer)
<div class="flex items-center justify-end gap-3 border-t border-gray-200 px-6 py-4 dark:border-gray-800">
{{ $footer }}
</div>
@endisset
</div>
</div>