diff --git a/CHANGELOG.md b/CHANGELOG.md index cb01037..310f734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.0.0 + +- Use native `dialog` + ## 4.5.4 - Convert to `.gjs` diff --git a/README.md b/README.md index 0ee558d..ca15e94 100644 --- a/README.md +++ b/README.md @@ -32,30 +32,27 @@ https://zestia.github.io/ember-modal-dialog ## Features +- Uses native `dialog` ✔︎ - Focus trap ✔︎ - Body scroll lock ✔︎ - Loading state handling ✔︎ - Optionally escapable ✔︎ -- Exceeds viewport detection ✔︎ -- Animatable (includes test waiters) ✔︎ +- Animatable (remains in the DOM until animated out) ✔︎ - Simple API ✔︎ ## Notes - This addon intentionally does not come with any styles. + - It is configured with [ember-test-waiters](https://github.com/emberjs/ember-test-waiters) so `await`ing in your test suite will just work. -- Does not use native `dialog` _yet_, because: - - Can't animate `::backdrop` - - Can't use `::backdrop` with CSS variables - - Does not provide a focus trap - - Does not provide a scroll lock -## Example +- Animating a modal dialog out is [not possible](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#dialog_keyframe_animations#css_3). Although you can achieve it with transitions. (see demo) -The modal dialog component isn't designed to be used on its own, but rather used to compose a new modal dialog component... in this example it's called "my-modal" +- Native body scroll lock only works on the body element (see demo) + +## Example ```handlebars -{{! my-modal.hbs }} Content @@ -96,10 +93,6 @@ Optional. Fired when the request to load data fails. Receives the error as a par Required. This action fires when `close` has been called, _and_ any animations have run to hide the modal dialog. -#### `@onEscape` - -Optional. Fired when escape is pressed or the user clicks outside the dialog box. You can use the API to call `close` for example. - ### API #### `close` @@ -109,11 +102,3 @@ Call this when you want to close the modal. It will first wait for any animation #### `isLoading` Whether the data required for the modal dialog to display is loading. - -#### `element` - -The DOM element of the modal dialog component. - -#### `boxElement` - -The inner DOM element of the modal dialog component, that contains the content. diff --git a/addon/components/modal-dialog.gjs b/addon/components/modal-dialog.gjs index c5847f8..e3f95b2 100644 --- a/addon/components/modal-dialog.gjs +++ b/addon/components/modal-dialog.gjs @@ -1,218 +1,47 @@ import Component from '@glimmer/component'; -import { modifier } from 'ember-modifier'; -import { tracked } from '@glimmer/tracking'; -import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'; import { action } from '@ember/object'; -import { on } from '@ember/modifier'; +import { tracked } from '@glimmer/tracking'; import { waitFor } from '@ember/test-waiters'; import { waitForAnimation } from '@zestia/animation-utils'; +import { on } from '@ember/modifier'; +import { modifier } from 'ember-modifier'; export default class ModalDialogComponent extends Component { - @tracked isInViewport; - @tracked isLoading = this.shouldLoad; - @tracked isShowing = true; + @tracked element; + @tracked isLoading = true; + @tracked isWarning; - element; - boxElement; - lastMouseDownElement; + modal = modifier( + (element) => { + this.element = element; + this.element.showModal(); + }, + { eager: false } + ); constructor() { super(...arguments); this.args.onReady?.(this.api); - - if (this.shouldLoad) { - this._load(); - } - } - - get shouldLoad() { - return typeof this.args.onLoad === 'function'; - } - - get containsModal() { - return !!this.boxElement.querySelector('.modal-dialog'); + this._load(); } @action - async close() { - this.isShowing = false; - - await this._waitForAnimation(); + handleKeyDown(event) { + if (event.key !== 'Escape') { + return; + } - this.args.onClose?.(); + this._handleEscape(event); } @action - handleMouseDown(event) { - this.lastMouseDownElement = event.target; - } - - @action - handleMouseUp(event) { - if (this.lastMouseDownElement === this.element) { - this._escape(event); - } - } - - registerElement = modifier( - (element) => { - this.element = element; - }, - { eager: false } - ); - - registerBoxElement = modifier( - (element) => { - this.boxElement = element; - }, - { eager: false } - ); - - bodyScrollLock = modifier( - (element) => { - disableBodyScroll(element, { - reserveScrollBarGap: true, - allowTouchMove: (element) => { - while (this.boxElement.contains(element)) { - if (element.scrollHeight > element.clientHeight) { - return true; - } - - element = element.parentElement; - } - } - }); - - return () => enableBodyScroll(element); - }, - { eager: false } - ); - - inViewport = modifier( - (element) => { - const handler = () => { - const rect = element.getBoundingClientRect(); - - this.isInViewport = - rect.top > 0 && - rect.left > 0 && - rect.bottom < window.innerHeight && - rect.right < window.innerWidth; - }; - - const observer = new MutationObserver(handler); - - observer.observe(element, { - childList: true, - subtree: true - }); - - window.addEventListener('resize', handler); - - handler(); - - return () => { - observer.disconnect(); - window.removeEventListener('resize', handler); - }; - }, - { eager: false } - ); - - trapFocus = modifier( - (element) => { - const handler = (event) => { - if (this.containsModal || event.key !== 'Tab') { - return; - } - - const focusable = element.querySelectorAll(` - a[href], - button:not(:disabled), - textarea:not(:disabled), - input:not(:disabled), - select:not(:disabled), - [tabindex="0"], - [contenteditable="true"] - `); - - const first = focusable.item(0); - const last = focusable.item(focusable.length - 1); - const focused = document.activeElement; - - if (event.shiftKey && focused === first) { - last.focus(); - event.preventDefault(); - } else if (!event.shiftKey && focused === last) { - first.focus(); - event.preventDefault(); - } - }; - - element.addEventListener('keydown', handler); - - return () => element.removeEventListener('keydown', handler); - }, - { eager: false } - ); - - internalFocus = modifier( - () => { - let last; - - const focused = () => last?.focus(); - const blurred = () => (last = document.activeElement); - - window.addEventListener('focus', focused); - window.addEventListener('blur', blurred); - - return () => { - window.removeEventListener('focus', focused); - window.removeEventListener('blur', blurred); - }; - }, - { eager: false } - ); - - externalFocus = modifier( - () => { - const last = document.activeElement; - - return () => { - try { - last.focus(); - } catch (error) { - // Squelch - } - }; - }, - { eager: false } - ); - - escapable = modifier( - () => { - const handler = (event) => { - if (this.containsModal || event.key !== 'Escape') { - return; - } - - this._escape(event); - }; - - window.addEventListener('keydown', handler); - - return () => window.removeEventListener('keydown', handler); - }, - { eager: false } - ); - - _escape(event) { - this.args.onEscape?.(this.api, event); + close() { + return this._close(); } async _load() { try { - const data = await this.args.onLoad(); + const data = await this.args.onLoad?.(); this.args.onLoaded?.(data); } catch (error) { this.args.onLoadError?.(error); @@ -221,20 +50,34 @@ export default class ModalDialogComponent extends Component { } } + _handleEscape(event) { + event.preventDefault(); + + if (this.args.escapable) { + this._close(); + } else { + this._warn(); + } + } + + @waitFor + async _close() { + this.element.close(); + await waitForAnimation(this.element, { maybe: true }); + this.args.onClose(); + } + @waitFor - _waitForAnimation() { - return Promise.all([ - waitForAnimation(this.element, { maybe: true }), - waitForAnimation(this.boxElement, { maybe: true }) - ]); + async _warn() { + this.isWarning = true; + await waitForAnimation(this.element); + this.isWarning = false; } get _api() { return { close: this.close, - isLoading: this.isLoading, - element: this.element, - boxElement: this.boxElement + isLoading: this.isLoading }; } @@ -246,31 +89,16 @@ export default class ModalDialogComponent extends Component { }); } diff --git a/package-lock.json b/package-lock.json index d578c77..ce3f2af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zestia/ember-modal-dialog", - "version": "4.5.3", + "version": "5.0.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zestia/ember-modal-dialog", - "version": "4.5.3", + "version": "5.0.0-0", "license": "MIT", "dependencies": { "@babel/core": "^7.23.6", diff --git a/package.json b/package.json index 30a50eb..b61b809 100644 --- a/package.json +++ b/package.json @@ -103,5 +103,5 @@ "test:ember": "ember test", "test:ember-compatibility": "ember try:each" }, - "version": "4.5.3" + "version": "5.0.0-0" } diff --git a/tests/dummy/app/components/example-modal.gjs b/tests/dummy/app/components/example-modal.gjs new file mode 100644 index 0000000..2c19258 --- /dev/null +++ b/tests/dummy/app/components/example-modal.gjs @@ -0,0 +1,35 @@ +/* https://github.com/ember-cli/eslint-plugin-ember/issues/2035 */ +/* eslint-disable no-unused-expressions */ + +import ModalDialog from '@zestia/ember-modal-dialog/components/modal-dialog'; +import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus'; +import LoremIpsumShort from 'dummy/components/lorem-ipsum-short'; +import { on } from '@ember/modifier'; + + diff --git a/tests/dummy/app/components/example-modal.hbs b/tests/dummy/app/components/example-modal.hbs deleted file mode 100644 index 319f086..0000000 --- a/tests/dummy/app/components/example-modal.hbs +++ /dev/null @@ -1,116 +0,0 @@ - - {{#if modal.isLoading}} -

