Overlays
Modal code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/modal.blade.php
@props([
'name',
'title' => null,
'description' => null,
'maxWidth' => 'lg',
'show' => false,
'dismissible' => true,
'closeOnBackdrop' => true,
'closeOnEscape' => true,
])
@php
/*
| Accessible modal dialog. Open/close it by dispatching window events:
| $dispatch('open-modal', 'my-modal') / $dispatch('close-modal', 'my-modal')
|
| maxWidth: sm, md, lg, xl, 2xl, 3xl, 4xl, full
|
| Usage:
| <x-ui.button @click="$dispatch('open-modal', 'confirm')">Open</x-ui.button>
| <x-ui.modal name="confirm" title="Delete item?">
| Body...
| <x-slot:footer><x-ui.button variant="danger">Delete</x-ui.button></x-slot:footer>
| </x-ui.modal>
*/
$maxWidthClass = [
'sm' => 'sm:max-w-sm', 'md' => 'sm:max-w-md', 'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl', '2xl' => 'sm:max-w-2xl', '3xl' => 'sm:max-w-3xl',
'4xl' => 'sm:max-w-4xl', 'full' => 'sm:max-w-full',
][$maxWidth] ?? 'sm:max-w-lg';
$dialogId = 'modal-'.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-modal.window="$event.detail === @js($name) ? open() : null"
x-on:close-modal.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 flex items-center justify-center overflow-y-auto px-4 py-6"
>
<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="fixed inset-0 bg-gray-950/60 backdrop-blur-sm"
></div>
<div
x-ref="panel"
tabindex="-1"
x-show="show"
x-transition:enter="ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
class="relative w-full {{ $maxWidthClass }} rounded-2xl 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 px-6 pt-6">
<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="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>