@svelte-put/modal
Version:
509 lines • 17 kB
TypeScript
import type { ClickOutsideParameters } from '@svelte-put/clickoutside';
import type { MovableParameters } from '@svelte-put/movable';
import type { ComponentEvents, ComponentProps, ComponentType, createEventDispatcher, SvelteComponentTyped } from 'svelte';
import type { Writable } from 'svelte/store';
import type Modal from './Modal.svelte';
/**
* The trigger that resolves the modal
*
* - `backdrop`: non-static backdrop was clicked
*
* - `x`: `x` button was clicked
*
* - `escape`: `escape` key was pressed
*
* - `clickoutside`: click outside of modal was detected
*
* - `pop`: modal was resolved from by calling the `pop` method **manually** from the modal store
*
* - `custom`: modal was resolved by a custom dispatch. Use this in your
* custom modal when extending the `resolve` event with additional props.
* See {@link ExtendedModalEvents}
*
*
*/
export type ResolveTrigger = 'backdrop' | 'x' | 'escape' | 'clickoutside' | 'pop' | 'custom';
/**
* The base interface when modal is resolved
*
*/
export type ModalComponentBaseResolved<ExtendedResolved extends Record<string, any> = {}> = {
trigger: ResolveTrigger;
} & ExtendedResolved;
/**
* The base events for modal
*
*/
export type ModalComponentBaseEvents<Resolved extends ModalComponentBaseResolved = ModalComponentBaseResolved> = {
resolve: CustomEvent<Resolved>;
};
/**
* The resolved type for modal
* @example
*
* ```typescript
* import CustomModal from './CustomModal.svelte';
* import type { ModalResolved } from '@svelte-put/modal';
*
* type CustomModalResolved = ModalResolved<CustomModal>;
* ```
*/
export type ModalResolved<Component extends ModalComponentBase> = ComponentEvents<Component>['resolve']['detail'];
/**
* The base slots for modal
*
*/
export interface ModalComponentBaseSlots {
/** content of the modal */
default: Record<string, never>;
/** backdrop of the modal */
backdrop: {
/** the resolved class name, merged from the default and the `classes` prop */
class: string;
/** default click handler for backdrop (dismiss with (trigger) as `backdrop`) */
onClick: () => void;
};
/** modal container */
container: {
/** the resolved class name, merged from the default and the `classes` prop */
class: string;
};
/** `x` button */
x: {
/** the resolved class name, merged from the default and the `classes` prop */
class: string;
/** default click handler for x button (dismiss with (trigger) as `x`) */
onClick: () => void;
/** the forwarded `xBtn` prop, see {@link ModalComponentBaseProps} */
xBtn: boolean;
};
/** content of `x` button */
'x-content': Record<string, never>;
}
/**
* The base events for modal
*
*/
export interface ModalComponentBaseProps {
/**
* whether the modal is at topmost position (last pushed),
* this prop is handled by the `ModalPortal` component and
* you don't have to touch this
*/
topmost?: boolean;
/**
* how the backdrop should render or behave. Defaults to `true`
*
* - `false` - no backdrop
*
* - `true` - backdrop that when clicked on will dismiss modal
*
* - `static` - backdrop that does not dismiss modal
*/
backdrop?: 'static' | boolean;
/**
* whether to render the `x` button (for dismissing modal). Defaults to `true`
*
* if you are overriding the `x` slot, this prop is forwarded to the slot template.
* See {@link ModalComponentBaseSlots}
*/
xBtn?: boolean;
/**
* whether to dismiss modal when `esc` key is pressed. Defaults to `true`
*/
escape?: boolean;
/**
* whether to dismiss modal when clicking outside. Most useful when
* you want to not have a backdrop but still dismiss modal when clicking
* outside the modal. Defaults to `false`.
*
* Can be provided as boolean or the parameter object
*/
clickoutside?: boolean | ClickOutsideParameters;
/**
* whether to make the modal "movable" (drag around).
* Can be provided as boolean or the parameter object.
* Defaults to `false`.
*/
movable?: boolean | MovableParameters;
/**
* custom class names for the modal elements.
*
* - If property is provided as `string`, it'll be added to the default class name of that element.
*
* - If property is provided as `{ override: string }`, it'll override the default class name of that element.
*
*
*
* As Svelte style is component-scoped. You will need to use either a global
* styling system like Tailwind or the `:global()` selector (example below).
*
* When overriding slots, the classes provided here will be merged with the default
* class names and passed to the slot template. See {@link ModalComponentBaseSlots}
* @example
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import type { ExtendedModalProps } from '@svelte-put/modal';
*
* type $$Props = ExtendedModalProps;
* </script>
*
* <Modal
* classes={{
* backdrop: 'bg-gray-500 bg-opacity-50',
* container: { override: 'custom-modal-container' },
* x: 'a-global-class',
* }}
* {...$$props}
* on:resolve
* >
* <svelte:fragment slot="x" let:class={className} let:xBtn let:onClick>
* {#if xBtn}
* <button type="button" class={className} on:click={onClick}>
* some custom svg here perhaps
* </button>
* {/if}
* </svelte:fragment>
* </Modal>
*
* <style>
* :global(.custom-modal-container) {
* background-color: #fff;
* with: 80%;
* }
* </style>
* ```
*/
classes?: Partial<Record<Exclude<keyof ModalComponentBaseSlots, 'default' | 'x-content'>, string | {
override: string;
}>>;
/**
* Some accessibility attributes passed to the modal container element as discussed
* in {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role | MDN Accessibility - Dialog}
*
*
*
* Since the base `Modal` does not know beforehand its content (passed by slots),
* responsibility for accessibility is delegated to you.
*
* By default, the `role` attribute is set to `dialog` without any aria attribute
* @example
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import type { ExtendedModalProps } from '@svelte-put/modal';
*
* $$Props = ExtendedModalProps;
* $$Events = ExtendedModalEvents<{
* confirmed: boolean;
* }>
*
* // create type-safe dispatch function
* const dispatch = createModalEventDispatcher<$$Events>();
* function resolve() {
* dispatch('resolve', {
* trigger: 'custom',
* confirmed: true,
* });
* }
* </script
*
* <Modal
* {...$$props}
* accessibility={{
* role: 'dialog',
* labelledBy: 'confirmation-dialog-title',
* describedBy: 'confirmation-dialog-description',
* }}
* on:resolve
* >
* <h2 id="confirmation-dialog-title">Submission Confirmation</h2>
* <p id="confirmation-dialog-description">Are you sure you want to submit?</p>
* <button type="button" on:click={() => resolve(true)}>Yes</button>
* <button type="button" on:click={() => resolve(false)}>No</button>
* </Modal>
* ```
*/
accessibility?: {
role: 'dialog' | 'alertdialog';
/** id of the element for `aria-labelledby` */
labelledBy?: string;
/** id of the element for `aria-describedby` */
describedBy?: string;
};
/**
* svelte event dispatcher. Should only pass this prop if extending the events.
* See {@link ExtendedModalEvents} for more details an dexamples
*/
dispatch?: ReturnType<typeof createEventDispatcher>;
}
/**
* The base component for building modal. Modals extending this component needs to
* meet these specified constraints
*
*/
export type ModalComponentBase = SvelteComponentTyped<{}, ModalComponentBaseEvents<ModalComponentBaseResolved>, {}>;
/**
* Either the Svelte modal component or an option object that specifies how
* to push the modal onto the stack
*
*/
export type ModalPushInput<Component extends ModalComponentBase> = ComponentType<Component> | {
/**
* id to track this modal
* @default (crypto && crypto.randomUUID && crypto.randomUUID()) ?? `modal-indexed-$\{modals.length\}`
*/
id?: string;
/**
* the component to push
*/
component: ComponentType<Component>;
/**
* props passed to pushed component
*/
props?: ComponentProps<Component>;
};
/**
* The return of the `push` method of modal store
*
*/
export interface ModalPushOutput<Component extends ModalComponentBase, Resolved extends ModalComponentBaseResolved = ModalResolved<Component>> {
/**
* id of the pushed modal, pass to `pop` to dismiss modal
* @example
*
* ```typescript
* import { createModalStore } from '@svelte-put/modal';
* const store = createModalStore();
* const pushed = store.push({ component: SomeComponent, props: { someProp: 'value' } });
* store.pop(pushed);
* ```
*/
id: string;
/** component to render in modal layout */
component: ComponentType<Component>;
/** props passed to modal component */
props: ComponentProps<Component>;
/**
* wait for the modal to resolve, and return the promise wrapping the resolved value
* @example
*
* ```typescript
* import { createModalStore } from '@svelte-put/modal';
* const store = createModalStore();
* const pushed = store.push({ component: ConfirmationModal });
* // assuming ConfirmationModal resolves with { confirmed: boolean }
* const { confirmed } = await pushed.resolve();
* ```
*/
resolve: () => Promise<Resolved>;
/**
* whether the modal has been resolved
*/
resolved: boolean;
}
/**
* Utility type for building custom props type from the base Modal component
* @example
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import type { ExtendedModalProps } from '@svelte-put/modal';
*
* $$Props = ExtendedModalProps<{ anotherProp: string }>;
*
* export let anotherProp: string;
* </script>
*
* <Modal {...$$restProps} on:resolve>
* <p>{anotherProp}</p>
* </Modal>
* ```
*/
export type ExtendedModalProps<ExtendedProps extends Record<string, any> = {}> = ComponentProps<Modal> & ExtendedProps;
/**
* Utility type for building custom events type from the base Modal component.
* Use in conjunction with {@link createModalEventDispatcher}
*
*
*
*
* When extending events, we need to create a new `dispatch` function.
* Remember to pass this new `dispatch` as prop to the `Modal` component.
*
* When dispatching your extended `resolve` event, you will also need to pass
* the `trigger` prop to the event detail. Any {@link ResolveTrigger} is valid,
* but in this context `custom` is recommended (see example below).
*
* For simple no-action modal, just forward the resolve event.
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import type { ExtendedModalProps } from '@svelte-put/modal';
*
* $$Props = ExtendedModalProps;
* </script
*
* <Modal {...$$props} on:resolve />
* ```
* @example
*
* Custom the base `resolve` event.
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import { createModalEventDispatcher } from '@svelte-put/modal;
* import type { ExtendedModalEvents, ExtendedModalProps } from '@svelte-put/modal';
*
* $$Props = ExtendedModalProps;
* $$Events = ExtendedModalEvents<{
* confirmed: boolean;
* }>
*
* // create type-safe dispatch function
* const dispatch = createModalEventDispatcher<$$Events>();
*
* function resolve() {
* dispatch('resolve', {
* trigger: 'custom',
* confirmed: true,
* });
* }
* </script>
*
* <Modal {...$$props} {dispatch}>
* <button type="button" on:click={() => resolve(true)}>Confirm</button>
* <button type="button" on:click={() => resolve(false)}>Cancel</button>
* </Modal>
* ```
* @example
*
* Add other events.
*
* ```html
* <script lang="ts">
* import Modal from '@svelte-put/modal/Modal.svelte';
* import { createModalEventDispatcher } from '@svelte-put/modal;
* import type { ExtendedModalEvents, ExtendedModalProps } from '@svelte-put/modal';
*
* $$Props = ExtendedModalProps;
* $$Events = ExtendedModalEvents<{}, {
* anotherEvent: string;
* }>
*
* // create type-safe dispatch function
* const dispatch = createModalEventDispatcher<$$Events>();
*
* function handleClick() {
* dispatch('anotherEvent', 'detail');
* }
* </script>
*
* <Modal {...$$props} {dispatch}>
* <button type="button" on:click={handleClick}>Another Event</button>
* </Modal>
* ```
*/
export type ExtendedModalEvents<ExtendedResolved extends Record<string, any> = {}, ExtendedEvents extends Record<string, CustomEvent<any>> = {}> = {
resolve: CustomEvent<ModalComponentBaseResolved & Partial<ExtendedResolved>>;
} & ExtendedEvents;
/**
* callback as one passed to ModalStore `onPop`
*
*/
export type ModalResolveCallback<Component extends ModalComponentBase = ModalComponentBase> = (resolved: ModalResolved<Component>) => void;
/**
*
*Push a new modal to the stack
* @param input - {@link ModalPushInput}
* @returns the {@link ModalPushOutput}
*/
export type ModalStorePush = <Component extends ModalComponentBase>(input: ModalPushInput<Component>) => ModalPushOutput<Component>;
/**
*
*Pop the modal with given id.
*If `id` is not provided, pop the topmost modal
*
*
*
*When calling this manually (rather than being called from the `ModalPortal` component),
*the trigger is expected to be `pop`;
* @param pushed - the returned {@link ModalPushOutput} output from `push`
* @param resolved - custom resolved value, if any
* @returns the popped {@link ModalPushOutput} or `undefined` in the
* case no modal was found that matches the specified input
*/
export type ModalStorePop = <Pushed extends ModalPushOutput<Component, Resolved>, Component extends ModalComponentBase, Resolved extends ModalResolved<Component>>(pushed?: Pushed, resolved?: Resolved) => Pushed | undefined;
/**
*
*callback for when a modal is popped. Can be called multiple times
*to registered multiple callbacks
*
*
*
*This should be called before the modal is pushed.
*
*If the same callback is registered multiple times, it will only be called once
*(be aware that inline arrow function will be a different function each time)
*
*After the modal is popped, the callback list will be cleared. Meaning
*next time the same modal is pushed, callback must be registered again.
*
*See example for typescript support.
* @example
*
* ```typescript
* import CustomModal from './CustomModal.svelte';
* import { createModalStore } from '@svelte-put/modal';
* import type { ModalResolveCallback, ModalResolved } from '@svelte-put/modal';
*
* const store = createModalStore();
* const pushed = store.push(CustomModal);
*
* let unsubscribe = store.onPop<CustomModal>(pushed.id, (resolved) => {});
* unsubscribe(); // to unregister the callback
*
* // or
*
* function onPop(resolved: ModalResolved<CustomModal>) {};
* unsubscribe = store.onPop<CustomModal>(pushed.id, onPop);
* unsubscribe(); // to unregister the callback
*
* // or
*
* const sideEffect: ModalResolveCallback<CustomModal> = (resolved) => {};
* unsubscribe = store.onPop(pushed.id, sideEffect);
* unsubscribe(); // to unregister the callback
* ```
* @param modalId - the id returned from push operation
* @param callback - {@link ModalResolveCallback}
* @returns the unsubscribe function, when call will remove the callback
*/
export type ModalStoreOnPop = <Component extends ModalComponentBase = ModalComponentBase>(modalId: string, callback: ModalResolveCallback<Component>) => () => void;
/**
*
*/
export type ModalStoreValue = ModalPushOutput<ModalComponentBase, ModalComponentBaseResolved>[];
/**
*
*/
export type ModalStoreSubscribe = Writable<ModalStoreValue>['subscribe'];
/**
*
*/
export interface ModalStore {
subscribe: ModalStoreSubscribe;
/** see {@link ModalStorePush} */
push: ModalStorePush;
/** see {@link ModalStorePop} */
pop: ModalStorePop;
/** see {@link ModalStoreOnPop} */
onPop: ModalStoreOnPop;
}
//# sourceMappingURL=modal.types.d.ts.map