218 lines
7.9 KiB
Svelte
218 lines
7.9 KiB
Svelte
<script context="module">import { fly, fade } from "svelte/transition";
|
|
import { prefersReducedMotionStore } from "../../index.js";
|
|
import { dynamicTransition } from "../../internal/transitions.js";
|
|
</script>
|
|
|
|
<script generics="TransitionIn extends Transition = FlyTransition, TransitionOut extends Transition = FlyTransition">import { createEventDispatcher } from "svelte";
|
|
const dispatch = createEventDispatcher();
|
|
import { focusTrap } from "../../actions/FocusTrap/focusTrap.js";
|
|
import { getModalStore } from "./stores.js";
|
|
export let position = "items-center";
|
|
export let components = {};
|
|
export let background = "bg-surface-100-800-token";
|
|
export let width = "w-modal";
|
|
export let height = "h-auto";
|
|
export let padding = "p-4";
|
|
export let spacing = "space-y-4";
|
|
export let rounded = "rounded-container-token";
|
|
export let shadow = "shadow-xl";
|
|
export let zIndex = "z-[999]";
|
|
export let buttonNeutral = "variant-ghost-surface";
|
|
export let buttonPositive = "variant-filled";
|
|
export let buttonTextCancel = "Cancel";
|
|
export let buttonTextConfirm = "Confirm";
|
|
export let buttonTextSubmit = "Submit";
|
|
export let regionBackdrop = "bg-surface-backdrop-token";
|
|
export let regionHeader = "text-2xl font-bold";
|
|
export let regionBody = "max-h-[200px] overflow-hidden";
|
|
export let regionFooter = "flex justify-end space-x-2";
|
|
export let transitions = !$prefersReducedMotionStore;
|
|
export let transitionIn = fly;
|
|
export let transitionInParams = { duration: 150, opacity: 0, x: 0, y: 100 };
|
|
export let transitionOut = fly;
|
|
export let transitionOutParams = { duration: 150, opacity: 0, x: 0, y: 100 };
|
|
const cBackdrop = "fixed top-0 left-0 right-0 bottom-0 overflow-y-auto";
|
|
const cTransitionLayer = "w-full h-fit min-h-full p-4 overflow-y-auto flex justify-center";
|
|
const cModal = "block overflow-y-auto";
|
|
const cModalImage = "w-full h-auto";
|
|
let promptValue;
|
|
const buttonTextDefaults = {
|
|
buttonTextCancel,
|
|
buttonTextConfirm,
|
|
buttonTextSubmit
|
|
};
|
|
let currentComponent;
|
|
let registeredInteractionWithBackdrop = false;
|
|
const modalStore = getModalStore();
|
|
modalStore.subscribe((modals) => {
|
|
if (!modals.length)
|
|
return;
|
|
if (modals[0].type === "prompt")
|
|
promptValue = modals[0].value;
|
|
buttonTextCancel = modals[0].buttonTextCancel || buttonTextDefaults.buttonTextCancel;
|
|
buttonTextConfirm = modals[0].buttonTextConfirm || buttonTextDefaults.buttonTextConfirm;
|
|
buttonTextSubmit = modals[0].buttonTextSubmit || buttonTextDefaults.buttonTextSubmit;
|
|
currentComponent = typeof modals[0].component === "string" ? components[modals[0].component] : modals[0].component;
|
|
});
|
|
function onBackdropInteractionBegin(event) {
|
|
if (!(event.target instanceof Element))
|
|
return;
|
|
const classList = event.target.classList;
|
|
if (classList.contains("modal-backdrop") || classList.contains("modal-transition")) {
|
|
registeredInteractionWithBackdrop = true;
|
|
}
|
|
}
|
|
function onBackdropInteractionEnd(event) {
|
|
if (!(event.target instanceof Element))
|
|
return;
|
|
const classList = event.target.classList;
|
|
if ((classList.contains("modal-backdrop") || classList.contains("modal-transition")) && registeredInteractionWithBackdrop) {
|
|
if ($modalStore[0].response)
|
|
$modalStore[0].response(void 0);
|
|
modalStore.close();
|
|
dispatch("backdrop", event);
|
|
}
|
|
registeredInteractionWithBackdrop = false;
|
|
}
|
|
function onClose() {
|
|
if ($modalStore[0].response)
|
|
$modalStore[0].response(false);
|
|
modalStore.close();
|
|
}
|
|
function onConfirm() {
|
|
if ($modalStore[0].response)
|
|
$modalStore[0].response(true);
|
|
modalStore.close();
|
|
}
|
|
function onPromptSubmit(event) {
|
|
event.preventDefault();
|
|
if ($modalStore[0].response)
|
|
$modalStore[0].response(promptValue);
|
|
modalStore.close();
|
|
}
|
|
function onKeyDown(event) {
|
|
if (!$modalStore.length)
|
|
return;
|
|
if (event.code === "Escape")
|
|
onClose();
|
|
}
|
|
$:
|
|
cPosition = $modalStore[0]?.position ?? position;
|
|
$:
|
|
classesBackdrop = `${cBackdrop} ${regionBackdrop} ${zIndex} ${$$props.class ?? ""} ${$modalStore[0]?.backdropClasses ?? ""}`;
|
|
$:
|
|
classesTransitionLayer = `${cTransitionLayer} ${cPosition ?? ""}`;
|
|
$:
|
|
classesModal = `${cModal} ${background} ${width} ${height} ${padding} ${spacing} ${rounded} ${shadow} ${$modalStore[0]?.modalClasses ?? ""}`;
|
|
$:
|
|
parent = {
|
|
position,
|
|
// ---
|
|
background,
|
|
width,
|
|
height,
|
|
padding,
|
|
spacing,
|
|
rounded,
|
|
shadow,
|
|
// ---
|
|
buttonNeutral,
|
|
buttonPositive,
|
|
buttonTextCancel,
|
|
buttonTextConfirm,
|
|
buttonTextSubmit,
|
|
// ---
|
|
regionBackdrop,
|
|
regionHeader,
|
|
regionBody,
|
|
regionFooter,
|
|
// ---
|
|
onClose
|
|
};
|
|
</script>
|
|
|
|
<svelte:window on:keydown={onKeyDown} />
|
|
|
|
{#if $modalStore.length > 0}
|
|
{#key $modalStore}
|
|
<!-- Backdrop -->
|
|
<!-- FIXME: resolve a11y warnings -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<div
|
|
class="modal-backdrop {classesBackdrop}"
|
|
data-testid="modal-backdrop"
|
|
on:mousedown={onBackdropInteractionBegin}
|
|
on:mouseup={onBackdropInteractionEnd}
|
|
on:touchstart|passive
|
|
on:touchend|passive
|
|
transition:dynamicTransition|global={{ transition: fade, params: { duration: 150 }, enabled: transitions }}
|
|
use:focusTrap={true}
|
|
>
|
|
<!-- Transition Layer -->
|
|
<div
|
|
class="modal-transition {classesTransitionLayer}"
|
|
in:dynamicTransition|global={{ transition: transitionIn, params: transitionInParams, enabled: transitions }}
|
|
out:dynamicTransition|global={{ transition: transitionOut, params: transitionOutParams, enabled: transitions }}
|
|
>
|
|
{#if $modalStore[0].type !== 'component'}
|
|
<!-- Modal: Presets -->
|
|
<div class="modal {classesModal}" data-testid="modal" role="dialog" aria-modal="true" aria-label={$modalStore[0].title ?? ''}>
|
|
<!-- Header -->
|
|
{#if $modalStore[0]?.title}
|
|
<header class="modal-header {regionHeader}">{@html $modalStore[0].title}</header>
|
|
{/if}
|
|
<!-- Body -->
|
|
{#if $modalStore[0]?.body}
|
|
<article class="modal-body {regionBody}">{@html $modalStore[0].body}</article>
|
|
{/if}
|
|
<!-- Image -->
|
|
{#if $modalStore[0]?.image && typeof $modalStore[0]?.image === 'string'}
|
|
<img class="modal-image {cModalImage}" src={$modalStore[0]?.image} alt="Modal" />
|
|
{/if}
|
|
<!-- Type -->
|
|
{#if $modalStore[0].type === 'alert'}
|
|
<!-- Template: Alert -->
|
|
<footer class="modal-footer {regionFooter}">
|
|
<button type="button" class="btn {buttonNeutral}" on:click={onClose}>{buttonTextCancel}</button>
|
|
</footer>
|
|
{:else if $modalStore[0].type === 'confirm'}
|
|
<!-- Template: Confirm -->
|
|
<footer class="modal-footer {regionFooter}">
|
|
<button type="button" class="btn {buttonNeutral}" on:click={onClose}>{buttonTextCancel}</button>
|
|
<button type="button" class="btn {buttonPositive}" on:click={onConfirm}>{buttonTextConfirm}</button>
|
|
</footer>
|
|
{:else if $modalStore[0].type === 'prompt'}
|
|
<!-- Template: Prompt -->
|
|
<form class="space-y-4" on:submit={onPromptSubmit}>
|
|
<input class="modal-prompt-input input" name="prompt" type="text" bind:value={promptValue} {...$modalStore[0].valueAttr} />
|
|
<footer class="modal-footer {regionFooter}">
|
|
<button type="button" class="btn {buttonNeutral}" on:click={onClose}>{buttonTextCancel}</button>
|
|
<button type="submit" class="btn {buttonPositive}">{buttonTextSubmit}</button>
|
|
</footer>
|
|
</form>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<!-- Modal: Components -->
|
|
<!-- Note: keep `contents` class to allow widths from children -->
|
|
<div
|
|
class="modal contents {$modalStore[0]?.modalClasses ?? ''}"
|
|
data-testid="modal-component"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={$modalStore[0].title ?? ''}
|
|
>
|
|
{#if currentComponent?.slot}
|
|
<svelte:component this={currentComponent?.ref} {...currentComponent?.props} {parent}>
|
|
{@html currentComponent?.slot}
|
|
</svelte:component>
|
|
{:else}
|
|
<svelte:component this={currentComponent?.ref} {...currentComponent?.props} {parent} />
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/key}
|
|
{/if}
|