diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index 47f36825dfd..a664d928d58 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -1876,6 +1876,10 @@ export namespace Components { * When provided, the method will be called before it is removed from its parent `calcite-flow`. */ "beforeBack": () => Promise; + /** + * Passes a function to run before the component closes. + */ + "beforeClose": () => Promise; /** * When `true`, displays a close button in the trailing side of the component's header. */ @@ -3672,6 +3676,10 @@ export namespace Components { "totalItems": number; } interface CalcitePanel { + /** + * Passes a function to run before the component closes. + */ + "beforeClose": () => Promise; /** * When `true`, displays a close button in the trailing side of the header. */ @@ -9733,6 +9741,10 @@ declare namespace LocalJSX { * When provided, the method will be called before it is removed from its parent `calcite-flow`. */ "beforeBack"?: () => Promise; + /** + * Passes a function to run before the component closes. + */ + "beforeClose"?: () => Promise; /** * When `true`, displays a close button in the trailing side of the component's header. */ @@ -11630,6 +11642,10 @@ declare namespace LocalJSX { "totalItems"?: number; } interface CalcitePanel { + /** + * Passes a function to run before the component closes. + */ + "beforeClose"?: () => Promise; /** * When `true`, displays a close button in the trailing side of the header. */ diff --git a/packages/calcite-components/src/components/flow-item/flow-item.e2e.ts b/packages/calcite-components/src/components/flow-item/flow-item.e2e.ts index f6da9a4c6e2..aa342c33c42 100644 --- a/packages/calcite-components/src/components/flow-item/flow-item.e2e.ts +++ b/packages/calcite-components/src/components/flow-item/flow-item.e2e.ts @@ -25,6 +25,10 @@ describe("calcite-flow-item", () => { describe("defaults", () => { defaults("calcite-flow-item", [ + { + propertyName: "beforeClose", + defaultValue: undefined, + }, { propertyName: "closable", defaultValue: false, @@ -199,6 +203,24 @@ describe("calcite-flow-item", () => { expect(calciteFlowItemBack).toHaveReceivedEvent(); }); + it("sets beforeClose on internal panel", async () => { + const page = await newE2EPage(); + await page.exposeFunction("beforeClose", () => Promise.reject()); + await page.setContent(""); + + await page.$eval( + "calcite-flow-item", + (el: HTMLCalciteFlowItemElement) => + (el.beforeClose = (window as typeof window & Pick).beforeClose), + ); + + await page.waitForChanges(); + + const panel = await page.find(`calcite-flow-item >>> calcite-panel`); + + expect(await panel.getProperty("beforeClose")).toBeDefined(); + }); + it("sets collapsible and collapsed on internal panel", async () => { const page = await newE2EPage(); diff --git a/packages/calcite-components/src/components/flow-item/flow-item.tsx b/packages/calcite-components/src/components/flow-item/flow-item.tsx index cdf1b1e9543..ac7f231ceea 100644 --- a/packages/calcite-components/src/components/flow-item/flow-item.tsx +++ b/packages/calcite-components/src/components/flow-item/flow-item.tsx @@ -99,6 +99,9 @@ export class FlowItem */ @Prop() beforeBack: () => Promise; + /** Passes a function to run before the component closes. */ + @Prop() beforeClose: () => Promise; + /** A description for the component. */ @Prop() description: string; @@ -365,11 +368,13 @@ export class FlowItem menuOpen, messages, overlayPositioning, + beforeClose, } = this; return ( { describe("defaults", () => { defaults("calcite-panel", [ + { + propertyName: "beforeClose", + defaultValue: undefined, + }, { propertyName: "widthScale", defaultValue: undefined, @@ -129,6 +133,49 @@ describe("calcite-panel", () => { expect(await container.isVisible()).toBe(false); }); + it("should handle rejected 'beforeClose' promise'", async () => { + const page = await newE2EPage(); + + const mockCallBack = jest.fn().mockReturnValue(() => Promise.reject()); + await page.exposeFunction("beforeClose", mockCallBack); + + await page.setContent(``); + + await page.$eval( + "calcite-panel", + (el: HTMLCalcitePanelElement) => + (el.beforeClose = (window as typeof window & Pick).beforeClose), + ); + await page.waitForChanges(); + + const panel = await page.find("calcite-panel"); + expect(await panel.getProperty("closed")).toBe(false); + panel.setProperty("closed", true); + await page.waitForChanges(); + + expect(mockCallBack).toHaveBeenCalledTimes(1); + }); + + it("should remain open with rejected 'beforeClose' promise'", async () => { + const page = await newE2EPage(); + + await page.exposeFunction("beforeClose", () => Promise.reject()); + await page.setContent(``); + + await page.$eval( + "calcite-panel", + (el: HTMLCalcitePanelElement) => + (el.beforeClose = (window as typeof window & Pick).beforeClose), + ); + + const panel = await page.find("calcite-panel"); + panel.setProperty("closed", true); + await page.waitForChanges(); + + expect(await panel.getProperty("closed")).toBe(false); + expect(panel.getAttribute("closed")).toBe(null); // Makes sure attribute is added back + }); + it("honors collapsed & collapsible properties", async () => { const page = await newE2EPage(); diff --git a/packages/calcite-components/src/components/panel/panel.tsx b/packages/calcite-components/src/components/panel/panel.tsx index 1e63dc7ac15..1d8a959cd89 100644 --- a/packages/calcite-components/src/components/panel/panel.tsx +++ b/packages/calcite-components/src/components/panel/panel.tsx @@ -77,9 +77,17 @@ export class Panel // // -------------------------------------------------------------------------- + /** Passes a function to run before the component closes. */ + @Prop() beforeClose: () => Promise; + /** When `true`, the component will be hidden. */ @Prop({ mutable: true, reflect: true }) closed = false; + @Watch("closed") + toggleDialog(value: boolean): void { + value ? this.close() : this.open(); + } + /** * When `true`, interaction is prevented and the component is displayed with lower opacity. */ @@ -206,6 +214,8 @@ export class Panel resizeObserver = createObserver("resize", () => this.resizeHandler()); + @State() isClosed = false; + @State() hasStartActions = false; @State() hasEndActions = false; @@ -288,16 +298,36 @@ export class Panel panelKeyDownHandler = (event: KeyboardEvent): void => { if (this.closable && event.key === "Escape" && !event.defaultPrevented) { - this.close(); + this.closed = true; event.preventDefault(); } }; - close = (): void => { + private handleCloseClick = (): void => { this.closed = true; this.calcitePanelClose.emit(); }; + open = (): void => { + this.isClosed = false; + }; + + close = async (): Promise => { + const beforeClose = this.beforeClose ?? (() => Promise.resolve()); + + try { + await beforeClose(); + } catch (_error) { + // close prevented + requestAnimationFrame(() => { + this.closed = false; + }); + return; + } + + this.isClosed = true; + }; + collapse = (): void => { this.collapsed = !this.collapsed; this.calcitePanelToggle.emit(); @@ -486,7 +516,7 @@ export class Panel aria-label={close} data-test="close" icon={ICONS.close} - onClick={this.close} + onClick={this.handleCloseClick} scale={this.scale} text={close} title={close} @@ -651,13 +681,13 @@ export class Panel } render(): VNode { - const { disabled, loading, panelKeyDownHandler, closed, closable } = this; + const { disabled, loading, panelKeyDownHandler, isClosed, closable } = this; const panelNode = (