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

feat(sheet, modal, popover): support stacked component sequential closing with escape #9231

Draft
wants to merge 11 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,11 @@ export class InputDatePicker
this.datePickerEl.reset();
}

/** Leverage the `focus-trap` builtin stack to handle closing a sequence of open components, instead of components handling own `escape`. */
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
onFocusTrapDeactivate(): void {
this.open = false;
}

setStartInput = (el: HTMLCalciteInputElement): void => {
this.startInput = el;
};
Expand Down Expand Up @@ -932,10 +937,6 @@ export class InputDatePicker
this.open = true;
this.focusOnOpen = true;
event.preventDefault();
} else if (key === "Escape") {
this.open = false;
event.preventDefault();
this.restoreInputFocus();
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@ export class InputTimePicker
this.calciteInputTimePickerClose.emit();
}

/** Leverage the `focus-trap` builtin stack to handle closing a sequence of open components, instead of components handling own `escape`. */
onFocusTrapDeactivate(): void {
this.open = false;
this.calciteInputEl.setFocus();
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
}

private delocalizeTimeString(value: string): string {
// we need to set the corresponding locale before parsing, otherwise it defaults to English (possible dayjs bug)
dayjs.locale(this.effectiveLocale.toLowerCase());
Expand Down Expand Up @@ -716,10 +722,6 @@ export class InputTimePicker
this.open = true;
this.focusOnOpen = true;
event.preventDefault();
} else if (key === "Escape" && this.open) {
this.open = false;
event.preventDefault();
this.calciteInputEl.setFocus();
}
};

Expand Down
20 changes: 5 additions & 15 deletions packages/calcite-components/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
EventEmitter,
h,
Host,
Listen,
Method,
Prop,
State,
Expand Down Expand Up @@ -393,20 +392,6 @@ export class Modal

@State() defaultMessages: ModalMessages;

//--------------------------------------------------------------------------
//
// Event Listeners
//
//--------------------------------------------------------------------------

@Listen("keydown", { target: "window" })
handleEscape(event: KeyboardEvent): void {
if (this.open && !this.escapeDisabled && event.key === "Escape" && !event.defaultPrevented) {
this.open = false;
event.preventDefault();
}
}

//--------------------------------------------------------------------------
//
// Events
Expand Down Expand Up @@ -498,6 +483,11 @@ export class Modal
deactivateFocusTrap(this);
}

/** Leverage the `focus-trap` builtin stack to handle closing a sequence of open components, instead of components handling own `escape`. */
onFocusTrapDeactivate(): void {
this.open = false;
}

@Watch("open")
toggleModal(value: boolean): void {
if (this.ignoreOpenChange) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,11 @@ export class Popover
deactivateFocusTrap(this);
}

/** Leverage the `focus-trap` builtin stack to handle closing a sequence of open components, instead of components handling own `escape`. */
onFocusTrapDeactivate(): void {
this.open = false;
}

