2026-07-15 16:30:17 +00:00
|
|
|
<script module lang="ts">
|
|
|
|
|
export type DialogSize = "default" | "large"
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-09-12 17:03:24 +00:00
|
|
|
<script lang="ts" generics="ChildrenProps extends Record<string, unknown>">
|
2026-01-30 22:14:12 +00:00
|
|
|
import type {Component} from "svelte"
|
2026-09-12 17:03:24 +00:00
|
|
|
import {onMount, setContext} from "svelte"
|
2025-11-03 22:43:27 +00:00
|
|
|
import cx from "classnames"
|
2026-09-12 17:03:24 +00:00
|
|
|
import {createFocusTrap} from "focus-trap"
|
|
|
|
|
import type {FocusTrap} from "focus-trap"
|
2024-10-17 23:03:50 +00:00
|
|
|
import {noop} from "@welshman/lib"
|
2024-10-15 22:52:30 +00:00
|
|
|
import {fade, fly} from "@lib/transition"
|
2026-03-13 00:21:01 +00:00
|
|
|
import Close from "@assets/icons/close.svg?dataurl"
|
|
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
|
|
|
|
import Button from "@lib/components/Button.svelte"
|
2026-09-12 17:03:24 +00:00
|
|
|
import {DIALOG_CONTEXT} from "@lib/components/dialog"
|
2024-10-15 22:52:30 +00:00
|
|
|
|
2026-01-30 22:14:12 +00:00
|
|
|
type Props = {
|
2026-09-12 17:03:24 +00:00
|
|
|
onClose?: () => void
|
|
|
|
|
active?: boolean
|
|
|
|
|
label?: string
|
|
|
|
|
restoreFocus?: boolean
|
2026-03-17 21:55:40 +00:00
|
|
|
noEscape?: boolean
|
2025-02-04 00:37:14 +00:00
|
|
|
fullscreen?: boolean
|
2026-07-15 16:30:17 +00:00
|
|
|
size?: DialogSize
|
2026-01-30 22:14:12 +00:00
|
|
|
children: {
|
2026-09-12 17:03:24 +00:00
|
|
|
component: Component<ChildrenProps>
|
|
|
|
|
props: ChildrenProps
|
2026-01-30 22:14:12 +00:00
|
|
|
}
|
2025-02-04 00:37:14 +00:00
|
|
|
}
|
2024-10-17 22:02:03 +00:00
|
|
|
|
2026-07-15 16:30:17 +00:00
|
|
|
const {
|
|
|
|
|
onClose = noop,
|
2026-09-12 17:03:24 +00:00
|
|
|
active = true,
|
|
|
|
|
label = undefined,
|
|
|
|
|
restoreFocus = true,
|
2026-07-15 16:30:17 +00:00
|
|
|
noEscape = false,
|
|
|
|
|
fullscreen = false,
|
|
|
|
|
size = "default",
|
|
|
|
|
children,
|
|
|
|
|
}: Props = $props()
|
2025-02-04 00:37:14 +00:00
|
|
|
|
2025-12-08 17:19:41 +00:00
|
|
|
const wrapperClass = $derived(
|
|
|
|
|
cx("absolute inset-0 flex sm:relative pointer-events-none", {
|
|
|
|
|
"items-center justify-center": fullscreen,
|
2026-07-15 16:30:17 +00:00
|
|
|
"items-end sm:items-center": !fullscreen,
|
|
|
|
|
"sm:w-[520px]": !fullscreen && size === "default",
|
|
|
|
|
"sm:w-[90%]": !fullscreen && size === "large",
|
2025-12-08 17:19:41 +00:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const innerClass = $derived(
|
2026-06-24 20:56:53 +00:00
|
|
|
cx("dialog relative text-content grow pointer-events-auto", "rounded-t-2xl sm:rounded-3xl", {
|
|
|
|
|
"bg-surface max-h-[90vh] flex flex-col max-w-full pb-sai sm:pb-0": !fullscreen,
|
|
|
|
|
}),
|
2025-02-04 00:37:14 +00:00
|
|
|
)
|
2026-04-01 18:33:56 +00:00
|
|
|
|
|
|
|
|
const buttonClass = $derived(
|
2026-06-24 20:56:53 +00:00
|
|
|
cx("absolute right-3 z-tooltip", {
|
2026-04-01 18:33:56 +00:00
|
|
|
"top-3": fullscreen,
|
2026-04-16 15:38:25 +00:00
|
|
|
"-top-4 mr-sai": !fullscreen,
|
2026-04-01 18:33:56 +00:00
|
|
|
}),
|
|
|
|
|
)
|
2026-09-12 17:03:24 +00:00
|
|
|
|
|
|
|
|
let titleId = $state<string | undefined>()
|
|
|
|
|
let contentLabel = $state<string | undefined>()
|
|
|
|
|
let element: HTMLElement
|
|
|
|
|
let panel: HTMLElement
|
|
|
|
|
let trap: FocusTrap | undefined
|
|
|
|
|
|
|
|
|
|
setContext(DIALOG_CONTEXT, {
|
|
|
|
|
registerLabel: (value: string) => {
|
|
|
|
|
contentLabel = value
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (contentLabel === value) {
|
|
|
|
|
contentLabel = undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
registerTitle: (id: string) => {
|
|
|
|
|
titleId = id
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (titleId === id) {
|
|
|
|
|
titleId = undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (trap) {
|
|
|
|
|
if (active) {
|
|
|
|
|
if (trap.active) {
|
|
|
|
|
trap.unpause()
|
|
|
|
|
} else {
|
|
|
|
|
trap.activate()
|
|
|
|
|
}
|
|
|
|
|
} else if (trap.active && !trap.paused) {
|
|
|
|
|
trap.pause()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
const autofocus = panel.querySelector<HTMLElement>("[autofocus]")
|
|
|
|
|
|
|
|
|
|
trap = createFocusTrap(element, {
|
|
|
|
|
allowOutsideClick: true,
|
|
|
|
|
escapeDeactivates: false,
|
|
|
|
|
fallbackFocus: panel,
|
|
|
|
|
...(autofocus ? {initialFocus: autofocus} : {}),
|
|
|
|
|
isolateSubtrees: false,
|
|
|
|
|
returnFocusOnDeactivate: false,
|
|
|
|
|
setReturnFocus: previous => (previous.isConnected ? previous : false),
|
|
|
|
|
tabbableOptions: {getShadowRoot: true},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (active) {
|
|
|
|
|
trap.activate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
trap?.deactivate({returnFocus: restoreFocus})
|
|
|
|
|
}
|
|
|
|
|
})
|
2024-10-15 22:52:30 +00:00
|
|
|
</script>
|
|
|
|
|
|
2026-09-12 17:03:24 +00:00
|
|
|
<div
|
|
|
|
|
bind:this={element}
|
|
|
|
|
class="dialog dialog-overlay flex justify-center items-center fixed inset-0 z-modal"
|
|
|
|
|
inert={!active}>
|
2024-10-15 22:52:30 +00:00
|
|
|
<button
|
2026-01-29 19:11:08 +00:00
|
|
|
type="button"
|
2026-09-12 17:03:24 +00:00
|
|
|
tabindex="-1"
|
|
|
|
|
aria-hidden="true"
|
2026-02-02 18:09:14 +00:00
|
|
|
class="absolute inset-0 cursor-pointer bg-black opacity-50 dark:opacity-75"
|
|
|
|
|
transition:fade={{duration: 200}}
|
2025-02-04 00:37:14 +00:00
|
|
|
onclick={onClose}>
|
2025-02-03 23:01:42 +00:00
|
|
|
</button>
|
2025-12-08 17:19:41 +00:00
|
|
|
<div class={wrapperClass}>
|
2026-06-24 20:56:53 +00:00
|
|
|
<div
|
2026-09-12 17:03:24 +00:00
|
|
|
bind:this={panel}
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby={titleId}
|
|
|
|
|
aria-label={titleId ? undefined : contentLabel || label}
|
|
|
|
|
tabindex="-1"
|
2026-06-24 20:56:53 +00:00
|
|
|
class={innerClass}
|
|
|
|
|
style={!fullscreen ? "box-shadow: var(--shadow-lg)" : undefined}
|
|
|
|
|
transition:fly>
|
2026-09-12 17:03:24 +00:00
|
|
|
<children.component {...children.props} />
|
2026-03-17 21:55:40 +00:00
|
|
|
{#if !noEscape}
|
2026-06-24 20:56:53 +00:00
|
|
|
<Button
|
2026-09-12 17:03:24 +00:00
|
|
|
aria-label="Close dialog"
|
2026-06-24 20:56:53 +00:00
|
|
|
class={cx("button button-neutral button-sm button-circle", buttonClass)}
|
|
|
|
|
onclick={onClose}>
|
2026-03-17 21:55:40 +00:00
|
|
|
<Icon icon={Close} size={6} />
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
2025-12-08 17:19:41 +00:00
|
|
|
</div>
|
2024-10-15 22:52:30 +00:00
|
|
|
</div>
|
2026-09-12 17:03:24 +00:00
|
|
|
<div class="tippy-target dialog-tippy-target"></div>
|
2024-10-15 22:52:30 +00:00
|
|
|
</div>
|