From 040490b0b534f88b27be61d30f8ce8a56456e5a3 Mon Sep 17 00:00:00 2001 From: Thomas Stenberg Oddsund Date: Tue, 2 Aug 2022 14:04:46 +0200 Subject: [PATCH 1/2] Added API for using slots for modal content and closebutton --- README.md | 155 +++++++++++++++++++++++++++++----------- package.json | 2 +- src/Modal.svelte | 59 ++++++++++----- types/Modal.svelte.d.ts | 21 +++++- 4 files changed, 172 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 5c077eb..a96ca2e 100644 --- a/README.md +++ b/README.md @@ -26,35 +26,42 @@ -**Live demo:** https://svelte.dev/repl/033e824fad0a4e34907666e7196caec4?version=3.20.1 +**Live demo:** [https://svelte.dev/repl/136d296195b543c4a4a6de127ff9e06e?version=3.35.0](https://svelte.dev/repl/136d296195b543c4a4a6de127ff9e06e?version=3.35.0) -**Works with:** Svelte `>=v3.4` (Tested until to `v3.20`) +**Works with:** Svelte `>=v3.35.0` ## Table of Contents +- [Table of Contents](#table-of-contents) - [Install](#install) - [Rollup Setup](#rollup-setup) - [Sapper Setup](#sapper-setup) - [Usage](#usage) - - [Svelte Store Example](#usage-with-a-svelte-store) + - [Usage with context](#usage-with-context) + - [Usage With a Svelte Store](#usage-with-a-svelte-store) + - [Usage with slots](#usage-with-slots) - [Styling](#styling) - - [SSR](#server-side-rendering) + - [Server-Side Rendering](#server-side-rendering) - [Accessibility](#accessibility) - [API](#api) - - [Component](#component-api) - - [Events](#component-events) + - [Component API](#component-api) + - [Component Events](#component-events) - [Context API](#context-api) - [Store API](#store-api) + - [Slot api](#slot-api) + - [Bind Props to a Component Shown as a Modal](#bind-props-to-a-component-shown-as-a-modal) - [FAQ](#faq) + - [Custom Close Button](#custom-close-button) +- [License](#license) ## Install +### Rollup Setup + ```bash npm install --save svelte-simple-modal ``` -#### Rollup Setup - Make sure that the main application's version of `svelte` is used for bundling by setting `rollup-plugin-node-resolve`'s `dedupe` option as follows: ```js @@ -69,7 +76,7 @@ export default { }; ``` -#### Sapper Setup +### Sapper Setup Make sure you install _svelte-simple-modal_ as a [dev-dependency](https://github.com/sveltejs/sapper-template#using-external-components). @@ -79,9 +86,13 @@ npm install -D svelte-simple-modal ## Usage +There are three ways to use the modal; with functions retrieved from context, setting modal content in a store or by using slots. + +### Usage with context + Import the `Modal` component into your main app component (e.g., `App.svelte`). -The modal is exposing [two context functions](#context-api): +The modal expose [two context functions](#context-api): - [`open()`](#open) opens a component as a modal. - [`close()`](#close) simply closes the modal. @@ -115,7 +126,7 @@ The modal is exposing [two context functions](#context-api):

🎉 {message} 🍾

