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(input-time-zone): add time zone offset and name mode #7913

Merged
merged 14 commits into from
Oct 3, 2023
19 changes: 17 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/calcite-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"focus-trap": "7.5.2",
"form-request-submit-polyfill": "2.0.0",
"lodash-es": "4.17.21",
"sortablejs": "1.15.0"
"sortablejs": "1.15.0",
"timezone-groups": "0.1.0"
},
"devDependencies": {
"@esri/calcite-design-tokens": "1.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
import {
accessible,
defaults,
Expand All @@ -11,105 +12,205 @@ import {
renders,
t9n,
} from "../../tests/commonTests";
import { html } from "../../../support/formatting";
import { toGMTLabel } from "./utils";
import { toUserFriendlyName } from "./utils";

describe("calcite-input-time-zone", () => {
describe("accessible", () => {
accessible("calcite-input-time-zone");
accessible(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`));
});

describe("focusable", () => {
focusable("calcite-input-time-zone");
focusable(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`));
});

describe("formAssociated", () => {
formAssociated("calcite-input-time-zone", { testValue: "-360", clearable: false });
formAssociated(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`), {
testValue: "-360",
clearable: false,
});
});

describe("hidden", () => {
hidden("calcite-input-time-zone");
hidden(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`));
});

describe("renders", () => {
renders("calcite-input-time-zone", { display: "block" });
renders(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`), { display: "block" });
});

describe("labelable", () => {
labelable("calcite-input-time-zone");
describe.skip("labelable", () => {
labelable(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`));
});

describe("reflects", () => {
reflects("calcite-input-time-zone", [
reflects(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`), [
{ propertyName: "disabled", value: true },
{ propertyName: "maxItems", value: 0 },
{ propertyName: "mode", value: "offset" },
{ propertyName: "open", value: true },
{ propertyName: "scale", value: "m" },
{ propertyName: "overlayPositioning", value: "absolute" },
]);
});

describe("defaults", () => {
defaults("calcite-input-time-zone", [
defaults(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`), [
{ propertyName: "disabled", defaultValue: false },
{ propertyName: "maxItems", defaultValue: 0 },
{ propertyName: "messageOverrides", defaultValue: undefined },
{ propertyName: "mode", defaultValue: "offset" },
{ propertyName: "open", defaultValue: false },
{ propertyName: "overlayPositioning", defaultValue: "absolute" },
{ propertyName: "scale", defaultValue: "m" },
]);
});

describe("disabled", () => {
disabled("calcite-input-time-zone", { shadowAriaAttributeTargetSelector: "calcite-combobox" });
disabled(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`), {
shadowAriaAttributeTargetSelector: "calcite-combobox",
});
});

describe("t9n", () => {
t9n("calcite-input-time-zone");
t9n(addTimeZoneNamePolyfill(html` <calcite-input-time-zone></calcite-input-time-zone>`));
});

describe("selects user's matching timezone offset by default", () => {
const timeZoneNamesAndOffsets = [
{ name: "America/Los_Angeles", offset: -420 },
{ name: "Europe/London", offset: 60 },
];
const testTimeZoneNamesAndOffsets = [
{ name: "America/Los_Angeles", offset: -420, label: "GMT-7" },
{ name: "America/Denver", offset: -360, label: "GMT-6" },
{ name: "Europe/London", offset: 60, label: "GMT+1" },
];

describe("mode", () => {
describe("offset (default)", () => {
describe("selects user's matching time zone offset on initialization", () => {
testTimeZoneNamesAndOffsets.forEach(({ name, offset, label }) => {
it(`selects default time zone for "${name}"`, async () => {
const page = await newE2EPage();
await page.emulateTimezone(name);
await page.setContent(addTimeZoneNamePolyfill(html`<calcite-input-time-zone></calcite-input-time-zone>`));
await page.waitForChanges();

const input = await page.find("calcite-input-time-zone");
expect(await input.getProperty("value")).toBe(`${offset}`);

const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

timeZoneNamesAndOffsets.forEach(({ name, offset }) => {
it(`selects default timezone for "${name}"`, async () => {
expect(await timeZoneItem.getProperty("textLabel")).toMatch(label);
});
});
});

it("allows users to preselect a time zone offset", async () => {
const page = await newE2EPage();
await page.emulateTimezone(name);
await page.setContent(html`<calcite-input-time-zone></calcite-input-time-zone>`);
await page.waitForTimeout(1000);
await page.emulateTimezone(testTimeZoneNamesAndOffsets[0].name);
await page.setContent(
await addTimeZoneNamePolyfill(
html` <calcite-input-time-zone value="${testTimeZoneNamesAndOffsets[1].offset}"></calcite-input-time-zone>`
)
);

const input = await page.find("calcite-input-time-zone");

expect(await input.getProperty("value")).toBe(`${offset}`);
expect(await input.getProperty("value")).toBe(`${testTimeZoneNamesAndOffsets[1].offset}`);

const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

expect(await timeZoneItem.getProperty("textLabel")).toMatch(toGMTLabel(offset / 60));
expect(await timeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneNamesAndOffsets[1].label);
});

it("ignores invalid values", async () => {
const page = await newE2EPage();
await page.emulateTimezone(testTimeZoneNamesAndOffsets[0].name);
await page.setContent(
await addTimeZoneNamePolyfill(html` <calcite-input-time-zone value="9000"></calcite-input-time-zone>`)
);

const input = await page.find("calcite-input-time-zone");

expect(await input.getProperty("value")).toBe(`${testTimeZoneNamesAndOffsets[0].offset}`);

const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

expect(await timeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneNamesAndOffsets[0].label);
});
});
});

it("allows users to preselect a timezone offset", async () => {
const page = await newE2EPage();
await page.emulateTimezone("America/Los_Angeles");
await page.setContent(html`<calcite-input-time-zone value="-360"></calcite-input-time-zone>`);
describe("name", () => {
describe("selects user's matching time zone name on initialization", () => {
testTimeZoneNamesAndOffsets.forEach(({ name }) => {
it(`selects default time zone for "${name}"`, async () => {
const page = await newE2EPage();
await page.emulateTimezone(name);
await page.setContent(
await addTimeZoneNamePolyfill(html` <calcite-input-time-zone mode="name"></calcite-input-time-zone>`)
);
await page.waitForChanges();

const input = await page.find("calcite-input-time-zone");
const input = await page.find("calcite-input-time-zone");
expect(await input.getProperty("value")).toBe(name);

expect(await input.getProperty("value")).toBe("-360");
const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

expect(await timeZoneItem.getProperty("textLabel")).toMatch(toUserFriendlyName(name));
});
});
});

it("allows users to preselect a time zone by name", async () => {
const page = await newE2EPage();
await page.emulateTimezone(testTimeZoneNamesAndOffsets[0].name);
await page.setContent(
await addTimeZoneNamePolyfill(html` <calcite-input-time-zone
mode="name"
value="${testTimeZoneNamesAndOffsets[1].name}"
></calcite-input-time-zone>`)
);

const input = await page.find("calcite-input-time-zone");

const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
expect(await input.getProperty("value")).toBe(testTimeZoneNamesAndOffsets[1].name);

expect(await timeZoneItem.getProperty("textLabel")).toMatch("GMT-6");
const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

expect(await timeZoneItem.getProperty("textLabel")).toMatch(
toUserFriendlyName(testTimeZoneNamesAndOffsets[1].name)
);
});

it("ignores invalid values", async () => {
const page = await newE2EPage();
await page.emulateTimezone(testTimeZoneNamesAndOffsets[0].name);
await page.setContent(
await addTimeZoneNamePolyfill(html` <calcite-input-time-zone
mode="name"
value="Does/Not/Exist"
></calcite-input-time-zone>`)
);

const input = await page.find("calcite-input-time-zone");

expect(await input.getProperty("value")).toBe(testTimeZoneNamesAndOffsets[0].name);

const timeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");

expect(await timeZoneItem.getProperty("textLabel")).toMatch(
toUserFriendlyName(testTimeZoneNamesAndOffsets[0].name)
);
});
});
});

it("does not allow users to deselect a timezone offset", async () => {
it("does not allow users to deselect a time zone offset", async () => {
const page = await newE2EPage();
await page.emulateTimezone("America/Los_Angeles");
await page.setContent(html`<calcite-input-time-zone value="-360" open></calcite-input-time-zone>`);
await page.emulateTimezone(testTimeZoneNamesAndOffsets[0].name);
await page.setContent(
addTimeZoneNamePolyfill(
html`
<calcite-input-time-zone value="${testTimeZoneNamesAndOffsets[1].offset}" open></calcite-input-time-zone>
`
)
);
await page.waitForChanges();

let selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
Expand All @@ -119,17 +220,86 @@ describe("calcite-input-time-zone", () => {
selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
const input = await page.find("calcite-input-time-zone");

expect(await input.getProperty("value")).toBe("-360");
expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch("GMT-6");
expect(await input.getProperty("value")).toBe(`${testTimeZoneNamesAndOffsets[1].offset}`);
expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneNamesAndOffsets[1].label);
});

it("supports setting maxItems to display", async () => {
const page = await newE2EPage();
await page.setContent(html`<calcite-input-time-zone max-items="7"></calcite-input-time-zone>`);

await page.setContent(
addTimeZoneNamePolyfill(html` <calcite-input-time-zone max-items="7"></calcite-input-time-zone>`)
);
const internalCombobox = await page.find("calcite-input-time-zone >>> calcite-combobox");

// we assume maxItems works properly on combobox
expect(await internalCombobox.getProperty("maxItems")).toBe(7);
});
});

/**
* Helper to inject an Intl polyfill to support time zone-related APIs
* Extended due to lack of support for "Intl.DateTimeFormatOptions#timeZoneName" in Chromium v92 (bundled in Puppeteer v10).
*
* @param testHtml
*/
function addTimeZoneNamePolyfill(testHtml: string): string {
return html` <script>
const OriginalDateTimeFormat = Intl.DateTimeFormat;

class ExtendedDateTimeFormat extends OriginalDateTimeFormat {
constructor(locales, options) {
const originalOptions = { ...options };
delete options?.timeZoneName;
super(locales, options);
this.originalOptions = originalOptions;
}

formatToParts(date) {
const originalParts = super.formatToParts(date);
const timeZoneName = this.originalOptions.timeZoneName;

if (timeZoneName === "shortOffset") {
const { timeZone } = this.originalOptions;

// hardcoding GMT and time zone names for this particular test suite
const offsetString =
"GMT" +
(timeZone === "America/Los_Angeles"
? "-7"
: timeZone === "America/Denver"
? "-6"
: timeZone === "Europe/London"
? "+1"
: "+0");

originalParts.push({ type: "timeZoneName", value: offsetString });
}

return originalParts;
}

resolvedOptions() {
const originalResolvedOptions = OriginalDateTimeFormat.prototype.resolvedOptions;
const options = originalResolvedOptions.call(this);
const timeZoneName = options.timeZoneName;

if (timeZoneName === "shortOffset") {
options.timeZoneName = undefined;
options.timeZone = options.timeZone || "UTC";
return options;
}

return options;
}
}

Intl.DateTimeFormat = ExtendedDateTimeFormat;

Intl.supportedValuesOf = function (key) {
if (key === "timeZone") {
return ["America/Los_Angeles", "America/Denver", "Europe/London"];
}
};
</script>
${testHtml}`;
}
Loading