click to show lil info panel

This commit is contained in:
2026-08-24 00:28:38 +08:00
parent 62993c977e
commit 842e119f1d
3 changed files with 244 additions and 15 deletions
+122
View File
@@ -0,0 +1,122 @@
<script lang="ts">
import { tick } from "svelte";
import { fly } from "svelte/transition";
import type { Snippet } from "svelte";
type Props = {
id: string;
label: string;
labelledBy: string;
open: boolean;
onOpenChange: (open: boolean, trigger: HTMLButtonElement) => void;
trigger: Snippet;
children: Snippet;
class?: string;
triggerClass?: string;
panelClass?: string;
panelStyle?: string;
};
let {
id,
label,
labelledBy,
open,
onOpenChange,
trigger,
children,
class: className = "",
triggerClass = "",
panelClass = "",
panelStyle,
}: Props = $props();
let root = $state<HTMLDivElement>();
let panel = $state<HTMLDivElement>();
let triggerButton = $state<HTMLButtonElement>();
$effect(() => {
if (open) {
tick().then(() => {
if (open) panel?.focus({ preventScroll: true });
});
}
});
function setOpen(nextOpen: boolean) {
if (triggerButton) onOpenChange(nextOpen, triggerButton);
}
function closeOnEscape(event: KeyboardEvent) {
if (open && event.key === "Escape") {
setOpen(false);
triggerButton?.focus({ preventScroll: true });
}
}
function closeOnOutsidePointer(event: PointerEvent) {
if (
open &&
root &&
event.target instanceof Node &&
!root.contains(event.target)
) {
setOpen(false);
}
}
function closeOnFocusOut(event: FocusEvent) {
if (
open &&
root &&
(!(event.relatedTarget instanceof Node) ||
!root.contains(event.relatedTarget))
) {
setOpen(false);
}
}
function closeOnWindowBlur() {
if (open) setOpen(false);
}
</script>
<svelte:window
onkeydown={closeOnEscape}
onpointerdown={closeOnOutsidePointer}
onblur={closeOnWindowBlur}
/>
<div
bind:this={root}
class={`relative grid place-items-center ${className}`}
onfocusout={closeOnFocusOut}
>
{#if open}
<div
bind:this={panel}
{id}
class={`absolute z-20 focus:outline-none ${panelClass}`}
style={panelStyle}
role="dialog"
aria-labelledby={labelledBy}
tabindex="-1"
>
<div transition:fly={{ y: 12, duration: 180 }}>
{@render children()}
</div>
</div>
{/if}
<button
bind:this={triggerButton}
type="button"
class={`grid cursor-pointer place-items-center border-0 bg-transparent p-0 transition-transform duration-150 hover:-translate-y-0.5 focus-visible:-translate-y-0.5 ${triggerClass}`}
aria-label={label}
aria-expanded={open}
aria-controls={id}
onclick={() => setOpen(!open)}
>
{@render trigger()}
</button>
</div>