``` -**Demo:** https://svelte.dev/repl/52e0ade6d42546d8892faf8528c70d30 +**Demo:** ### Usage With a Svelte Store @@ -136,12 +147,36 @@ Alternatively, you can use a [Svelte store](#store-api) to show/hide a component ``` -**Demo:** https://svelte.dev/repl/aec0c7d9f5084e7daa64f6d0c8ef0209 +**Demo:** The `` component is the same as in the example above. To hide the modal programmatically, simply unset the store. E.g., `modal.set(null)`. +### Usage with slots + +You can also use slots for both modal content and close buttons. Content not marked with a slot will be placed in the default slot and shown where the modal is placed. + +```svelte + + + + + + + +``` + +**Demo:** + +The `` component is the same as in the examples above. + +To hide the modal programmatically, simply set the show variable to a falsy value. + ### Styling The modal comes pre-styled for convenience but you can easily extent or replace the styling using either custom CSS classes or explicit CSS styles. @@ -162,7 +197,7 @@ Custom CSS classes can be applied via the `classBg`, `classWindow`, `classWindow ``` -**Demo:** https://svelte.dev/repl/f2a988ddbd5644f18d7cd4a4a8277993 +**Demo:** > Note: to take full control over the modal styles with CSS classes you have to reset existing styles via `unstyled={true}` as internal CSS classes are always applied last due to Svelte's class scoping. @@ -178,7 +213,7 @@ Alternatively, you can also apply CSS styles directly via the `styleBg`, `styleW ``` -**Demo:** https://svelte.dev/repl/50df1c694b3243c1bd524b27f86eec51 +**Demo:** ### Server-Side Rendering @@ -228,33 +263,34 @@ The `ariaLabel` is automatically omitted when `ariaLabelledBy` is specified. The `` component accepts the following optional properties: -| Property | Type | Default | Description | -| ------------------------- | -------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **show** | Component \| null | `null` | A Svelte component to show as the modal. See [Store API](#store-api) for details. | -| **ariaLabel** | string \| null | `null` | Accessibility label of the modal. See [W3C WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#aria-label) for details. | -| **ariaLabelledBy** | string \| null | `null` | Element ID holding the accessibility label of the modal. See [W3C WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby) for details. | -| **closeButton** | Component \| boolean | `true` | If `true` a button for closing the modal is rendered. You can also pass in a [custom Svelte component](#custom-close-button) to have full control over the styling. | -| **closeOnEsc** | boolean | `true` | If `true` the modal will close when pressing the escape key. | -| **closeOnOuterClick** | boolean | `true` | If `true` the modal will close when clicking outside the modal window. | -| **transitionBg** | function | `svelte.fade` | Transition function for the background. | -| **transitionBgProps** | BlurParams | `{}` | Properties of the transition function for the background. | -| **transitionWindow** | function | `svelte.fade` | Transition function for the window. | -| **transitionWindowProps** | BlurParams | `{}` | Properties of the transition function for the window. | -| **classBg** | string \| null | `null` | Class name for the background element. | -| **classWindowWrap** | string \| null | `null` | Class name for the modal window wrapper element. | -| **classWindow** | string \| null | `null` | Class name for the modal window element. | -| **classContent** | string \| null | `null` | Class name for the modal content element. | -| **classCloseButton** | string \| null | `null` | Class name for the built-in close button. | -| **styleBg** | Record | `{}` | Style properties of the background. | -| **styleWindowWrap** | Record | `{}` | Style properties of the modal window wrapper element. | -| **styleWindow** | Record | `{}` | Style properties of the modal window. | -| **styleContent** | Record | `{}` | Style properties of the modal content. | -| **styleCloseButton** | Record | `{}` | Style properties of the built-in close button. | -| **unstyled** | boolean | `false` | When `true`, the default styles are not applied to the modal elements. | -| **disableFocusTrap** | boolean | `false` | If `true` elements outside the modal can be in focus. This can be useful in certain edge cases. | -| **isTabbable** | (node: Element): boolean | internal function | A function to determine if an HTML element is tabbable. | -| **key** | string | `"simple-modal"` | The context key that is used to expose `open()` and `close()`. Adjust to avoid clashes with other contexts. | -| **setContext** | function | `svelte.setContent` | You can normally ingore this property when you have [configured Rollup properly](#rollup-setup). If you want to bundle simple-modal with its own version of Svelte you have to pass `setContext()` from your main app to simple-modal using this parameter. | +| Property | Type | Default | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **show** | Component \| null | `null` | A Svelte component to show as the modal. See [Store API](#store-api) for details. | +| **ariaLabel** | string \| null | `null` | Accessibility label of the modal. See [W3C WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#aria-label) for details. | +| **ariaLabelledBy** | string \| null | `null` | Element ID holding the accessibility label of the modal. See [W3C WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby) for details. | +| **closeButton** | Component \| boolean | `true` | If `true` a button for closing the modal is rendered. You can also pass in a [custom Svelte component](#custom-close-button) to have full control over the styling. | +| **closeOnEsc** | boolean | `true` | If `true` the modal will close when pressing the escape key. | +| **closeOnOuterClick** | boolean | `true` | If `true` the modal will close when clicking outside the modal window. | +| **transitionBg** | function | `svelte.fade` | Transition function for the background. | +| **transitionBgProps** | BlurParams | `{}` | Properties of the transition function for the background. | +| **transitionWindow** | function | `svelte.fade` | Transition function for the window. | +| **transitionWindowProps** | BlurParams | `{}` | Properties of the transition function for the window. | +| **classBg** | string \| null | `null` | Class name for the background element. | +| **classWindowWrap** | string \| null | `null` | Class name for the modal window wrapper element. | +| **classWindow** | string \| null | `null` | Class name for the modal window element. | +| **classContent** | string \| null | `null` | Class name for the modal content element. | +| **classCloseButton** | string \| null | `null` | Class name for the built-in close button. | +| **styleBg** | Record | `{}` | Style properties of the background. | +| **styleWindowWrap** | Record | `{}` | Style properties of the modal window wrapper element. | +| **styleWindow** | Record | `{}` | Style properties of the modal window. | +| **styleContent** | Record | `{}` | Style properties of the modal content. | +| **styleCloseButton** | Record | `{}` | Style properties of the built-in close button. | +| **unstyled** | boolean | `false` | When `true`, the default styles are not applied to the modal elements. | +| **disableFocusTrap** | boolean | `false` | If `true` elements outside the modal can be in focus. This can be useful in certain edge cases. | +| **isTabbable** | (node: Element): boolean | internal function | A function to determine if an HTML element is tabbable. | +| **key** | string | `"simple-modal"` | The context key that is used to expose `open()` and `close()`. Adjust to avoid clashes with other contexts. | +| **setContext** | function | `svelte.setContent` | You can normally ingore this property when you have [configured Rollup properly](#rollup-setup). If you want to bundle simple-modal with its own version of Svelte you have to pass `setContext()` from your main app to simple-modal using this parameter. | +| **callbacks** | {onOpen: (event: CustomEvent) => void, onOpened: (event: CustomEvent) => void, onClose: (event: CustomEvent) => void, onClosed: (event: CustomEvent) => void } \| null | null | An object with callbacks to be called on opening and closing transitions. Can be used in addition to the callbacks passed to bind, or alone. | ### Component Events @@ -345,10 +381,43 @@ You can also use [Svelte stores](https://svelte.dev/tutorial/writable-stores) to