Please wait...

- {{else}} - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est - eget nulla malesuada pretium id eu ipsum. Nam eu turpis et quam rutrum - molestie a vitae tortor. Maecenas nec ex est. Nulla quis leo id leo - commodo sagittis. Nullam diam dolor, semper sed convallis ut, facilisis - sollicitudin quam. Ut a fermentum turpis, ac gravida elit. Fusce sed leo - mollis nibh tincidunt ornare. Quisque convallis sodales erat, eget - ultricies nulla pellentesque ut. Curabitur nec sem lorem. -

- -

- Vivamus malesuada arcu vitae mauris fringilla, sed lacinia nunc porta. - Nullam id eros vitae arcu blandit tincidunt. Fusce porta sed mi sit amet - varius. Proin congue accumsan rutrum. Aenean ac cursus ex. Sed sed - venenatis urna. Aliquam elementum felis et eros egestas pellentesque. - Nulla vulputate libero non cursus hendrerit. -

- -

- Aenean suscipit rhoncus sapien a blandit. Morbi urna nisi, porttitor sit - amet lacus vitae, suscipit porta ipsum. Fusce blandit orci nec faucibus - malesuada. In feugiat libero id nisl egestas, vel cursus nisi pulvinar. - Pellentesque accumsan libero id nulla tincidunt tristique. Sed eget - ligula enim. Cras iaculis lacinia ipsum id sodales. In sagittis tempus - maximus. Duis ultrices urna magna, vitae vestibulum augue eleifend - vitae. -

