Skip to content

fix(input-time-zone): fix inconsistent open/close events when opened programmatically #9864

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

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
formAssociated,
hidden,
labelable,
openClose,
reflects,
renders,
t9n,
Expand Down Expand Up @@ -123,6 +124,10 @@ describe("calcite-input-time-zone", () => {
});
});

describe("openClose", () => {
openClose(simpleTestProvider);
});

describe("t9n", () => {
t9n(simpleTestProvider);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ export class InputTimeZone
/** When `true`, displays and positions the component. */
@Prop({ mutable: true, reflect: true }) open = false;

@Watch("open")
openChanged(value: boolean): void {
this.comboboxEl.open = value;
}

/**
* Determines the type of positioning to use for the overlaid content.
*
Expand Down Expand Up @@ -338,6 +343,7 @@ export class InputTimeZone

private setComboboxRef = (el: HTMLCalciteComboboxElement): void => {
this.comboboxEl = el;
this.comboboxEl.open = this.open;
};

private onComboboxBeforeClose = (event: CustomEvent): void => {
Expand Down Expand Up @@ -485,7 +491,6 @@ export class InputTimeZone
onCalciteComboboxChange={this.onComboboxChange}
onCalciteComboboxClose={this.onComboboxClose}
onCalciteComboboxOpen={this.onComboboxOpen}
open={this.open}
overlayPositioning={this.overlayPositioning}
placeholder={
this.mode === "name" ? this.messages.namePlaceholder : this.messages.offsetPlaceholder
Expand Down
98 changes: 45 additions & 53 deletions packages/calcite-components/src/tests/commonTests/openClose.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { E2EPage } from "@stencil/core/testing";
import { toHaveNoViolations } from "jest-axe";
import { GlobalTestProps, newProgrammaticE2EPage, skipAnimations } from "../utils";
import { getTag, simplePageSetup } from "./utils";
import { TagOrHTML } from "./interfaces";
import { GlobalTestProps, toProgrammaticE2EPage, skipAnimations } from "../utils";
import { getTagAndPage } from "./utils";
import { ComponentTag, ComponentTestSetup } from "./interfaces";

expect.extend(toHaveNoViolations);

Expand Down Expand Up @@ -67,58 +67,25 @@ interface OpenCloseOptions {
* }
* })
*
* @param componentTagOrHTML - The component tag or HTML markup to test against.
* @param {ComponentTestSetup} componentTestSetup - A component tag, html, or the tag and e2e page for setting up a test.
* @param {object} [options] - Additional options to assert.
*/

export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOptions): void {
export function openClose(componentTestSetup: ComponentTestSetup, options?: OpenCloseOptions): void {
const defaultOptions: OpenCloseOptions = {
initialToggleValue: false,
openPropName: "open",
};
const customizedOptions = { ...defaultOptions, ...options };

type EventOrderWindow = GlobalTestProps<{ events: string[] }>;
const eventSequence = setUpEventSequence(componentTagOrHTML);

function setUpEventSequence(componentTagOrHTML: TagOrHTML): string[] {
const tag = getTag(componentTagOrHTML);

async function testOpenCloseEvents(tag: ComponentTag, page: E2EPage): Promise<void> {
const camelCaseTag = tag.replace(/-([a-z])/g, (lettersAfterHyphen) => lettersAfterHyphen[1].toUpperCase());
const eventSuffixes = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`];

return eventSuffixes.map((suffix) => `${camelCaseTag}${suffix}`);
}

async function setUpPage(componentTagOrHTML: TagOrHTML, page: E2EPage): Promise<void> {
await page.evaluate(
(eventSequence: string[], initialToggleValue: boolean, openPropName: string, componentTagOrHTML: string) => {
const receivedEvents: string[] = [];

(window as EventOrderWindow).events = receivedEvents;
const eventSequence = [`BeforeOpen`, `Open`, `BeforeClose`, `Close`].map((suffix) => `${camelCaseTag}${suffix}`);

eventSequence.forEach((eventType) => {
document.addEventListener(eventType, (event) => receivedEvents.push(event.type));
});
await setUpPage(tag, page);

if (!initialToggleValue) {
return;
}

const component = document.createElement(componentTagOrHTML);
component[openPropName] = true;

document.body.append(component);
},
eventSequence,
customizedOptions.initialToggleValue,
customizedOptions.openPropName,
componentTagOrHTML,
);
}

async function testOpenCloseEvents(componentTagOrHTML: TagOrHTML, page: E2EPage): Promise<void> {
const tag = getTag(componentTagOrHTML);
const element = await page.find(tag);

const [beforeOpenEvent, openEvent, beforeCloseEvent, closeEvent] = eventSequence.map((event) =>
Expand Down Expand Up @@ -164,6 +131,33 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti
expect(openSpy).toHaveReceivedEventTimes(1);

expect(await page.evaluate(() => (window as EventOrderWindow).events)).toEqual(eventSequence);

async function setUpPage(tag: ComponentTag, page: E2EPage): Promise<void> {
await page.evaluate(
(eventSequence: string[], initialToggleValue: boolean, openPropName: string, tag: ComponentTag) => {
const receivedEvents: string[] = [];

(window as EventOrderWindow).events = receivedEvents;

eventSequence.forEach((eventType) => {
document.addEventListener(eventType, (event) => receivedEvents.push(event.type));
});

if (!initialToggleValue) {
return;
}

const component = document.createElement(tag);
component[openPropName] = true;

document.body.append(component);
},
eventSequence,
customizedOptions.initialToggleValue,
customizedOptions.openPropName,
tag,
);
}
}

/**
Expand All @@ -173,35 +167,33 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti

if (customizedOptions.initialToggleValue === true) {
it("emits on initialization with animations enabled", async () => {
const page = await newProgrammaticE2EPage();
const { page, tag } = await getTagAndPage(componentTestSetup);
await toProgrammaticE2EPage(page);
await skipAnimations(page);
await setUpPage(componentTagOrHTML, page);
await testOpenCloseEvents(componentTagOrHTML, page);
await testOpenCloseEvents(tag, page);
});

it("emits on initialization with animations disabled", async () => {
const page = await newProgrammaticE2EPage();
const { page, tag } = await getTagAndPage(componentTestSetup);
await toProgrammaticE2EPage(page);
await page.addStyleTag({
content: `:root { --calcite-duration-factor: 0; }`,
});
await setUpPage(componentTagOrHTML, page);
await testOpenCloseEvents(componentTagOrHTML, page);
await testOpenCloseEvents(tag, page);
});
} else {
it(`emits with animations enabled`, async () => {
const page = await simplePageSetup(componentTagOrHTML);
const { page, tag } = await getTagAndPage(componentTestSetup);
await skipAnimations(page);
await setUpPage(componentTagOrHTML, page);
await testOpenCloseEvents(componentTagOrHTML, page);
await testOpenCloseEvents(tag, page);
});

it(`emits with animations disabled`, async () => {
const page = await simplePageSetup(componentTagOrHTML);
const { page, tag } = await getTagAndPage(componentTestSetup);
await page.addStyleTag({
content: `:root { --calcite-duration-factor: 0; }`,
});
await setUpPage(componentTagOrHTML, page);
await testOpenCloseEvents(componentTagOrHTML, page);
await testOpenCloseEvents(tag, page);
});
}
}
16 changes: 16 additions & 0 deletions packages/calcite-components/src/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ export async function newProgrammaticE2EPage(): Promise<E2EPage> {
return page;
}

/**
* Clears up an existing E2E page for tests that need to work with elements programmatically.
*
* **Note**: whenever possible, use `newProgrammaticE2EPage` to create a new page instead of reusing an existing one.
*
* @param page
* @returns {Promise<E2EPage>} an e2e page
*/
export async function toProgrammaticE2EPage(page: E2EPage): Promise<E2EPage> {
await page.evaluate(() => {
document.body.innerHTML = "";
});

return page;
}

/**
* Sets CSS vars to skip animations/transitions.
*
Expand Down
Loading