diff --git a/showcase/app/controllers/components/flyout.js b/showcase/app/controllers/components/flyout.js index eb514e684c0..d8b175593ba 100644 --- a/showcase/app/controllers/components/flyout.js +++ b/showcase/app/controllers/components/flyout.js @@ -12,6 +12,10 @@ export default class FlyoutController extends Controller { @tracked largeFlyoutActive = false; @tracked dropdownInitiatedFlyoutActive = false; @tracked dropdownInitiatedWithReturnedFocusFlyoutActive = false; + @tracked deactivateFlyoutOnCloseActive = false; + @tracked deactivateFlyoutOnDestroyActive = false; + @tracked deactivateFlyoutOnSubmitActive = false; + @tracked deactivateFlyoutOnSubmitValidationError = false; @action activateFlyout(Flyout) { @@ -22,4 +26,18 @@ export default class FlyoutController extends Controller { deactivateFlyout(Flyout) { this[Flyout] = false; } + + @action + deactivateFlyoutOnSubmit(event) { + event.preventDefault(); // Prevent page reload + const formData = new FormData(event.target); + const value = formData.get('deactivate-flyout-on-submit__input'); + + if (!value) { + this.deactivateFlyoutOnSubmitValidationError = true; + } else { + this.deactivateFlyoutOnSubmitValidationError = false; + this.deactivateFlyoutOnSubmitActive = false; + } + } } diff --git a/showcase/app/controllers/components/modal.js b/showcase/app/controllers/components/modal.js index 7fd256e21b9..9e24bff72a0 100644 --- a/showcase/app/controllers/components/modal.js +++ b/showcase/app/controllers/components/modal.js @@ -20,6 +20,10 @@ export default class ModalController extends Controller { @tracked isDismissDisabled; @tracked dropdownInitiatedModalActive = false; @tracked dropdownInitiatedWithReturnedFocusModalActive = false; + @tracked deactivateModalOnCloseActive = false; + @tracked deactivateModalOnDestroyActive = false; + @tracked deactivateModalOnSubmitActive = false; + @tracked deactivateModalOnSubmitValidationError = false; @action activateModal(modal) { @@ -39,6 +43,20 @@ export default class ModalController extends Controller { } } + @action + deactivateModalOnSubmit(event) { + event.preventDefault(); // Prevent page reload + const formData = new FormData(event.target); + const value = formData.get('deactivate-modal-on-submit__input'); + + if (!value) { + this.deactivateModalOnSubmitValidationError = true; + } else { + this.deactivateModalOnSubmitValidationError = false; + this.deactivateModalOnSubmitActive = false; + } + } + @action resetIsDismissDisabled() { this.isDismissDisabled = false; diff --git a/showcase/app/templates/components/flyout.hbs b/showcase/app/templates/components/flyout.hbs index 3c1205761cb..02bd1d25812 100644 --- a/showcase/app/templates/components/flyout.hbs +++ b/showcase/app/templates/components/flyout.hbs @@ -266,17 +266,17 @@ - + Flyout title - - + +

Flyout content

-
- + + - +
{{/if}} @@ -297,17 +297,122 @@ id="dropdown-initiated-flyout-with-returned-focus" @onClose={{fn this.deactivateFlyout "dropdownInitiatedWithReturnedFocusFlyoutActive"}} @returnFocusTo="dropdown-initiated-flyout-with-returned-focus-toggle" - as |M| + as |F| > - + Flyout title - - + +

Flyout content

-
- + + + + + + {{/if}} + +
+
+ + + + {{#if this.deactivateFlyoutOnCloseActive}} + + + Flyout title + + +

Clicking the "confirm" button executes the + F.close + method.

+

This is equivalent to a + manual dismiss (Esc + key, click outsite, click dismiss button) because they're all calling the same function, which invokes the + native + close() + method of the + Dialog + HTML element, who then will cause the + willDestroyNode + action to execute.

+
+ -
+ + + {{/if}} + + + + {{#if this.deactivateFlyoutOnDestroyActive}} + + + Flyout title + + +

Clicking the "confirm" button will directly remove the + flyout from the DOM.

+

This is not equivalent to + a manual dismiss (Esc + key, click outsite, click dismiss button) because it will trigger directly the + willDestroyNode + action.

+
+ + + +
+ {{/if}} + + + + {{#if this.deactivateFlyoutOnSubmitActive}} + + + Flyout title + + +

Clicking the "confirm" button will submit the form and + the associated action will remove the flyout from the DOM.

+

This is not equivalent + to a manual dismiss (Esc + key, click outsite, click dismiss button) because it will trigger directly the + willDestroyNode + action.

+
+ + Fill in this input + This is a fake input, used to emulate validation on submit + {{#if this.deactivateFlyoutOnSubmitValidationError}} + Fill in the input above + {{/if}} + +
+
+ + + + + +
{{/if}} diff --git a/showcase/app/templates/components/modal.hbs b/showcase/app/templates/components/modal.hbs index 355153aadc2..23cbcc74172 100644 --- a/showcase/app/templates/components/modal.hbs +++ b/showcase/app/templates/components/modal.hbs @@ -568,5 +568,109 @@ {{/if}} +
+
+ + + + {{#if this.deactivateModalOnCloseActive}} + + + Modal title + + +

Clicking the "confirm" button executes the + F.close + method.

+

This is equivalent to a + manual dismiss (Esc + key, click outsite, click dismiss button) because they're all calling the same function, which invokes the + native + close() + method of the + Dialog + HTML element, who then will cause the + willDestroyNode + action to execute.

+
+ + + +
+ {{/if}} + + + + {{#if this.deactivateModalOnDestroyActive}} + + + Modal title + + +

Clicking the "confirm" button will directly remove the + modal from the DOM.

+

This is not equivalent to + a manual dismiss (Esc + key, click outsite, click dismiss button) because it will trigger directly the + willDestroyNode + action.

+
+ + + +
+ {{/if}} + + + + {{#if this.deactivateModalOnSubmitActive}} + + + Modal title + + +

Clicking the "confirm" button will submit the form and + the associated action will remove the modal from the DOM.

+

This is not equivalent + to a manual dismiss (Esc + key, click outsite, click dismiss button) because it will trigger directly the + willDestroyNode + action.

+
+ + Fill in this input + This is a fake input, used to emulate validation on submit + {{#if this.deactivateModalOnSubmitValidationError}} + Fill in the input above + {{/if}} + +
+
+ + + + + + +
+ {{/if}} + {{! template-lint-enable no-autofocus-attribute }} \ No newline at end of file diff --git a/showcase/tests/integration/components/hds/flyout/index-test.js b/showcase/tests/integration/components/hds/flyout/index-test.js index d403644538a..e30840297c8 100644 --- a/showcase/tests/integration/components/hds/flyout/index-test.js +++ b/showcase/tests/integration/components/hds/flyout/index-test.js @@ -3,11 +3,12 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { module, skip, test } from 'qunit'; +import { module, test, skip } from 'qunit'; import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, + triggerKeyEvent, resetOnerror, setupOnerror, settled, @@ -138,6 +139,162 @@ module('Integration | Component | hds/flyout/index', function (hooks) { await click('button.hds-flyout__dismiss'); assert.dom('#test-flyout').isNotVisible(); }); + test('it should close the flyout when the "close" function is called', async function (assert) { + await render( + hbs` + + + + `, + ); + assert.dom('#test-flyout').isVisible(); + await click('#cancel-button'); + assert.dom('#test-flyout').isNotVisible(); + }); + test('it should close the flyout when the "esc" key is pressed', async function (assert) { + await render( + hbs`Title`, + ); + assert.dom('#test-flyout').isVisible(); + await triggerKeyEvent('.hds-flyout', 'keydown', 'Escape'); + assert.dom('#test-flyout').isNotVisible(); + }); + test('it should close the flyout when clicking outside', async function (assert) { + await render( + hbs`Title`, + ); + assert.dom('#test-flyout').isVisible(); + await click('.hds-flyout__overlay'); + assert.dom('#test-flyout').isNotVisible(); + }); + + // BODY OVERFLOW + + test('it should close the flyout and remove the body overflow style - manual dismiss', async function (assert) { + await render( + hbs`Title`, + ); + + // when the flyout is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-flyout').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the flyout is closed the `overflow:hidden` style should be removed + await click('button.hds-flyout__dismiss'); + assert.dom('#test-flyout').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the flyout and remove the body overflow style - click outside', async function (assert) { + await render( + hbs`Title`, + ); + + // when the flyout is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-flyout').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the flyout is closed the `overflow:hidden` style should be removed + await click('.hds-flyout__overlay'); + assert.dom('#test-flyout').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the flyout and remove the body overflow style - dismiss via `F.close`', async function (assert) { + await render( + hbs` + Title + + + + `, + ); + + // when the flyout is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-flyout').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the flyout is closed the `overflow:hidden` style should be removed + await click('#cancel-button'); + assert.dom('#test-flyout').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the flyout and remove the body overflow style - direct DOM removal', async function (assert) { + this.set('isFlyoutRendered', false); + this.set( + 'deactivateFlyout', + function () { + this.set('isFlyoutRendered', false); + }.bind(this), + ); + + await render( + hbs` + {{#if this.isFlyoutRendered}} + + Title + + + + + {{/if}} + `, + ); + + assert.dom('#test-flyout').doesNotExist(); + this.set('isFlyoutRendered', true); + assert.dom('#test-flyout').exists(); + + // when the flyout is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-flyout').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the flyout is removed from the DOM the `overflow:hidden` style should be removed + await click('#confirm-button'); + assert.dom('#test-flyout').doesNotExist(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the flyout and remove the body overflow style - form submit', async function (assert) { + this.set('isFlyoutRendered', false); + this.set( + 'deactivateFlyoutOnSubmit', + function (event) { + event.preventDefault(); // prevent page reload + this.set('isFlyoutRendered', false); + }.bind(this), + ); + + await render( + hbs` + {{#if this.isFlyoutRendered}} + + Title + +
+ + + + + + {{/if}} + `, + ); + + assert.dom('#test-flyout').doesNotExist(); + this.set('isFlyoutRendered', true); + assert.dom('#test-flyout').exists(); + + // when the flyout is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-flyout').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the form is submitted and the flyout is removed from the DOM the `overflow:hidden` style should be removed + await click('#submit-button'); + assert.dom('#test-flyout').doesNotExist(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); // ACCESSIBILITY @@ -176,7 +333,7 @@ module('Integration | Component | hds/flyout/index', function (hooks) { assert.dom('#test-button').isFocused(); }); - // not sure how to reach the `body` element, it says "body is not a valid root element" + // this test is flaky in CI, so skipping for now skip('it returns focus to the `body` element, if the one that initiated the open event not anymore in the DOM', async function (assert) { await render( hbs` @@ -194,7 +351,7 @@ module('Integration | Component | hds/flyout/index', function (hooks) { await click('#test-interactive'); assert.true(this.showFlyout); await click('button.hds-flyout__dismiss'); - assert.dom('body', 'body').isFocused(); + assert.dom('body', 'document').isFocused(); }); test('it returns focus to a specific element if provided via`@returnFocusTo`', async function (assert) { @@ -232,7 +389,7 @@ module('Integration | Component | hds/flyout/index', function (hooks) { assert.ok(opened); }); - skip('it should call `onClose` function if provided', async function (assert) { + test('it should call `onClose` function if provided', async function (assert) { let closed = false; this.set('onClose', () => (closed = true)); await render( diff --git a/showcase/tests/integration/components/hds/modal/index-test.js b/showcase/tests/integration/components/hds/modal/index-test.js index 3f4067bdc7a..98342bd2ec1 100644 --- a/showcase/tests/integration/components/hds/modal/index-test.js +++ b/showcase/tests/integration/components/hds/modal/index-test.js @@ -139,6 +139,23 @@ module('Integration | Component | hds/modal/index', function (hooks) { await click('#cancel-button'); assert.dom('#test-modal').isNotVisible(); }); + test('it should close the modal when the "esc" key is pressed', async function (assert) { + await render( + hbs`Title`, + ); + assert.dom('#test-modal').isVisible(); + await triggerKeyEvent('.hds-modal', 'keydown', 'Escape'); + assert.dom('#test-modal').isNotVisible(); + }); + // TODO! while we decide what to do about the original bug + skip('it should close the modal when clicking outside', async function (assert) { + await render( + hbs`Title`, + ); + assert.dom('#test-modal').isVisible(); + await click('.hds-modal__overlay'); + assert.dom('#test-modal').isNotVisible(); + }); test('it should not close the modal when `@isDismissDisabled` is `true`', async function (assert) { this.set('isDismissDisabled', true); await render( @@ -169,6 +186,134 @@ module('Integration | Component | hds/modal/index', function (hooks) { assert.dom('#test-modal').isNotVisible(); }); + // BODY OVERFLOW + + test('it should close the modal and remove the body overflow style - manual dismiss', async function (assert) { + await render( + hbs`Title`, + ); + + // when the modal is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-modal').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the modal is closed the `overflow:hidden` style should be removed + await click('button.hds-modal__dismiss'); + assert.dom('#test-modal').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the modal and remove the body overflow style - click outside', async function (assert) { + await render( + hbs`Title`, + ); + + // when the modal is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-modal').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the modal is closed the `overflow:hidden` style should be removed + await click('.hds-modal__overlay'); + assert.dom('#test-flyout').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the modal and remove the body overflow style - dismiss via `F.close`', async function (assert) { + await render( + hbs` + Title + + + + `, + ); + + // when the modal is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-modal').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the modal is closed the `overflow:hidden` style should be removed + await click('#cancel-button'); + assert.dom('#test-modal').isNotVisible(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the modal and remove the body overflow style - direct DOM removal', async function (assert) { + this.set('isModalRendered', false); + this.set( + 'deactivateModal', + function () { + this.set('isModalRendered', false); + }.bind(this), + ); + + await render( + hbs` + {{#if this.isModalRendered}} + + Title + + + + + {{/if}} + `, + ); + + assert.dom('#test-modal').doesNotExist(); + this.set('isModalRendered', true); + assert.dom('#test-modal').exists(); + + // when the modal is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-modal').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the modal is removed from the DOM the `overflow:hidden` style should be removed + await click('#confirm-button'); + assert.dom('#test-modal').doesNotExist(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + + test('it should close the modal and remove the body overflow style - form submit', async function (assert) { + this.set('isModalRendered', false); + this.set( + 'deactivateModalOnSubmit', + function (event) { + event.preventDefault(); // prevent page reload + this.set('isModalRendered', false); + }.bind(this), + ); + + await render( + hbs` + {{#if this.isModalRendered}} + + Title + + + + + + + + {{/if}} + `, + ); + + assert.dom('#test-modal').doesNotExist(); + this.set('isModalRendered', true); + assert.dom('#test-modal').exists(); + + // when the modal is open the `` element gets applied an overflow:hidden via inline style + assert.dom('#test-modal').isVisible(); + assert.dom('body', document).hasStyle({ overflow: 'hidden' }); + + // when the form is submitted and the modal is removed from the DOM the `overflow:hidden` style should be removed + await click('#submit-button'); + assert.dom('#test-modal').doesNotExist(); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); + }); + // ACCESSIBILITY test('it uses the title as name for the dialog', async function (assert) {