- - {{#if this.showMoreContent}} -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est - eget nulla malesuada pretium id eu ipsum. Nam eu turpis et quam rutrum - molestie a vitae tortor. Maecenas nec ex est. Nulla quis leo id leo - commodo sagittis. Nullam diam dolor, semper sed convallis ut, - facilisis sollicitudin quam. Ut a fermentum turpis, ac gravida elit. - Fusce sed leo mollis nibh tincidunt ornare. Quisque convallis sodales - erat, eget ultricies nulla pellentesque ut. Curabitur nec sem lorem. -

- -

- Vivamus malesuada arcu vitae mauris fringilla, sed lacinia nunc porta. - Nullam id eros vitae arcu blandit tincidunt. Fusce porta sed mi sit - amet varius. Proin congue accumsan rutrum. Aenean ac cursus ex. Sed - sed venenatis urna. Aliquam elementum felis et eros egestas - pellentesque. Nulla vulputate libero non cursus hendrerit. -

- -

- Aenean suscipit rhoncus sapien a blandit. Morbi urna nisi, porttitor - sit amet lacus vitae, suscipit porta ipsum. Fusce blandit orci nec - faucibus malesuada. In feugiat libero id nisl egestas, vel cursus nisi - pulvinar. Pellentesque accumsan libero id nulla tincidunt tristique. - Sed eget ligula enim. Cras iaculis lacinia ipsum id sodales. In - sagittis tempus maximus. Duis ultrices urna magna, vitae vestibulum - augue eleifend vitae. Cras tristique est quis ipsum ullamcorper - tincidunt. Phasellus non dolor in urna scelerisque gravida. Ut - volutpat tortor vitae risus tristique placerat. Vivamus iaculis varius - nunc, nec tincidunt lorem facilisis non. Maecenas ultricies risus - urna, finibus luctus erat posuere vitae. -

- -

- Mauris interdum sagittis neque id interdum. Nam eu vulputate felis. - Praesent nec rutrum dui. Vivamus hendrerit tortor a tortor blandit - viverra. Aliquam sagittis risus erat, nec imperdiet libero posuere - eget. Aenean sed hendrerit ligula, eu rhoncus nisi. Aliquam ipsum - mauris, cursus ut viverra vitae, lacinia accumsan arcu. Integer lorem - tellus, sagittis ut euismod at, lacinia eu tortor. In sed felis - pretium, dictum massa eu, tincidunt sapien. Aliquam in ante eu nisl - sodales consequat quis eu eros. Aenean tempus risus et erat egestas, - id lobortis massa laoreet. -

- -

- Nunc accumsan non mi laoreet varius. Morbi porttitor dapibus enim, eu - tincidunt massa gravida sed. Mauris facilisis ipsum mauris. Praesent - et accumsan leo. Nullam facilisis quam ut nisl finibus, ut dictum - nulla pulvinar. Vestibulum quam urna, ultricies ac ornare eu, egestas - sit amet turpis. Nunc dapibus mauris tellus. Etiam tristique convallis - mi, at pellentesque arcu pretium sed. Pellentesque rutrum ultrices - laoreet. -

- {{/if}} -
- - - - - {{/if}} - -
\ No newline at end of file diff --git a/tests/dummy/app/components/example-modal.js b/tests/dummy/app/components/example-modal.js deleted file mode 100644 index 823af8b..0000000 --- a/tests/dummy/app/components/example-modal.js +++ /dev/null @@ -1,12 +0,0 @@ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; - -export default class ExampleModalComponent extends Component { - @tracked showMoreContent = false; - - @action - addMoreContent() { - this.showMoreContent = true; - } -} diff --git a/tests/dummy/app/components/lorem-ipsum-long.gjs b/tests/dummy/app/components/lorem-ipsum-long.gjs new file mode 100644 index 0000000..e6bfed5 --- /dev/null +++ b/tests/dummy/app/components/lorem-ipsum-long.gjs @@ -0,0 +1,186 @@ +/* https://github.com/ember-cli/eslint-plugin-ember/issues/2035 */ +/* eslint-disable no-unused-expressions */ + + diff --git a/tests/dummy/app/components/lorem-ipsum-short.gjs b/tests/dummy/app/components/lorem-ipsum-short.gjs new file mode 100644 index 0000000..3116416 --- /dev/null +++ b/tests/dummy/app/components/lorem-ipsum-short.gjs @@ -0,0 +1,19 @@ +/* https://github.com/ember-cli/eslint-plugin-ember/issues/2035 */ +/* eslint-disable no-unused-expressions */ + + diff --git a/tests/dummy/app/controllers/application.js b/tests/dummy/app/controllers/application.js index 8541760..ad2f485 100644 --- a/tests/dummy/app/controllers/application.js +++ b/tests/dummy/app/controllers/application.js @@ -1,16 +1,10 @@ -/* eslint-disable no-alert */ - import Controller from '@ember/controller'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { later } from '@ember/runloop'; export default class ApplicationController extends Controller { @tracked isEscapable = false; - @tracked clickOutsideToEscape = true; @tracked loadDelay = false; - @tracked confirmEscape = false; - @tracked escapeOnFocusLeave = false; @tracked showExampleModal = false; @action @@ -24,30 +18,8 @@ export default class ApplicationController extends Controller { } @action - resetClickOutsideToEscape() { - this.clickOutsideToEscape = true; - } - - @action - handleEscapeExampleModal(modal, event) { - const escapable = - this.isEscapable && - ((this.clickOutsideToEscape && event instanceof MouseEvent) || - event instanceof KeyboardEvent); - - if (!escapable) { - return; - } - - let close = true; - - if (this.confirmEscape) { - close = confirm('Are you sure?'); - } - - if (close) { - modal.close(); - } + setEscapable(event) { + this.isEscapable = event.target.checked; } @action @@ -56,8 +28,6 @@ export default class ApplicationController extends Controller { return; } - return new Promise((resolve) => { - later(resolve, 2000); - }); + return new Promise((resolve) => setTimeout(resolve, 2000)); } } diff --git a/tests/dummy/app/styles/app.css b/tests/dummy/app/styles/app.css deleted file mode 100644 index 2763afa..0000000 --- a/tests/dummy/app/styles/app.css +++ /dev/null @@ -1 +0,0 @@ -/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ diff --git a/tests/dummy/app/styles/app.scss b/tests/dummy/app/styles/app.scss index e327ea4..51375da 100644 --- a/tests/dummy/app/styles/app.scss +++ b/tests/dummy/app/styles/app.scss @@ -1,78 +1,82 @@ -@keyframes fade-in { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } +:root { + overflow: hidden; } -@keyframes fade-out { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } +// stylelint-disable-next-line selector-max-type +body { + overflow: auto; + height: 100vh; + margin: 0; } -:root { - --modal-bg-colour: rgba(0 0 0 / 20%); - --modal-box-bg-colour: white; - --modal-box-border-colour: grey; - --modal-box-padding: 15px; - --modal-ani-speed: 300ms; - --focus-outline-colour: red; - --example-text-colour: darkgrey; - --space: 20px; - --width: 480px; - --height: 508px; +.inside-the-application { + margin: 20px; // stylelint-disable-line scale-unlimited/declaration-strict-value } .example-text { - color: var(--example-text-colour); -} - -:focus { - outline: 2px solid var(--focus-outline-colour); + color: darkgrey; // stylelint-disable-line } .modal-dialog { - background-color: var(--modal-bg-colour); - display: flex; - justify-content: center; - align-items: center; - position: fixed; - inset: 0; -} - -.modal-dialog__box { - background-color: var(--modal-box-bg-colour); - border: 1px solid var(--modal-box-border-colour); + background-color: white; box-sizing: border-box; - width: var(--width); - padding: var(--space); - margin: var(--space); -} + width: 480px; + opacity: 0; + transition: + opacity 1s, + overlay 1s allow-discrete, + display 1s allow-discrete; -.modal-dialog[data-showing='true'] { - animation: fade-in var(--modal-ani-speed); -} + &::backdrop { + background-color: rgb(0 0 0 / 0%); + transition: + background-color 1s, + overlay 1s allow-discrete, + display 1s allow-discrete; + } -.modal-dialog[data-showing='false'] { - animation: fade-out var(--modal-ani-speed); -} + &[data-warning] { + animation: warn 300ms; + } + + &[aria-busy='true'] { + text-align: center; + } + + &[open] { + opacity: 1; + } + + &[open]::backdrop { + background-color: rgb(0 0 0 / 30%); + } + + // stylelint-disable-next-line scss/at-rule-no-unknown + @starting-style { + &[open] { + opacity: 0; + } -.modal-dialog__box[aria-busy='true'] { - display: flex; - align-items: center; - justify-content: center; - min-height: var(--height); + &[open]::backdrop { + background-color: rgb(0 0 0 / 0%); + } + } } -.modal-dialog__box[data-in-viewport='false'] { - overflow-y: scroll; - max-height: 100%; +@keyframes warn { + 0% { + transform: scale(1); + } + + 40% { + transform: scale(1.02); + } + + 60% { + transform: scale(1.02); + } + + 100% { + transform: scale(1); + } } diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 9ea51e5..2528d97 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -3,195 +3,41 @@ @zestia/ember-modal-dialog -
- - Options - +

+

-
- - - -
- - - -
- +

-

+

-
-
- - - - +

+ +

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut est eget - nulla malesuada pretium id eu ipsum. Nam eu turpis et quam rutrum molestie - a vitae tortor. Maecenas nec ex est. Nulla quis leo id leo commodo - sagittis. Nullam diam dolor, semper sed convallis ut, facilisis - sollicitudin quam. Ut a fermentum turpis, ac gravida elit. Fusce sed leo - mollis nibh tincidunt ornare. Quisque convallis sodales erat, eget - ultricies nulla pellentesque ut. Curabitur nec sem lorem. -

- -

- Vivamus malesuada arcu vitae mauris fringilla, sed lacinia nunc porta. - Nullam id eros vitae arcu blandit tincidunt. Fusce porta sed mi sit amet - varius. Proin congue accumsan rutrum. Aenean ac cursus ex. Sed sed - venenatis urna. Aliquam elementum felis et eros egestas pellentesque. - Nulla vulputate libero non cursus hendrerit. -

- -

- Aenean suscipit rhoncus sapien a blandit. Morbi urna nisi, porttitor sit - amet lacus vitae, suscipit porta ipsum. Fusce blandit orci nec faucibus - malesuada. In feugiat libero id nisl egestas, vel cursus nisi pulvinar. - Pellentesque accumsan libero id nulla tincidunt tristique. Sed eget ligula - enim. Cras iaculis lacinia ipsum id sodales. In sagittis tempus maximus. - Duis ultrices urna magna, vitae vestibulum augue eleifend vitae. Cras - tristique est quis ipsum ullamcorper tincidunt. Phasellus non dolor in - urna scelerisque gravida. Ut volutpat tortor vitae risus tristique - placerat. Vivamus iaculis varius nunc, nec tincidunt lorem facilisis non. - Maecenas ultricies risus urna, finibus luctus erat posuere vitae. -

- -

- Fusce congue lacus ut augue varius faucibus non vel nunc. Suspendisse - aliquet mi nisi, vel pharetra ipsum euismod ut. Proin elementum ante eu - metus volutpat, vitae feugiat turpis commodo. Quisque tincidunt eget - mauris ac iaculis. Proin rhoncus ligula ut augue sollicitudin aliquet at - vel tellus. Vivamus congue sed nibh id gravida. Nulla fermentum aliquet - sapien id pulvinar. -

- -

- Morbi ut dolor mauris. Sed elementum, arcu et tincidunt tincidunt, dolor - elit finibus dolor, at volutpat metus felis at nisl. Nunc tempor blandit - suscipit. Phasellus lacinia felis ac placerat dapibus. Vestibulum ante - ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; - Vivamus nec velit et elit scelerisque elementum. Mauris volutpat ante non - condimentum vestibulum. Integer faucibus odio ac orci placerat feugiat. - Sed fermentum lectus vitae sem molestie interdum. Integer egestas purus et - velit facilisis, id mollis neque mollis. Sed risus eros, dignissim - lobortis lacinia vel, vehicula sed sem. Phasellus nec interdum leo, sed - mollis turpis. -

- -

- Fusce bibendum urna a purus condimentum, vitae volutpat turpis facilisis. - Pellentesque nec nisl lectus. Donec turpis mauris, bibendum a est a, - feugiat mollis nisi. Suspendisse luctus enim felis, eu ornare neque tempor - et. Nullam nibh nisl, imperdiet dictum nisl ut, rutrum maximus ligula. - Morbi euismod augue nisi, at vestibulum neque mattis venenatis. Morbi enim - risus, eleifend vel mauris dictum, sagittis sagittis urna. Aliquam tempor - dignissim urna, vitae tempor odio finibus quis. -

- -

- Mauris interdum sagittis neque id interdum. Nam eu vulputate felis. - Praesent nec rutrum dui. Vivamus hendrerit tortor a tortor blandit - viverra. Aliquam sagittis risus erat, nec imperdiet libero posuere eget. - Aenean sed hendrerit ligula, eu rhoncus nisi. Aliquam ipsum mauris, cursus - ut viverra vitae, lacinia accumsan arcu. Integer lorem tellus, sagittis ut - euismod at, lacinia eu tortor. In sed felis pretium, dictum massa eu, - tincidunt sapien. Aliquam in ante eu nisl sodales consequat quis eu eros. - Aenean tempus risus et erat egestas, id lobortis massa laoreet. -

- -

- Nunc accumsan non mi laoreet varius. Morbi porttitor dapibus enim, eu - tincidunt massa gravida sed. Mauris facilisis ipsum mauris. Praesent et - accumsan leo. Nullam facilisis quam ut nisl finibus, ut dictum nulla - pulvinar. Vestibulum quam urna, ultricies ac ornare eu, egestas sit amet - turpis. Nunc dapibus mauris tellus. Etiam tristique convallis mi, at - pellentesque arcu pretium sed. Pellentesque rutrum ultrices laoreet. -

- -

- Donec tincidunt in dolor nec aliquam. Suspendisse potenti. Curabitur quam - erat, posuere non mauris ac, aliquet auctor sem. Mauris imperdiet massa id - lectus tincidunt bibendum sed ut felis. Donec viverra augue dui, eu - viverra diam pretium in. Maecenas dictum nec tortor sed rhoncus. Quisque - rhoncus dui a elit scelerisque ultricies. Donec quis porta libero. Sed - vestibulum a tellus ac cursus. -

- -

- Donec luctus enim eget erat condimentum ornare. Sed volutpat bibendum - odio, et laoreet nisi tempor ac. Aliquam diam ex, hendrerit in elementum - quis, placerat non magna. Vestibulum vulputate mauris eu tortor suscipit, - ac bibendum magna rhoncus. Curabitur lacinia, ipsum vitae tincidunt - dapibus, velit augue tincidunt est, ac ultrices nisl ex eu sapien. Etiam - consequat sem eget tortor ultrices, at ultrices nulla vulputate. Vivamus - venenatis eleifend luctus. Nam eget augue imperdiet velit varius lobortis - ut nec lacus. Aenean iaculis neque quis tempus suscipit. Fusce aliquet - mattis eros, nec mattis diam feugiat eu. -

+
{{#if this.showExampleModal}} {{/if}}
diff --git a/tests/integration/components/modal-dialog-test.gjs b/tests/integration/components/modal-dialog-test.gjs index 307002d..bba9276 100755 --- a/tests/integration/components/modal-dialog-test.gjs +++ b/tests/integration/components/modal-dialog-test.gjs @@ -1,30 +1,31 @@ -import { module, test } from 'qunit'; +import { module, test, skip } from 'qunit'; import { setupRenderingTest } from 'dummy/tests/helpers'; import waitForAnimation from 'dummy/tests/helpers/wait-for-animation'; import { defer } from 'rsvp'; import { modifier } from 'ember-modifier'; import ModalDialog from '@zestia/ember-modal-dialog/components/modal-dialog'; -import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus'; import { on } from '@ember/modifier'; import { tracked } from '@glimmer/tracking'; import { click, - find, - focus, - getRootElement, render, - rerender, + waitFor, settled, - triggerEvent, triggerKeyEvent } from '@ember/test-helpers'; module('modal-dialog', function (hooks) { setupRenderingTest(hooks); + let close; + + hooks.beforeEach(function (assert) { + close = () => assert.step('closed'); + }); + module('rendering', function () { test('it works', async function (assert) { - assert.expect(5); + assert.expect(4); await render(); - assert.dom('.modal-dialog').hasAttribute('data-showing', 'true'); - assert.dom('.modal-dialog').hasClass('foo', 'splattributes'); - assert.dom('.modal-dialog__box').hasAttribute('role', 'dialog'); - assert.dom('.modal-dialog__box').hasAttribute('aria-modal', 'true'); - assert.dom('.modal-dialog__box').hasAttribute('aria-busy', 'false'); - }); - }); - - module('internal focus', function () { - let state; - - hooks.beforeEach(function () { - state = new (class { - @tracked show; - })(); - }); - - test('no focusable elements', async function (assert) { - assert.expect(1); - - await render(); - - await focus('.external'); - - state.show = true; - - await triggerEvent(window, 'blur'); - await triggerEvent(window, 'focus'); - - assert.deepEqual(document.activeElement, find('.external')); - }); - - test('with focusable elements', async function (assert) { - assert.expect(1); - - await render(); - - await focus('.external'); - - state.show = true; - - await focus('.internal2'); - await triggerEvent(window, 'blur'); - await triggerEvent(window, 'focus'); - - assert.deepEqual(document.activeElement, find('.internal2')); - }); - - test('ie', async function (assert) { - assert.expect(0); - - await render(); - - await triggerEvent(window, 'focus'); - }); - }); - - module('external focus', function (hooks) { - let state; - - hooks.beforeEach(function () { - state = new (class { - @tracked showButton; - @tracked showModal; - })(); - - return render(); - }); - - test('focus is restored after close', async function (assert) { - assert.expect(3); - - state.showButton = true; - - await focus('.external'); - - assert.dom('.external').isFocused(); - - state.showModal = true; - - await rerender(); - - assert.dom('.internal').isFocused(); - - state.showModal = false; - - await rerender(); - - assert.dom('.external').isFocused(); - }); - - test('does not blow up', async function (assert) { - assert.expect(3); - - state.showButton = true; - - await focus('.external'); - - assert.dom('.external').isFocused(); - - state.showModal = true; - state.showButton = false; - - await rerender(); - - assert.dom('.internal').isFocused(); - - state.showModal = false; - - await rerender(); - - assert.dom(document.body).isFocused(); - }); - }); - - module('focus trap', function (hooks) { - hooks.beforeEach(function () { - return render(); - }); - - test('tabbing forwards', async function (assert) { - assert.expect(1); - - await focus('.third'); - await triggerKeyEvent('.modal-dialog__box', 'keydown', 'Tab'); - - assert.deepEqual(find('.first'), document.activeElement); - }); - - test('tabbing backwards', async function (assert) { - assert.expect(1); - - await focus('.first'); - await triggerKeyEvent('.modal-dialog__box', 'keydown', 'Tab', { - shiftKey: true - }); - - assert.deepEqual(find('.third'), document.activeElement); - }); - - test('nested modals', async function (assert) { - assert.expect(1); - - await render(); - - await focus('.nested button'); - await triggerKeyEvent('.nested .modal-dialog__box', 'keydown', 'Tab'); - - assert.dom('.nested button').isFocused(); - }); - }); - - module('in viewport', function () { - test('changing content', async function (assert) { - assert.expect(2); - - getRootElement().parentNode.classList.add('full-screen'); - - await render(); - - assert.dom('.modal-dialog__box').hasAttribute('data-in-viewport', 'true'); - - find('.internal').innerHTML = '
'.repeat(10000); - - await settled(); - assert - .dom('.modal-dialog__box') - .hasAttribute('data-in-viewport', 'false'); - - getRootElement().parentNode.classList.remove('full-screen'); - }); - }); - - module('body scroll lock', function () { - test('it works', async function (assert) { - assert.expect(3); - - const state = new (class { - @tracked show; - })(); - - await render(); - - assert.dom(document.body).hasStyle({ overflow: 'visible' }); - - state.show = true; - - await settled(); - - assert.dom(document.body).hasStyle({ overflow: 'hidden' }); - - state.show = false; - - await settled(); - - assert.dom(document.body).hasStyle({ overflow: 'visible' }); + .dom('.modal-dialog') + .hasAttribute('open') + .hasClass('foo', 'splattributes') + .hasTagName('dialog') + .hasAttribute('aria-busy', 'false'); }); }); @@ -325,15 +88,15 @@ module('modal-dialog', function (hooks) {
); - assert.dom('.modal-dialog__box').hasAttribute('aria-busy', 'true'); - assert.dom('.modal-dialog__box').hasText('Please wait…'); + assert.dom('.modal-dialog').hasAttribute('aria-busy', 'true'); + assert.dom('.modal-dialog').hasText('Please wait…'); deferred.resolve('World'); await settled(); - assert.dom('.modal-dialog__box').hasAttribute('aria-busy', 'false'); - assert.dom('.modal-dialog__box').hasText('Hello World'); + assert.dom('.modal-dialog').hasAttribute('aria-busy', 'false'); + assert.dom('.modal-dialog').hasText('Hello World'); }); test('failure', async function (assert) { @@ -355,8 +118,8 @@ module('modal-dialog', function (hooks) { ); - assert.dom('.modal-dialog__box').hasAttribute('aria-busy', 'false'); - assert.dom('.modal-dialog__box').hasText('Failed sorry'); + assert.dom('.modal-dialog').hasAttribute('aria-busy', 'false'); + assert.dom('.modal-dialog').hasText('Failed sorry'); }); test('infinite revalidation', async function (assert) { @@ -382,12 +145,6 @@ module('modal-dialog', function (hooks) { }); module('closing', function (hooks) { - let close; - - hooks.beforeEach(function (assert) { - close = () => assert.step('closed'); - }); - test('waits for animation', async function (assert) { assert.expect(4); @@ -401,11 +158,9 @@ module('modal-dialog', function (hooks) { assert.verifySteps([]); - const animations = await waitForAnimation('.modal-dialog', { - animationName: 'fade-out' - }); + const animations = await waitForAnimation('.modal-dialog'); - assert.strictEqual(animations.length, 1); + assert.strictEqual(animations.length, 2); await settled(); @@ -417,9 +172,10 @@ module('modal-dialog', function (hooks) { await render(