Files
coolify/resources/views/components/forms/listbox.blade.php
T
Andras Bacsai de4e74b4cd feat(server): fall back Sentinel URL to request host
Use the current request scheme/host when no FQDN or public IP is set,
skipping loopback hosts. Polish boarding/server create cards, optional
listbox tooltips, and cover the layout and Sentinel URL behavior in tests.
2026-08-07 15:41:39 +02:00

84 lines
4.0 KiB
PHP

@props([
'id' => null,
'label' => null,
'helper' => null,
'required' => false,
'options' => [], // list of ['value' => ..., 'label' => ..., 'disabled' => bool]
'placeholder' => 'Select…',
'emptyText' => 'No options available.',
'live' => false,
'onChange' => null, // optional $wire method to call after a selection
'wire' => true, // false = purely client-side value (no Livewire binding)
'value' => null, // initial value when wire=false
'disabled' => false,
'tooltip' => true,
])
<div class="w-full min-w-0">
@if ($label)
{{--
Keep helper outside the label so taps do not open the listbox trigger.
Margin lives only on this wrapper (mb-0 on the label) so spacing matches
plain text labels and is not doubled by .application-settings-form label rules.
--}}
{{-- Fixed h-4 matches the helper icon so side-by-side fields align with or without a helper. --}}
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
<label for="{{ $id }}-trigger" class="mb-0! flex items-center gap-1.5 leading-4">
{{ $label }}
@if ($required)
<x-highlighted text="*" />
@endif
</label>
@if ($helper)
<x-helper :helper="$helper" />
@endif
</div>
@endif
<div class="relative min-w-0" x-data="{
open: false,
options: @js(array_values($options)),
value: @if (!$wire) @js($value) @elseif ($live) @entangle($id).live @else @entangle($id) @endif,
get current() {
const found = this.options.find((option) => String(option.value) === String(this.value));
return found ? found.label : @js($placeholder);
},
choose(option) {
if (option.disabled) return;
this.open = false;
if (String(option.value) === String(this.value)) return;
this.value = option.value;
@if ($onChange) this.$nextTick(() => this.$wire.{{ $onChange }}()); @endif
}
}" x-modelable="value" {{ $attributes->whereStartsWith('x-model') }}
{{ $attributes->whereStartsWith('x-effect') }}
@click.outside="open = false" @keydown.escape="open = false">
<button id="{{ $id }}-trigger" type="button" class="listbox-trigger" @click="open = !open"
@disabled($disabled) {{ $attributes->whereStartsWith('x-bind:disabled') }} aria-haspopup="listbox"
:aria-expanded="open" @if ($tooltip) :title="current" @endif>
<span class="listbox-trigger-label" x-text="current"></span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="size-3.5 shrink-0 opacity-60">
<path stroke-linecap="round" stroke-linejoin="round" d="m8 9 4-4 4 4m0 6-4 4-4-4" />
</svg>
</button>
<div class="listbox-panel" x-show="open" x-cloak role="listbox">
<div x-show="options.length === 0"
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
{{ $emptyText }}
</div>
<template x-for="option in options" :key="String(option.value)">
<button type="button" class="listbox-option" role="option"
:class="{ 'listbox-option-disabled': option.disabled }"
:aria-selected="String(option.value) === String(value)" @click="choose(option)">
<span class="truncate" x-text="option.label"></span>
<svg x-show="String(option.value) === String(value)" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"
class="size-3.5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</button>
</template>
</div>
</div>
</div>