Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: ability to prevent modal close #953

Merged
merged 5 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions packages/uui-modal/lib/uui-modal-sidebar.element.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css, html, PropertyValueMap } from 'lit';
import { property } from 'lit/decorators.js';
import { UUIModalCloseEvent, UUIModalElement } from './uui-modal.element';
import { UUIModalElement } from './uui-modal.element';
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';

export type UUIModalSidebarSize = 'small' | 'medium' | 'large' | 'full';
Expand All @@ -13,11 +13,6 @@ export class UUIModalSidebarElement extends UUIModalElement {
@property({ reflect: true })
size: UUIModalSidebarSize = 'full';

constructor() {
super();
this.addEventListener(UUIModalCloseEvent, this.#onClose.bind(this));
}

protected firstUpdated(
_changedProperties: Map<string | number | symbol, unknown>,
): void {
Expand All @@ -44,16 +39,14 @@ export class UUIModalSidebarElement extends UUIModalElement {
return this._dialogElement?.getBoundingClientRect().width ?? 0;
}

#onClose(event: Event) {
event.preventDefault();

forceClose() {
if (this.isClosing) return;

this.isClosing = true;
this.style.setProperty('--uui-modal-offset', -this.#getWidth + 'px');

setTimeout(() => {
this._closeModal();
super.forceClose();
}, this.transitionDuration);
}

Expand Down
18 changes: 14 additions & 4 deletions packages/uui-modal/lib/uui-modal.element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { LitElement, css } from 'lit';
import { property, query } from 'lit/decorators.js';

export const UUIModalOpenEvent = 'uui:modal-open';
export const UUIModalCloseEvent = 'uui:modal-close';
export const UUIModalCloseEndEvent = 'uui:modal-close-end';

export class UUIModalElement extends LitElement {
@query('dialog')
protected _dialogElement?: HTMLDialogElement;
Expand Down Expand Up @@ -46,12 +49,17 @@ export class UUIModalElement extends LitElement {
event?.preventDefault();
event?.stopImmediatePropagation();

const openEvent = new CustomEvent('open', {
const openEvent = new CustomEvent(UUIModalOpenEvent, {
cancelable: true,
});
// TODO: get rid of legacy 'open'-event sometime in the future. [NL]
const legacyOpenEvent = new CustomEvent('open', {
cancelable: true,
});

this.dispatchEvent(openEvent);
if (openEvent.defaultPrevented) return;
this.dispatchEvent(legacyOpenEvent);
if (openEvent.defaultPrevented || legacyOpenEvent.defaultPrevented) return;

this._openModal();
};
Expand All @@ -67,7 +75,7 @@ export class UUIModalElement extends LitElement {

if (closeEvent.defaultPrevented) return;

this._closeModal();
this.forceClose();
};

protected _openModal() {
Expand All @@ -76,12 +84,14 @@ export class UUIModalElement extends LitElement {
this._dialogElement?.addEventListener('cancel', this.close);
}

protected _closeModal() {
public forceClose() {
this.isClosing = true;
this.isOpen = false;
this._dialogElement?.close();

// TODO: get rid of legacy 'close-end'-event sometime in the future. [NL]
this.dispatchEvent(new CustomEvent('close-end'));
this.dispatchEvent(new CustomEvent(UUIModalCloseEndEvent));

this.remove();
}
Expand Down
10 changes: 5 additions & 5 deletions packages/uui-modal/lib/uui-modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,18 @@ Example from the sidebar:

All events from the modal base can be cancelled, thus preventing them from executing code, which enables you to customize the behavior.

#### `open`
#### `uui:modal-open`

Dispatched on first render. This will set open to true and show the modal. Can be cancelled if you want to prevent the modal from opening. But then you'll have to manually call `_openModal()` when you want to open the modal.

#### `close`
#### `uui:modal-close`

Dispatched when the modal is closed. Can be cancelled if you want to prevent the modal from closing. But then you'll have to manually call `_closeModal()` when you want to close the modal.
Dispatched when the modal is closed. Can be cancelled if you want to prevent the modal from closing. But then you'll have to manually call `forceClose()` when you want to close the modal.
This is used in the `uui-modal-sidebar` to wait for the animation to finish before removing the modal from the DOM.

#### `close-end`
#### `uui:modal-close-end`

This event is triggered before removing the component from the DOM, either after animations or delays or when `_closeModal()` is manually invoked.
This event is triggered before removing the component from the DOM, either after animations or delays or when `forceClose()` is manually invoked.

### CSS Variables

Expand Down
55 changes: 54 additions & 1 deletion packages/uui-modal/lib/uui-modal.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { html, fixture, expect } from '@open-wc/testing';
import { html, fixture, expect, oneEvent } from '@open-wc/testing';
import {
UUIModalDialogElement,
UUIModalSidebarElement,
UUIModalContainerElement,
UUIModalCloseEvent,
UUIModalCloseEndEvent,
} from '.';

describe('UUIModalContainerElement', () => {
Expand Down Expand Up @@ -53,4 +55,55 @@ describe('UUIModalSidebarElement', () => {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible();
});

describe('properties', () => {
it('has a size property', () => {
expect(element).to.have.property('size');
});

it('has a index property', () => {
expect(element).to.have.property('index');
});

it('has a uniqueIndex property', () => {
expect(element).to.have.property('uniqueIndex');
});

it('has a transitionDuration property', () => {
expect(element).to.have.property('transitionDuration');
});
});

it('can close', async () => {
expect(element.isOpen).to.be.true;

const listener = oneEvent(element, UUIModalCloseEvent);

element.close();

const event = await listener;
expect(event).to.exist;

const endListener = oneEvent(element, UUIModalCloseEndEvent);

const endEvent = await endListener;
expect(endEvent).to.exist;

expect(element.isOpen).to.be.false;
});

it('can have a prevented close', async () => {
expect(element.isOpen).to.be.true;

expect(element.isClosing).to.be.false;
element.addEventListener(UUIModalCloseEvent, e => e.preventDefault());
const closeListener = oneEvent(element, UUIModalCloseEvent);
element.close();

const closeEvent = await closeListener;
expect(closeEvent).to.exist;

expect(element.isClosing).to.be.false;
expect(element.isOpen).to.be.true;
});
});
Loading