diff --git a/packages/calcite-components/src/components/dialog/dialog.e2e.ts b/packages/calcite-components/src/components/dialog/dialog.e2e.ts index 3d12c48e6ce..5b89686a390 100644 --- a/packages/calcite-components/src/components/dialog/dialog.e2e.ts +++ b/packages/calcite-components/src/components/dialog/dialog.e2e.ts @@ -279,7 +279,6 @@ describe("calcite-dialog", () => { const messageOverrides = { close: "shut the front door" }; - await page.$eval("calcite-dialog", (el: Dialog["el"]) => (el.beforeClose = (window as TestWindow).beforeClose)); dialog.setProperty("closeDisabled", true); dialog.setProperty("loading", true); dialog.setProperty("menuOpen", true); @@ -304,7 +303,6 @@ describe("calcite-dialog", () => { expect(await panel.getProperty("icon")).toBe("x"); expect(await panel.getProperty("iconFlipRtl")).toBe(true); expect((await panel.getProperty("messageOverrides")).close).toBe(messageOverrides.close); - expect(await panel.getProperty("beforeClose")).toBeDefined(); }); it("outsideCloseDisabled", async () => { @@ -470,7 +468,7 @@ describe("calcite-dialog", () => { const closeButton = await page.find(`calcite-dialog >>> calcite-panel >>> #${PanelIDS.close}`); await closeButton.click(); await page.waitForChanges(); - expect(mockCallBack).toHaveBeenCalledTimes(2); + expect(mockCallBack).toHaveBeenCalledTimes(1); expect(await page.find(`calcite-dialog >>> .${CSS.containerOpen}`)).toBeNull(); }); @@ -499,7 +497,7 @@ describe("calcite-dialog", () => { await page.waitForChanges(); await closeEventSpy; - expect(mockCallBack).toHaveBeenCalledTimes(2); + expect(mockCallBack).toHaveBeenCalledTimes(1); expect(await page.find(`calcite-dialog >>> .${CSS.containerOpen}`)).toBeNull(); }); @@ -522,7 +520,7 @@ describe("calcite-dialog", () => { dialog.removeAttribute("open"); await page.waitForChanges(); - expect(mockCallBack).toHaveBeenCalledTimes(2); + expect(mockCallBack).toHaveBeenCalledTimes(1); expect(await page.find(`calcite-dialog >>> .${CSS.containerOpen}`)).toBeNull(); }); @@ -541,7 +539,7 @@ describe("calcite-dialog", () => { dialog.setProperty("open", false); await page.waitForChanges(); - expect(mockCallBack).toHaveBeenCalledTimes(2); + expect(mockCallBack).toHaveBeenCalledTimes(1); }); it("should remain open with rejected 'beforeClose' promise'", async () => { diff --git a/packages/calcite-components/src/components/dialog/dialog.scss b/packages/calcite-components/src/components/dialog/dialog.scss index 4894258340d..f94969afde3 100644 --- a/packages/calcite-components/src/components/dialog/dialog.scss +++ b/packages/calcite-components/src/components/dialog/dialog.scss @@ -193,7 +193,22 @@ calcite-panel { } .panel { - @apply rounded; + @apply invisible + rounded + opacity-0; + + transition: + visibility 0ms linear var(--calcite-internal-animation-timing-slow), + opacity var(--calcite-internal-animation-timing-slow) $easing-function; +} + +.container--open .panel { + @apply visible + opacity-100; + + transition: + visibility 0ms linear, + opacity var(--calcite-internal-animation-timing-slow) $easing-function; } .container--open { diff --git a/packages/calcite-components/src/components/dialog/dialog.tsx b/packages/calcite-components/src/components/dialog/dialog.tsx index c7432adf329..d416299a1cc 100644 --- a/packages/calcite-components/src/components/dialog/dialog.tsx +++ b/packages/calcite-components/src/components/dialog/dialog.tsx @@ -76,8 +76,6 @@ export class Dialog extends LitElement implements OpenCloseComponent { usePreventDocumentScroll = usePreventDocumentScroll()(this); - private ignoreOpenChange = false; - private interaction: Interactable; private mutationObserver: MutationObserver = createObserver("mutation", () => @@ -202,11 +200,10 @@ export class Dialog extends LitElement implements OpenCloseComponent { get open(): boolean { return this._open; } - set open(open: boolean) { - const oldOpen = this._open; - if (open !== oldOpen) { - this._open = open; - this.toggleDialog(open); + set open(value: boolean) { + const oldValue = this._open; + if (value !== oldValue) { + this.setOpenState(value); } } @@ -386,16 +383,22 @@ export class Dialog extends LitElement implements OpenCloseComponent { this.calciteDialogClose.emit(); } - private toggleDialog(value: boolean): void { - if (this.ignoreOpenChange) { - return; + private async setOpenState(value: boolean): Promise { + if (this.beforeClose && !value) { + try { + await this.beforeClose?.(); + } catch { + return; + } } + this._open = value; + if (value) { - this.openDialog(); - } else { - this.closeDialog(); + await this.componentOnReady(); } + + this.opened = value; } private handleOpenedChange(value: boolean): void { @@ -713,6 +716,7 @@ export class Dialog extends LitElement implements OpenCloseComponent { return; } + event.preventDefault(); event.stopPropagation(); this.open = false; } @@ -723,11 +727,6 @@ export class Dialog extends LitElement implements OpenCloseComponent { } } - private async openDialog(): Promise { - await this.componentOnReady(); - this.opened = true; - } - private handleOutsideClose(): void { if (this.outsideCloseDisabled) { return; @@ -736,24 +735,6 @@ export class Dialog extends LitElement implements OpenCloseComponent { this.open = false; } - private async closeDialog(): Promise { - if (this.beforeClose) { - try { - await this.beforeClose(); - } catch { - // close prevented - requestAnimationFrame(() => { - this.ignoreOpenChange = true; - this.open = true; - this.ignoreOpenChange = false; - }); - return; - } - } - - this.opened = false; - } - private handleMutationObserver(): void { this.focusTrap.updateContainerElements(); } @@ -795,10 +776,8 @@ export class Dialog extends LitElement implements OpenCloseComponent { Promise; }>; +type TestPanelWindow = GlobalTestProps<{ + lastEventCancelable: boolean; + lastEventDefaultPrevented: boolean; + calledTimes: number; +}>; + const panelTemplate = (scrollable = false) => html`
@@ -360,6 +366,43 @@ describe("calcite-panel", () => { expect(calcitePanelClose).toHaveReceivedEventTimes(1); }); + it("close event can be cancelled", async () => { + const page = await newProgrammaticE2EPage(); + await page.evaluate(() => { + (window as TestPanelWindow).calledTimes = 0; + + const panel = document.createElement("calcite-panel"); + panel.heading = "Hello World"; + panel.closable = true; + panel.innerText = "Hello World"; + + panel.addEventListener("calcitePanelClose", (event) => { + event.preventDefault(); + // needed to work around event spy limitation - details are captured before event is canceled + (window as TestPanelWindow).lastEventCancelable = event.cancelable; + (window as TestPanelWindow).lastEventDefaultPrevented = event.defaultPrevented; + (window as TestPanelWindow).calledTimes++; + }); + + document.body.append(panel); + }); + await page.waitForChanges(); + + const panel = await page.find("calcite-panel"); + const closeButton = await page.find(`calcite-panel >>> #${IDS.close}`); + await closeButton.click(); + await page.waitForChanges(); + + const calledTimes = await page.evaluate(() => (window as TestPanelWindow).calledTimes); + const lastEventCancelable = await page.evaluate(() => (window as TestPanelWindow).lastEventCancelable); + const lastEventDefaultPrevented = await page.evaluate(() => (window as TestPanelWindow).lastEventDefaultPrevented); + + expect(calledTimes).toBe(1); + expect(lastEventCancelable).toBe(true); + expect(lastEventDefaultPrevented).toBe(true); + expect(await panel.getProperty("closed")).toBe(false); + }); + it("toggle event should fire when collapsed", async () => { const page = await newE2EPage(); await page.setContent("Hello World!"); diff --git a/packages/calcite-components/src/components/panel/panel.tsx b/packages/calcite-components/src/components/panel/panel.tsx index b64d822567a..e3a198fa731 100644 --- a/packages/calcite-components/src/components/panel/panel.tsx +++ b/packages/calcite-components/src/components/panel/panel.tsx @@ -1,5 +1,4 @@ // @ts-strict-ignore -import { PropertyValues } from "lit"; import { LitElement, property, createEvent, h, method, state, JsxNode } from "@arcgis/lumina"; import { focusFirstTabbable, @@ -74,6 +73,8 @@ export class Panel extends LitElement implements InteractiveComponent { */ messages = useT9n(); + private _closed = false; + //#endregion //#region State Properties @@ -102,8 +103,6 @@ export class Panel extends LitElement implements InteractiveComponent { @state() hasStartActions = false; - @state() isClosed = false; - @state() showHeaderContent = false; //#endregion @@ -117,7 +116,16 @@ export class Panel extends LitElement implements InteractiveComponent { @property({ reflect: true }) closable = false; /** When `true`, the component will be hidden. */ - @property({ reflect: true }) closed = false; + @property({ reflect: true }) + get closed(): boolean { + return this._closed; + } + set closed(value: boolean) { + const oldValue = this._closed; + if (value !== oldValue) { + this.setClosedState(value); + } + } /** * Specifies the direction of the collapse. @@ -210,7 +218,7 @@ export class Panel extends LitElement implements InteractiveComponent { //#region Events /** Fires when the close button is clicked. */ - calcitePanelClose = createEvent({ cancelable: false }); + calcitePanelClose = createEvent({ cancelable: true }); /** Fires when the content is scrolled. */ calcitePanelScroll = createEvent({ cancelable: false }); @@ -225,24 +233,7 @@ export class Panel extends LitElement implements InteractiveComponent { constructor() { super(); this.listen("keydown", this.panelKeyDownHandler); - } - - async load(): Promise { - this.isClosed = this.closed; - } - - override willUpdate(changes: PropertyValues): void { - /* TODO: [MIGRATION] First time Lit calls willUpdate(), changes will include not just properties provided by the user, but also any default values your component set. - To account for this semantics change, the checks for (this.hasUpdated || value != defaultValue) was added in this method - Please refactor your code to reduce the need for this check. - Docs: https://qawebgis.esri.com/arcgis-components/?path=/docs/lumina-transition-from-stencil--docs#watching-for-property-changes */ - if (changes.has("closed") && this.hasUpdated) { - if (this.closed) { - this.close(); - } else { - this.open(); - } - } + this.listen("calcitePanelClose", this.panelCloseHandler); } override updated(): void { @@ -257,6 +248,18 @@ export class Panel extends LitElement implements InteractiveComponent { //#region Private Methods + private async setClosedState(value: boolean): Promise { + if (this.beforeClose && value) { + try { + await this.beforeClose?.(); + } catch { + return; + } + } + + this._closed = value; + } + private resizeHandler(): void { const { panelScrollEl } = this; @@ -282,36 +285,27 @@ export class Panel extends LitElement implements InteractiveComponent { this.containerEl = node; } - private panelKeyDownHandler(event: KeyboardEvent): void { - if (this.closable && event.key === "Escape" && !event.defaultPrevented) { - this.handleUserClose(); - event.preventDefault(); - } + private closeClickHandler(): void { + this.emitCloseEvent(); } - private handleUserClose(): void { - this.closed = true; + private emitCloseEvent(): void { this.calcitePanelClose.emit(); } - private open(): void { - this.isClosed = false; + private panelKeyDownHandler(event: KeyboardEvent): void { + if (this.closable && event.key === "Escape" && !event.defaultPrevented) { + this.emitCloseEvent(); + event.preventDefault(); + } } - private async close(): Promise { - const beforeClose = this.beforeClose ?? (() => Promise.resolve()); - - try { - await beforeClose(); - } catch { - // close prevented - requestAnimationFrame(() => { - this.closed = false; - }); + private panelCloseHandler(event: CustomEvent): void { + if (event.defaultPrevented) { return; } - this.isClosed = true; + this.closed = true; } private collapse(): void { @@ -501,7 +495,7 @@ export class Panel extends LitElement implements InteractiveComponent { ariaLabel={close} icon={ICONS.close} id={IDS.close} - onClick={this.handleUserClose} + onClick={this.closeClickHandler} scale={this.scale} text={close} title={close} @@ -664,15 +658,10 @@ export class Panel extends LitElement implements InteractiveComponent { } override render(): JsxNode { - const { disabled, loading, isClosed } = this; + const { disabled, loading, closed } = this; const panelNode = ( -
+ +
+
closable with event prevented
+
+ + + +

+ Enim nascetur erat faucibus ornare varius arcu fames bibendum habitant felis elit ante. Nibh morbi massa + curae; leo semper diam aenean congue taciti eu porta. Varius faucibus ridiculus donec. Montes sit ligula + purus porta ante lacus habitasse libero cubilia purus! In quis congue arcu maecenas felis cursus + pellentesque nascetur porta donec non. Quisque, rutrum ligula pharetra justo habitasse facilisis rutrum + neque. Magnis nostra nec nulla dictumst taciti consectetur. Non porttitor tempor orci dictumst magna porta + vitae. +

+

+ Ipsum nostra tempus etiam augue ullamcorper scelerisque sapien potenti erat nisi gravida. Vehicula sem + tristique sed. Nullam, sociis imperdiet ullamcorper? Dapibus fames primis ridiculus vulputate, habitant + inceptos! Nunc torquent lorem urna vehicula volutpat donec nec. Orci massa eu nec donec enim fames, + faucibus quam aenean. Laoreet tellus tempor quisque ornare lobortis praesent erat senectus natoque + consectetur donec imperdiet. Quis sem cum gravida dictumst a pretium purus aptent amet id. Orci habitasse, + praesent facilisis condimentum. Nec elit turpis leo. +

+

+ Tempus per volutpat diam tempor mauris parturient vulputate leo id libero quisque. Mattis aliquam dictum + venenatis fringilla. Taciti venenatis, ultrices sollicitudin consequat. Sapien fusce est iaculis potenti + ut auctor potenti. Nisi malesuada feugiat vulputate vitae porttitor. Nullam nullam nullam accumsan quis + magna in. Elementum, nascetur gravida cras scelerisque inceptos aenean inceptos potenti. Lobortis + condimentum accumsan posuere curabitur fermentum diam, natoque quisque. Eget placerat sed aptent orci urna + fusce magnis. Vel lacus magnis nunc. +

+
+ +
+
+
header with actions