mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-03 20:20:29 +00:00
fix: emoji picker dropdown escapes dialog overflow clipping
Portal the dropdown into the nearest <dialog> element so it stays in the browser's top layer, and temporarily set overflow:visible on the dialog while the picker is open so it isn't clipped.
This commit is contained in:
@@ -10,13 +10,62 @@
|
||||
let { value = $bindable(), placeholder = '⚡', size = 'sm' }: Props = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let triggerEl = $state<HTMLButtonElement>();
|
||||
let dropdownX = $state(0);
|
||||
let dropdownY = $state(0);
|
||||
|
||||
/**
|
||||
* Portal into the nearest <dialog> (stays in top-layer, escapes overflow
|
||||
* children like .dialog-body), or document.body if not inside a dialog.
|
||||
*/
|
||||
function portal(node: HTMLElement) {
|
||||
const dialog = triggerEl?.closest('dialog');
|
||||
const target = dialog || document.body;
|
||||
target.appendChild(node);
|
||||
|
||||
// <dialog> has browser-default overflow:auto which clips children.
|
||||
// Temporarily override while the dropdown is mounted.
|
||||
if (dialog) {
|
||||
dialog.style.overflow = 'visible';
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.remove();
|
||||
if (dialog) {
|
||||
dialog.style.overflow = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleWindowClick(e: MouseEvent) {
|
||||
if (open && !(e.target as HTMLElement)?.closest('.emoji-picker-button')) {
|
||||
open = false;
|
||||
if (open) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target?.closest('.emoji-picker-button') && !target?.closest('.epb-dropdown')) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOpen() {
|
||||
if (!open && triggerEl) {
|
||||
const triggerRect = triggerEl.getBoundingClientRect();
|
||||
const dialog = triggerEl.closest('dialog');
|
||||
if (dialog) {
|
||||
// dialog with transform creates a containing block —
|
||||
// position: absolute is relative to it, so offset accordingly
|
||||
const dialogRect = dialog.getBoundingClientRect();
|
||||
dropdownX = triggerRect.left - dialogRect.left;
|
||||
dropdownY = triggerRect.bottom - dialogRect.top + 4;
|
||||
} else {
|
||||
dropdownX = triggerRect.left;
|
||||
dropdownY = triggerRect.bottom + 4;
|
||||
}
|
||||
}
|
||||
open = !open;
|
||||
}
|
||||
|
||||
function handleSelect(emoji: string) {
|
||||
value = emoji;
|
||||
open = false;
|
||||
@@ -27,17 +76,18 @@
|
||||
|
||||
<div class="emoji-picker-button" class:size-md={size === 'md'}>
|
||||
<button
|
||||
bind:this={triggerEl}
|
||||
type="button"
|
||||
class="trigger"
|
||||
class:open
|
||||
class:empty={!value}
|
||||
onclick={() => (open = !open)}
|
||||
onclick={toggleOpen}
|
||||
>
|
||||
{value || placeholder}
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div class="dropdown">
|
||||
<div class="epb-dropdown" use:portal style="position:absolute; z-index:99999; left:{dropdownX}px; top:{dropdownY}px;">
|
||||
<EmojiPicker selected={value} onselect={handleSelect} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -80,11 +130,4 @@
|
||||
.trigger.empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user