fix(api-tokens): float the permissions dropdown over the token modal

This commit is contained in:
Andras Bacsai
2026-10-30 15:16:40 +01:00
parent fb854a7cff
commit 34e872482d
2 changed files with 24 additions and 6 deletions
@@ -262,7 +262,7 @@
@open-api-token-create.window="modalOpen = true" @api-token-issued.window="modalOpen = false">
<x-slot:content><span></span></x-slot:content>
<form wire:submit="addNewToken" id="api-token-create-form" class="lg:w-2xl">
<form wire:submit="addNewToken" id="api-token-create-form" class="lg:w-3xl">
@include('livewire.security.partials.api-token-fields', [
'form' => 'form',
'expiryOptions' => $expirationList,
@@ -280,7 +280,7 @@
@open-api-token-edit.window="modalOpen = true" @api-token-updated.window="modalOpen = false">
<x-slot:content><span></span></x-slot:content>
<form x-on:submit.prevent="$wire.updateToken(editingToken.id)" id="api-token-edit-form" class="lg:w-2xl">
<form x-on:submit.prevent="$wire.updateToken(editingToken.id)" id="api-token-edit-form" class="lg:w-3xl">
<p class="mb-4 text-[12px] text-neutral-500 dark:text-fg-dim">
The token value does not change. Clients that use it get the new access on their next request.
</p>
@@ -14,8 +14,24 @@
<h4 class="text-[12px] font-semibold text-black dark:text-fg">Permissions</h4>
<x-helper helper="Only grant the abilities this token needs." />
</div>
{{-- A fixed panel escapes the scrolling modal body, so it is not cut off. --}}
<div class="relative" x-data="{
permissionsOpen: false,
panelPosition: {},
positionPanel() {
const gap = 4;
const edge = 12;
const triggerRect = this.$refs.permissionsTrigger.getBoundingClientRect();
const panelHeight = Math.min(this.$refs.permissionsPanel.scrollHeight, 256);
const fitsBelow = window.innerHeight - triggerRect.bottom - gap >= panelHeight;
this.panelPosition = {
top: `${fitsBelow ? triggerRect.bottom + gap : Math.max(edge, triggerRect.top - gap - panelHeight)}px`,
left: `${triggerRect.left}px`,
width: `${triggerRect.width}px`,
minWidth: `${triggerRect.width}px`,
maxWidth: `${triggerRect.width}px`,
};
},
get isRoot() {
return ($wire.{{ $form }}.permissions ?? []).includes('root');
},
@@ -24,15 +40,17 @@
.map((permission) => permission.split(':').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' '))
.join(', ');
},
}" @click.outside="permissionsOpen = false" @keydown.escape.window="permissionsOpen = false">
<button type="button" class="listbox-trigger" @click="permissionsOpen = !permissionsOpen"
}" @click.outside="permissionsOpen = false" @keydown.escape.window="permissionsOpen = false"
@resize.window="permissionsOpen && positionPanel()" @scroll.window.capture="permissionsOpen && positionPanel()">
<button type="button" x-ref="permissionsTrigger" class="listbox-trigger" @click="permissionsOpen = !permissionsOpen"
aria-haspopup="listbox" :aria-expanded="permissionsOpen">
<span class="truncate" x-text="'Selected permissions: ' + summary"></span>
<x-reicon name="chevron-down" class="size-3.5 shrink-0 opacity-60" />
</button>
<div x-cloak x-show="permissionsOpen" x-transition.origin.top
class="listbox-panel top-full! mt-1! w-full!" role="listbox">
<div x-cloak x-show="permissionsOpen" x-ref="permissionsPanel" x-transition.origin.top
x-effect="if (permissionsOpen) $nextTick(() => positionPanel())"
class="listbox-panel" style="position: fixed; z-index: 9999" :style="panelPosition" role="listbox">
<div class="listbox-option p-0!">
<x-forms.checkbox id="permission-root" label="Root" fullWidth
wire:model.live="{{ $form }}.permissions" domValue="root"