Skip to content

Commit

Permalink
Add cancel event
Browse files Browse the repository at this point in the history
  • Loading branch information
RichardHelm committed Aug 1, 2024
1 parent a711d16 commit 0de40cc
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 39 deletions.
84 changes: 80 additions & 4 deletions libs/components/src/lib/dialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,11 @@ The dialog has a default `--dialog-max-block-size`. If the content is larger, th

<div class="table-wrapper">

| Name | Type | Bubbles | Composed | Description |
| ------- | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open` | `CustomEvent<undefined>` | No | Yes | The `open` event fires when the dialog opens. |
| `close` | `CustomEvent<string>` | No | Yes | The `close` event fires when the dialog closes (either via user interaction or via the API). It returns the return value inside the event's details property. |
| Name | Type | Bubbles | Composed | Description |
| -------- | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open` | `CustomEvent<undefined>` | No | Yes | The `open` event fires when the dialog opens. |
| `close` | `CustomEvent<string>` | No | Yes | The `close` event fires when the dialog closes (either via user interaction or via the API). It returns the return value inside the event's details property. |
| `cancel` | `CustomEvent<undefined>` | No | Yes | The `cancel` event fires when the user requests to close the dialog. You can prevent the dialog from closing by calling `.preventDefault()` on the event. |

</div>

Expand Down Expand Up @@ -448,3 +449,78 @@ You can use a `form` with `method=dialog` inside a dialog. This will make the di
</form>
</vwc-dialog>
```

### Confirm Closing of Dialog

```html preview 400px
<style>
vwc-text-area {
width: 100%;
}
</style>
<vwc-button label="Open Dialog" onclick="openDialog()"></vwc-button>
<vwc-dialog id="dialog" headline="Dialog" modal open>
<vwc-text-area
id="input"
slot="body"
label="Important Data"
value="Some important data"
></vwc-text-area>
<vwc-button
slot="action-items"
label="Cancel"
appearance="outlined"
onclick="closeDialog()"
></vwc-button>
<vwc-button
slot="action-items"
label="Save"
appearance="filled"
onclick="closeDialog()"
></vwc-button>
</vwc-dialog>
<vwc-dialog
id="confirm"
headline="Unsaved Changes"
subtitle="Are you sure you want to discard your changes?"
modal
>
<vwc-button
slot="action-items"
label="Cancel"
appearance="outlined"
onclick="closeConfirm()"
></vwc-button>
<vwc-button
autofocus
slot="action-items"
label="Discard"
appearance="filled"
connotation="alert"
onclick="discardChanges()"
></vwc-button>
</vwc-dialog>
<script>
document.querySelector('#dialog').addEventListener('cancel', (e) => {
e.preventDefault();
document.querySelector('#confirm').open = true;
});
function openDialog() {
document.querySelector('#dialog').open = true;
}
function closeDialog() {
document.querySelector('#dialog').open = false;
}
function closeConfirm() {
document.querySelector('#confirm').open = false;
}
function discardChanges() {
closeConfirm();
closeDialog();
}
</script>
```
64 changes: 60 additions & 4 deletions libs/components/src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ describe('vwc-dialog', () => {
await elementUpdated(element);
}

function clickDismissButton() {
(dialogEl.querySelector('.dismiss-button') as HTMLElement).click();
}

let element: Dialog;
let dialogEl: HTMLDialogElement;

Expand Down Expand Up @@ -301,6 +305,33 @@ describe('vwc-dialog', () => {
});
});

describe('cancel event', function () {
const triggerCancelEvent = clickDismissButton;

beforeEach(async () => {
await showDialog();
});

it('should prevent dialog from closing when cancelled', async () => {
element.addEventListener('cancel', (event) => {
event.preventDefault();
});

triggerCancelEvent();

expect(element.open).toEqual(true);
});

it('should not bubble', async () => {
const onCancel = jest.fn();
element.parentElement!.addEventListener('cancel', onCancel);

triggerCancelEvent();

expect(onCancel).not.toBeCalled();
});
});

describe('scrimClick', function () {
function createMouseEventOutsideTheDialog(type: string) {
return new MouseEvent(type, {
Expand Down Expand Up @@ -380,6 +411,15 @@ describe('vwc-dialog', () => {
expect(element.open).toEqual(false);
});

it('should emit a cancel event when scrim is clicked', async function () {
const cancelSpy = jest.fn();
element.addEventListener('cancel', cancelSpy);
const event = createMouseEventOutsideTheDialog('mousedown');
dialogEl.dispatchEvent(event);
await elementUpdated(element);
expect(cancelSpy).toHaveBeenCalledTimes(1);
});

it('should leave the dialog open on scrim click when no light dismiss', async function () {
element.noLightDismiss = true;
await elementUpdated(element);
Expand Down Expand Up @@ -452,15 +492,23 @@ describe('vwc-dialog', () => {
element.addEventListener('close', spy);
await showDialog();

const dismissButton = dialogEl.querySelector(
'.dismiss-button'
) as HTMLElement;
dismissButton.click();
clickDismissButton();

expect(element.open).toEqual(false);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should emit a cancel event when dismiss button is clicked', async function () {
const cancelSpy = jest.fn();
element.addEventListener('cancel', cancelSpy);
await showDialog();

clickDismissButton();

expect(element.open).toEqual(false);
expect(cancelSpy).toHaveBeenCalledTimes(1);
});

it('should preventDefault of cancel events on the dialog', async () => {
const cancelEvent = new Event('cancel');
cancelEvent.preventDefault = jest.fn();
Expand Down Expand Up @@ -554,6 +602,14 @@ describe('vwc-dialog', () => {
expect(element.open).toEqual(false);
});

it('should fire cancel event on escape key press', async function () {
const cancelSpy = jest.fn();
element.addEventListener('cancel', cancelSpy);
await showModalDialog();
await triggerEscapeKey();
expect(cancelSpy).toHaveBeenCalledTimes(1);
});

it('should remain open on escape key when not modal', async function () {
await showDialog();
await triggerEscapeKey();
Expand Down
11 changes: 2 additions & 9 deletions libs/components/src/lib/dialog/dialog.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,10 @@ function renderDismissButton(buttonTag: string) {
size="condensed"
class="dismiss-button"
icon="close-line"
@click="${(x) => (x.open = false)}"
@click="${(x) => x._handleCloseRequest()}"
></${buttonTag}>`;
}