storeArrowEl = (el: SVGElement): void => {
this.arrowEl = el;
this.reposition(true);
Expand Down
142 changes: 141 additions & 1 deletion packages/calcite-components/src/components/sheet/sheet.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { newE2EPage } from "@stencil/core/testing";
import { E2EElement, newE2EPage, E2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
import { focusable, renders, hidden, defaults, accessible } from "../../tests/commonTests";
import { GlobalTestProps, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils";
Expand Down Expand Up @@ -541,4 +541,144 @@ describe("calcite-sheet properties", () => {
expect(closeSpy).toHaveReceivedEventTimes(1);
});
});

const componentStack = html`
<calcite-sheet id="example-sheet" label="libero nunc" position="inline-start" display-mode="overlay">
<calcite-panel closable>
<calcite-block open heading="Preview Sheet options"> </calcite-block>
<calcite-button onClick="openComponent('example-modal')"> Open Modal from Sheet</calcite-button>
</calcite-panel>
</calcite-sheet>

<calcite-modal id="example-modal">
<div slot="content">
<p>This is an example modal that opens from a Sheet.</p>
</div>
<calcite-button slot="back" width="full" onClick="openComponent('another-modal')"
>Open Another Modal</calcite-button
>
</calcite-modal>

<calcite-modal id="another-modal">
<div slot="content" style="display: flex; flex-direction: column; gap: 12px">
<p>
This is an example of a another modal that opens from a modal. This modal an input date picker, a combobox, a
dropdown, a popover and a tooltip.
</p>
<calcite-label>
Input Date Picker
<calcite-input-date-picker value="2023-03-07" id="input-date-picker"></calcite-input-date-picker>
</calcite-label>
<calcite-label>
Input Time Picker
<calcite-input-time-picker name="calcite-input-time-picker"></calcite-input-time-picker>
</calcite-label>
<calcite-combobox
label="test"
placeholder="placeholder"
max-items="8"
selection-mode="ancestors"
style="width: 200px"
id="combobox"
>
<calcite-combobox-item value="Grand 1" text-label="Grand 1">
<calcite-combobox-item value="Parent 1" text-label="Parent 1">
<calcite-combobox-item value="Child 1" text-label="Child 1"></calcite-combobox-item>
<calcite-combobox-item value="Child 2" text-label="Child 2"></calcite-combobox-item>
</calcite-combobox-item>
</calcite-combobox-item>
</calcite-combobox>
<calcite-dropdown scale="s" width-scale="s" id="dropdown">
<calcite-button icon-end="hamburger" appearance="outline" slot="trigger">Scale S</calcite-button>
<calcite-dropdown-group group-title="View">
<calcite-dropdown-item icon-end="list-bullet" selected>List</calcite-dropdown-item>
<calcite-dropdown-item icon-end="grid">Grid</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
<calcite-popover
heading="Heading"
label="right end popover"
reference-element="popover-button"
placement="right-end"
id="popover-heading"
closable
style="width: 25vw"
id="popover"
>
<div style="padding: 0.5rem 1rem 0.75rem">
<p style="margin-top: 0">Example Popover.</p>
</div>
</calcite-popover>
<calcite-button appearance="outline" id="popover-button" icon-start="popup">Example Popover</calcite-button>
<calcite-tooltip placement="auto" reference-element="tooltip-auto-ref"> Example Tooltip </calcite-tooltip>
<calcite-button appearance="outline" id="tooltip-auto-ref">auto</calcite-button>
</div>
</calcite-modal>
<calcite-button onClick="openComponent('example-sheet')"> Open Sheet </calcite-button>
`;

it("closes a stack of open components sequentially in visual order", async () => {
const page = await newE2EPage();
await page.setContent(componentStack);
await skipAnimations(page);

async function openAndCheckVisibility(page: E2EPage, element: E2EElement) {
element.setProperty("open", true);
await page.waitForChanges();
expect(await element.isVisible()).toBe(true);
}

const sheet = await page.find("calcite-sheet");
await openAndCheckVisibility(page, sheet);

const firstModal = await page.find("#example-modal");
await openAndCheckVisibility(page, firstModal);

const secondModal = await page.find("#another-modal");
await openAndCheckVisibility(page, secondModal);

async function testInputPicker(page: E2EPage, pickerSelector: string, modal: E2EElement) {
const inputPicker = await page.find(pickerSelector);
inputPicker.click();
await page.waitForChanges();
expect(await inputPicker.getProperty("open")).toBe(true);

await page.keyboard.press("Escape");
await page.waitForChanges();
Elijbet marked this conversation as resolved.
Show resolved Hide resolved
expect(await inputPicker.getProperty("open")).toBe(false);

await page.keyboard.press("Escape");
await page.waitForChanges();
expect(await modal.isVisible()).toBe(false);

modal.setProperty("open", true);
await page.waitForChanges();
}

await testInputPicker(page, "calcite-input-date-picker", secondModal);
await testInputPicker(page, "calcite-input-time-picker", secondModal);

secondModal.setProperty("open", true);
await page.waitForChanges();

const popoverButton = await page.find("#popover-button");
popoverButton.click();
await page.waitForChanges();
const popover = await page.find("calcite-popover");
expect(await popover.getProperty("open")).toBe(true);

await page.keyboard.press("Escape");
await page.waitForChanges();
expect(await popover.getProperty("open")).toBe(false);

async function pressEscapeAndCheckVisibility(page: E2EPage, element: E2EElement, expectedVisibility: boolean) {
page.keyboard.press("Escape");
await page.waitForChanges();
expect(await element.isVisible()).toBe(expectedVisibility);
}
Elijbet marked this conversation as resolved.
Show resolved Hide resolved

await pressEscapeAndCheckVisibility(page, secondModal, false);
await pressEscapeAndCheckVisibility(page, firstModal, false);
await pressEscapeAndCheckVisibility(page, sheet, false);
});
});
20 changes: 5 additions & 15 deletions packages/calcite-components/src/components/sheet/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
EventEmitter,
h,
Host,
Listen,
Method,
Prop,
VNode,
Expand Down Expand Up @@ -218,20 +217,6 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo
this.handleMutationObserver(),
);

//--------------------------------------------------------------------------
//
// Event Listeners
//
//--------------------------------------------------------------------------

