From b976d19f25e70d0ad8e3d6b24bf675ab214798e7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:19:04 +0200 Subject: [PATCH] fix(ui): improve dropdown positioning and empty backup states Refactor dropdown behavior, add sorted resource selection to breadcrumbs, and show an empty state when no backup executions exist. --- resources/css/app.css | 20 -- resources/views/components/dropdown.blade.php | 11 +- .../floating-dropdown-script.blade.php | 69 ----- .../views/components/forms/listbox.blade.php | 54 +++- .../views/components/table/dropdown.blade.php | 15 +- .../views/components/top-breadcrumb.blade.php | 79 ++++-- resources/views/layouts/base.blade.php | 1 - .../database/backup-executions.blade.php | 244 +++++++++--------- .../Feature/BreadcrumbSelectionStyleTest.php | 13 + tests/Feature/DatabaseBackupsLayoutTest.php | 7 + tests/Feature/EmptyStateComponentTest.php | 9 +- .../Feature/ListboxTriggerTruncationTest.php | 27 +- tests/Feature/StandardTableComponentsTest.php | 24 +- 13 files changed, 274 insertions(+), 299 deletions(-) delete mode 100644 resources/views/components/floating-dropdown-script.blade.php diff --git a/resources/css/app.css b/resources/css/app.css index 62f718b79..fcb97b814 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1939,27 +1939,7 @@ html[data-theme="custom"] textarea:disabled { box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); } -.floating-dropdown-panel { - min-width: 0; -} - -/* Older menus still render inside their page container. On mobile, keep - those panels fully visible until they are migrated to the portaled - dropdown component. Portaled panels carry their own fixed coordinates. */ @media (max-width: 767px) { - .listbox-panel:not([style*="position: fixed"]) { - position: fixed !important; - top: 50% !important; - right: auto !important; - left: 50% !important; - width: max-content; - max-width: calc(100vw - 1.5rem) !important; - max-height: calc(100dvh - 1.5rem) !important; - box-sizing: border-box; - transform: translate(-50%, -50%) !important; - z-index: 9999 !important; - } - /* The mobile top bar's backdrop filter makes it the containing block for fixed descendants. Keep this menu anchored below its trigger instead of centering it within the short top bar. */ diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php index d466f3907..b48b04143 100644 --- a/resources/views/components/dropdown.blade.php +++ b/resources/views/components/dropdown.blade.php @@ -25,10 +25,13 @@ const triggerRect = this.$refs.trigger.getBoundingClientRect(); const panelRect = this.$refs.panel.getBoundingClientRect(); const viewportPadding = 8; - const left = Math.max( - viewportPadding, - (window.innerWidth - panelRect.width) / 2, - ); + let left = triggerRect.left; + + if ((left + panelRect.width + viewportPadding) > window.innerWidth) { + left = window.innerWidth - panelRect.width - viewportPadding; + } + + left = Math.max(viewportPadding, left); let top = triggerRect.bottom + 4; const maxTop = window.innerHeight - panelRect.height - viewportPadding; diff --git a/resources/views/components/floating-dropdown-script.blade.php b/resources/views/components/floating-dropdown-script.blade.php deleted file mode 100644 index 81381c5cd..000000000 --- a/resources/views/components/floating-dropdown-script.blade.php +++ /dev/null @@ -1,69 +0,0 @@ - diff --git a/resources/views/components/forms/listbox.blade.php b/resources/views/components/forms/listbox.blade.php index 22e1865de..bfcb4437e 100644 --- a/resources/views/components/forms/listbox.blade.php +++ b/resources/views/components/forms/listbox.blade.php @@ -14,7 +14,7 @@ 'value' => null, // initial value when wire=false 'disabled' => false, 'tooltip' => true, - 'portal' => true, + 'portal' => false, 'preserveValue' => false, ]) @@ -43,13 +43,9 @@ @endif @endif -
requestAnimationFrame(() => this.positionPanel())); + } + }, + positionPanel(panel = null) { + const trigger = this.$refs.trigger; + panel ??= document.getElementById(@js($panelId)); + if (!trigger || !panel) return; + + const gap = 4; + const edge = 12; + const triggerRect = trigger.getBoundingClientRect(); + const panelWidth = Math.min( + Math.max(triggerRect.width, panel.offsetWidth), + window.innerWidth - (edge * 2), + ); + const panelHeight = Math.min(panel.scrollHeight, 256); + const fitsBelow = window.innerHeight - triggerRect.bottom - gap >= panelHeight; + const top = fitsBelow + ? triggerRect.bottom + gap + : Math.max(edge, triggerRect.top - gap - panelHeight); + const left = Math.min( + Math.max(edge, triggerRect.left), + window.innerWidth - panelWidth - edge, + ); + + panel.style.top = `${top}px`; + panel.style.left = `${left}px`; + panel.style.width = `${panelWidth}px`; + panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`; + panel.style.minWidth = `${triggerRect.width}px`; + this.positioned = true; + }, + }" x-modelable="value" :class="{ 'pointer-events-none opacity-70': saving }" {{ $attributes->whereStartsWith('x-model') }} {{ $attributes->whereStartsWith('x-effect') }} @if ($preserveValue) wire:ignore @endif - @click.outside="open = false" @keydown.escape="open = false" @resize.window="open && schedulePosition()"> + @click.outside="open = false" @keydown.escape="open = false" @resize.window="open && positionPanel()">