function handleEscapeKey(dialog: Dialog, event: Event) {
if ((event as KeyboardEvent).key === 'Escape' && dialog._openedAsModal) {
dialog.open = false;
}
return true;
}

export const DialogTemplate: (
context: ElementDefinitionContext,
definition: FoundationElementDefinition
Expand All @@ -72,7 +65,7 @@ export const DialogTemplate: (
return html<Dialog>`
<${elevationTag} dp="8">
<dialog class="${getClasses}"
@keydown="${(x, c) => handleEscapeKey(x, c.event)}"
@keydown="${(x, c) => x._onKeyDown(c.event as KeyboardEvent)}"
@cancel="${(_, c) => c.event.preventDefault()}"
aria-label="${(x) => x.ariaLabel}"
?aria-modal="${(x) => x._openedAsModal}"
Expand Down
30 changes: 29 additions & 1 deletion libs/components/src/lib/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type IconPlacement = 'top' | 'side';
* @slot action-items - Use the action-items slot in order to add action buttons to the bottom of the dialog.
* @event {CustomEvent<undefined>} open - The `open` event fires when the dialog opens.
* @event {CustomEvent<string>} close - The `close` event fires when the dialog closes (either via user interaction or via the API). It returns the return value inside the event's details property.
* @event {CustomEvent<undefined>} cancel - The `cancel` event fires when the user requests to close the dialog. You can prevent the dialog from closing by calling `.preventDefault()` on the event.
* @vueModel open open open,close `(event.target as any).open`
*/
export class Dialog extends FoundationElement {
Expand Down Expand Up @@ -146,7 +147,9 @@ export class Dialog extends FoundationElement {
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width;

this.open = clickedInDialog;
if (!clickedInDialog) {
this._handleCloseRequest();
}
};

#handleInternalFormSubmit = (event: SubmitEvent) => {
Expand All @@ -157,6 +160,31 @@ export class Dialog extends FoundationElement {
this.open = false;
};

/**
* @internal
*/
_onKeyDown(event: KeyboardEvent) {
if ((event as KeyboardEvent).key === 'Escape' && this._openedAsModal) {
this._handleCloseRequest();
return false;
}
return true;
}

/**
* @internal
*/
_handleCloseRequest() {
if (
this.$emit('cancel', undefined, {
bubbles: false,
cancelable: true,
})
) {
this.open = false;
}
}

close() {
this.open = false;
}
Expand Down
9 changes: 5 additions & 4 deletions libs/components/src/lib/side-drawer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ The `app-content` slot sets assigned nodes to the main application content, the

<div class="table-wrapper">

| Name | Type | Bubbles | Composed | Description |
| ------- | ------------------------ | ------- | -------- | ----------------------------- |
| `open` | `CustomEvent<undefined>` | No | Yes | Fired when the menu is opened |
| `close` | `CustomEvent<undefined>` | No | Yes | Fired when the menu is closed |
| Name | Type | Bubbles | Composed | Description |
| -------- | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `open` | `CustomEvent<undefined>` | No | Yes | Fired when the side drawer is opened. |
| `close` | `CustomEvent<undefined>` | No | Yes | Fired when the side drawer is closed. |
| `cancel` | `CustomEvent<undefined>` | No | Yes | Fired when the user requests to close the side-drawer. You can prevent the side drawer from closing by calling `.preventDefault()` on the event. |

</div>

Expand Down
54 changes: 50 additions & 4 deletions libs/components/src/lib/side-drawer/side-drawer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const COMPONENT_TAG = 'vwc-side-drawer';
describe('vwc-side-drawer', () => {
let element: SideDrawer;

const clickOnScrim = () =>
(element.shadowRoot?.querySelector('.scrim') as HTMLElement).click();

beforeEach(async () => {
element = (await fixture(`<${COMPONENT_TAG}>
</${COMPONENT_TAG}>`)) as SideDrawer;
Expand Down Expand Up @@ -184,16 +187,53 @@ describe('vwc-side-drawer', () => {
});
});

describe('cancel event', function () {
const triggerCancelEvent = clickOnScrim;

beforeEach(async () => {
element.modal = true;
element.open = true;
await elementUpdated(element);
});

it('should prevent side drawer from closing when cancelled', async () => {
element.addEventListener('cancel', (event) => {
event.preventDefault();
});

triggerCancelEvent();

expect(element.open).toEqual(true);
});

it('should not bubble', async () => {
const onCancel = jest.fn();
element.parentElement!.addEventListener('cancel', onCancel);

triggerCancelEvent();

expect(onCancel).not.toBeCalled();
});
});

describe('scrim', () => {
it('should close after clicking on scrim', async () => {
element.modal = true;
element.open = true;
await elementUpdated(element);
const scrim: any = element.shadowRoot?.querySelector('.scrim');
scrim?.click();
await elementUpdated(element);
clickOnScrim();
expect(element.open).toEqual(false);
});

it('should emit cancel event after clicking on scrim', async () => {
const cancelSpy = jest.fn();
element.addEventListener('cancel', cancelSpy);
element.modal = true;
element.open = true;
await elementUpdated(element);
clickOnScrim();
expect(cancelSpy).toHaveBeenCalledTimes(1);
});
});

describe('keydown', () => {
Expand All @@ -208,10 +248,16 @@ describe('vwc-side-drawer', () => {

it('should close after keydown on Escape', async () => {
control.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
await elementUpdated(element);
expect(element.open).toEqual(false);
});

it('should emit cancel after keydown on Escape', async () => {
const cancelSpy = jest.fn();
element.addEventListener('cancel', cancelSpy);
control.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(cancelSpy).toHaveBeenCalledTimes(1);
});

it('should leave open after keydown that is not Escape', async () => {
control.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(element.open).toEqual(true);
Expand Down
Loading

0 comments on commit 0de40cc

Please sign in to comment.