🎉 Hi 🍾

``` -**Demo:** https://svelte.dev/repl/6f55b643195646408e780ceeb779ac2a +**Demo:** An added benefit of using stores is that the component opening the modal does not have to be a parent of ``. For instance, in the example above, `App.svelte` is toggling `Popup.svelte` as a modal even though `App.svelte` is not a child of ``. +### Slot api + +You can use slots to place the modal content and close button. Content which are not marked with a slot will be placed in the default slot, and shown where the modal is placed. To summarize; + +| Slot name | Example | Placement | +| ---------------- | --------------------- | ---------------------------------------------------------------- | +| modalContent | `slot="modalContent"` | Content of the modal | +| closeButton | `slot="closeButton"` | Close button of the modal | +| modalContent | `slot="example"` | Not shown | +| no slot provided | empty | Where the modal component is placed in the DOM, without wrapping | + +```svelte + + + + + + + + + +

🎉 Hi 🍾

+``` + +**Demo:** + +The benefit of using slots is that you can still send in props as normal, meaning that the component props will be reactive as before. +It's also easy to hide the modal programatically when some condition are met, e.g. the list triggering the modal is suddenly empty, as you only have to unset the show variable. + #### Bind Props to a Component Shown as a Modal Sometimes you want to pass properties to the component shown as a modal. To accomplish this, you can use our `bind()` helper function as follows: @@ -367,7 +436,7 @@ If you've worked with React/JSX then think of `const c = bind(Component, props)` ## FAQ -#### Custom Close Button +### Custom Close Button **This feature requires Svelte >=v3.19!** diff --git a/package.json b/package.json index ac7680e..838a52b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "types": "types/index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 0", - "build": "rm -rf lib && rm -rf types && rollup -c && sed -i '/from \"\"/d' types/index.d.ts", + "build": "rm -rf lib && rm -rf types && rollup -c && sed -i '' '/from \"\"/d' types/index.d.ts", "precommit": "NODE_ENV=production lint-staged; npm run lint", "prepare": "npm run lint && npm run build", "prerelease": "npm run lint; rm -f dist.zip; npm run build; zip dist.zip lib/* src/* types/*", diff --git a/src/Modal.svelte b/src/Modal.svelte index 256b4cb..8997ba2 100644 --- a/src/Modal.svelte +++ b/src/Modal.svelte @@ -196,6 +196,12 @@ */ export let disableFocusTrap = false; + /** + * Object with callbacks to be called on transition change + * @type { { onOpen?: (event: CustomEvent) => void, onOpened?: (event: CustomEvent) => void, onClose?: (event: CustomEvent) => void, onClosed?: (event: CustomEvent) => void } | null } + */ + export let callbacks = null; + const defaultState = { ariaLabel, ariaLabelledBy, @@ -253,6 +259,8 @@ const isFunction = (f) => !!(f && f.constructor && f.call && f.apply); + const displaying = () => Component || ($$slots.modalContent && show); + const updateStyleTransition = () => { cssBg = toCssString( Object.assign( @@ -279,12 +287,13 @@ let onClosed = toVoid; const open = (NewComponent, newProps = {}, options = {}, callback = {}) => { - Component = bind(NewComponent, newProps); + Component = isFunction(NewComponent) && bind(NewComponent, newProps); state = { ...defaultState, ...options }; updateStyleTransition(); disableScroll(); onOpen = (event) => { if (callback.onOpen) callback.onOpen(event); + if (callbacks && callbacks.onOpen) callbacks.onOpen(event); /** * The open event is fired right before the modal opens * @event {void} open @@ -299,6 +308,7 @@ }; onClose = (event) => { if (callback.onClose) callback.onClose(event); + if (callbacks && callbacks.onClose) callbacks.onClose(event); /** * The close event is fired right before the modal closes * @event {void} close @@ -313,6 +323,7 @@ }; onOpened = (event) => { if (callback.onOpened) callback.onOpened(event); + if (callbacks && callbacks.onOpened) callbacks.onOpened(event); /** * The opened event is fired after the modal's opening transition * @event {void} opened @@ -321,6 +332,7 @@ }; onClosed = (event) => { if (callback.onClosed) callback.onClosed(event); + if (callbacks && callbacks.onClosed) callbacks.onClosed(event); /** * The closed event is fired after the modal's closing transition * @event {void} closed @@ -330,7 +342,10 @@ }; const close = (callback = {}) => { - if (!Component) return; + if (!displaying()) return; + if ($$slots.modalContent) { + show = null; + } onClose = callback.onClose || onClose; onClosed = callback.onClosed || onClosed; Component = null; @@ -338,12 +353,12 @@ }; const handleKeydown = (event) => { - if (state.closeOnEsc && Component && event.key === 'Escape') { + if (state.closeOnEsc && displaying() && event.key === 'Escape') { event.preventDefault(); close(); } - if (Component && event.key === 'Tab' && !state.disableFocusTrap) { + if (displaying() && event.key === 'Tab' && !state.disableFocusTrap) { // trap focus const nodes = modalWindow.querySelectorAll('*'); const tabbable = Array.from(nodes) @@ -401,7 +416,9 @@ $: { if (isMounted) { - if (isFunction(show)) { + if ($$slots.modalContent && show) { + open(); + } else if (isFunction(show)) { open(show); } else { close(); @@ -420,7 +437,7 @@ -{#if Component} +{#if Component || ($$slots.modalContent && show)}
{#if state.closeButton} - {#if isFunction(state.closeButton)} - - {:else} -
diff --git a/types/Modal.svelte.d.ts b/types/Modal.svelte.d.ts index c579edf..6d89a44 100644 --- a/types/Modal.svelte.d.ts +++ b/types/Modal.svelte.d.ts @@ -1,5 +1,5 @@ /// -import type { SvelteComponentTyped } from "svelte"; +import type { SvelteComponentTyped } from 'svelte'; /** * Create a Svelte component with props bound to it. @@ -10,6 +10,12 @@ export declare function bind( ): Component; export interface ModalProps { + /** + * A function to determine if an HTML element is tabbable + * @default undefined + */ + isTabbable?: (node: Element) => boolean; + /** * Svelte component to be shown as the modal * @default null @@ -156,6 +162,17 @@ export interface ModalProps { * @default false */ disableFocusTrap?: boolean; + + /** + * Object with callbacks to be called on transition change + * @default null + */ + callbacks?: { + onOpen?: (event: CustomEvent) => void; + onOpened?: (event: CustomEvent) => void; + onClose?: (event: CustomEvent) => void; + onClosed?: (event: CustomEvent) => void; + } | null; } export default class Modal extends SvelteComponentTyped< @@ -168,5 +185,5 @@ export default class Modal extends SvelteComponentTyped< opened: CustomEvent; closed: CustomEvent; }, - { default: {} } + { default: {}; closeButton: {}; modalContent: {} } > {} From a60cea812fec4a70f51c42b13b7e7d193b52ef62 Mon Sep 17 00:00:00 2001 From: Thomas Stenberg Oddsund Date: Wed, 3 Aug 2022 22:21:55 +0200 Subject: [PATCH 2/2] Update svelte version in package.json --- package-lock.json | 4 ++-- package.json | 4 ++-- types/Modal.svelte.d.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f14a93c..5423dab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,11 @@ "rollup": "^2.56.3", "rollup-plugin-svelte": "^7.1.0", "sveld": "^0.15.1", - "svelte": "^3.31.2", + "svelte": "^3.35.0", "typescript": "^4.4.4" }, "peerDependencies": { - "svelte": "^3.31.2" + "svelte": "^3.35.0" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 838a52b..32a6c1f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/flekschas/svelte-simple-modal#readme", "peerDependencies": { - "svelte": "^3.31.2" + "svelte": "^3.35.0" }, "devDependencies": { "@tsconfig/svelte": "^2.0.1", @@ -52,7 +52,7 @@ "rollup": "^2.56.3", "rollup-plugin-svelte": "^7.1.0", "sveld": "^0.15.1", - "svelte": "^3.31.2", + "svelte": "^3.35.0", "typescript": "^4.4.4" } } diff --git a/types/Modal.svelte.d.ts b/types/Modal.svelte.d.ts index 6d89a44..acaa548 100644 --- a/types/Modal.svelte.d.ts +++ b/types/Modal.svelte.d.ts @@ -1,5 +1,5 @@ /// -import type { SvelteComponentTyped } from 'svelte'; +import type { SvelteComponentTyped } from "svelte"; /** * Create a Svelte component with props bound to it.