@Listen("keydown", { target: "window" })
handleEscape(event: KeyboardEvent): void {
if (this.open && !this.escapeDisabled && event.key === "Escape" && !event.defaultPrevented) {
this.open = false;
event.preventDefault();
}
}

//--------------------------------------------------------------------------
//
// Events
Expand Down Expand Up @@ -298,6 +283,11 @@ export class Sheet implements OpenCloseComponent, FocusTrapComponent, LoadableCo
deactivateFocusTrap(this);
}

/** Leverage the `focus-trap` builtin stack to handle closing a sequence of open components, instead of components handling own `escape`. */
onFocusTrapDeactivate(): void {
this.open = false;
}

private setTransitionEl = (el: HTMLDivElement): void => {
this.transitionEl = el;
this.contentId = ensureId(el);
Expand Down
85 changes: 84 additions & 1 deletion packages/calcite-components/src/demos/sheet.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

<body>
<demo-dom-swapper>
<h1 style="margin: 0 auto; text-align: center">Sheet</h1>
<!-- <h1 style="margin: 0 auto; text-align: center">Sheet</h1>
Elijbet marked this conversation as resolved.
Show resolved Hide resolved

<calcite-sheet
width-scale="l"
Expand Down Expand Up @@ -124,6 +124,89 @@ <h1 style="margin: 0 auto; text-align: center">Sheet</h1>
document.querySelectorAll("calcite-sheet").forEach((sheet) => (sheet.open = false));
document.querySelectorAll("calcite-panel").forEach((panel) => (panel.closed = false));
});
</script> -->

<calcite-sheet id="example-sheet" label="libero nunc" position="inline-start" display-mode="overlay">
<calcite-panel closable>
<calcite-block open heading="Preview Sheet options"> </calcite-block>
<calcite-button onClick="openComponent('example-modal')"> Open Modal from Sheet</calcite-button>
</calcite-panel>
</calcite-sheet>

<calcite-modal id="example-modal">
<div slot="content">
<p>This is an example modal that opens from a Sheet.</p>
</div>
<calcite-button slot="back" width="full" onClick="openComponent('another-modal')"
>Open Another Modal</calcite-button
>
</calcite-modal>

<calcite-modal id="another-modal">
<div slot="content" style="display: flex; flex-direction: column; gap: 12px">
<p>
This is an example of a another modal that opens from a modal. This modal an input date picker, a combobox,
a dropdown, a popover and a tooltip.
</p>
<calcite-label>
Input Date Picker
<calcite-input-date-picker value="2023-03-07" id="input-date-picker"></calcite-input-date-picker>
</calcite-label>
<calcite-label>
Input Time Picker
<calcite-input-time-picker name="calcite-input-time-picker"></calcite-input-time-picker>
</calcite-label>
<calcite-combobox
label="test"
placeholder="placeholder"
max-items="8"
selection-mode="ancestors"
style="width: 200px"
id="combobox"
>
<calcite-combobox-item value="Grand 1" text-label="Grand 1">
<calcite-combobox-item value="Parent 1" text-label="Parent 1">
<calcite-combobox-item value="Child 1" text-label="Child 1"></calcite-combobox-item>
<calcite-combobox-item value="Child 2" text-label="Child 2"></calcite-combobox-item>
</calcite-combobox-item>
</calcite-combobox-item>
</calcite-combobox>
<calcite-dropdown scale="s" width-scale="s" id="dropdown">
<calcite-button icon-end="hamburger" appearance="outline" slot="trigger">Scale S</calcite-button>
<calcite-dropdown-group group-title="View">
<calcite-dropdown-item icon-end="list-bullet" selected>List</calcite-dropdown-item>
<calcite-dropdown-item icon-end="grid">Grid</calcite-dropdown-item>
</calcite-dropdown-group>
</calcite-dropdown>
<calcite-popover
heading="Heading"
label="right end popover"
reference-element="heading-title-content-cta"
placement="right-end"
id="popover-heading"
closable
style="width: 25vw"
id="popover"
>
<div style="padding: 0.5rem 1rem 0.75rem">
<p style="margin-top: 0">Example Popover.</p>
</div>
</calcite-popover>
<calcite-button appearance="outline" id="heading-title-content-cta" icon-start="popup"
>Example Popover</calcite-button
>
<calcite-tooltip placement="auto" reference-element="tooltip-auto-ref"> Example Tooltip </calcite-tooltip>
<calcite-button appearance="outline" id="tooltip-auto-ref">auto</calcite-button>
</div>
</calcite-modal>

<calcite-button onClick="openComponent('example-sheet')"> Open Sheet </calcite-button>

<script>
function openComponent(id) {
const component = document.getElementById(id);
component.open = true;
}
</script>
</demo-dom-swapper>
</body>
Expand Down