diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts index 4c6763a5815..459c8b51ce6 100644 --- a/packages/calcite-components/src/components.d.ts +++ b/packages/calcite-components/src/components.d.ts @@ -2615,6 +2615,10 @@ export namespace Components { "value": string; } interface CalciteInputTimeZone { + /** + * When `true`, an empty value (`null`) will be allowed as a `value`. When `false`, an offset or name value is enforced, and clearing the input or blurring will restore the last valid `value`. + */ + "clearable": boolean; /** * When `true`, interaction is prevented and the component is displayed with lower opacity. */ @@ -10091,6 +10095,10 @@ declare namespace LocalJSX { "value"?: string; } interface CalciteInputTimeZone { + /** + * When `true`, an empty value (`null`) will be allowed as a `value`. When `false`, an offset or name value is enforced, and clearing the input or blurring will restore the last valid `value`. + */ + "clearable"?: boolean; /** * When `true`, interaction is prevented and the component is displayed with lower opacity. */ diff --git a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json index 477bb116d51..93a716c624d 100644 --- a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json +++ b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json @@ -1,5 +1,7 @@ { "chooseTimeZone": "Choose time zone.", + "offsetPlaceholder": "Search by city, region or offset", + "namePlaceholder": "Search by time zone", "timeZoneLabel": "({offset}) {cities}", "Africa/Abidjan": "Abidjan", "Africa/Accra": "Accra", diff --git a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json index 477bb116d51..93a716c624d 100644 --- a/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json +++ b/packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json @@ -1,5 +1,7 @@ { "chooseTimeZone": "Choose time zone.", + "offsetPlaceholder": "Search by city, region or offset", + "namePlaceholder": "Search by time zone", "timeZoneLabel": "({offset}) {cities}", "Africa/Abidjan": "Abidjan", "Africa/Accra": "Accra", diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts index 6beb2983b38..c5ca280a48a 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts @@ -1,4 +1,4 @@ -import { newE2EPage } from "@stencil/core/testing"; +import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing"; import { html } from "../../../support/formatting"; import { accessible, @@ -304,25 +304,106 @@ describe("calcite-input-time-zone", () => { }); }); - it("does not allow users to deselect a time zone offset", async () => { - const page = await newE2EPage(); - await page.emulateTimezone(testTimeZoneItems[0].name); - await page.setContent( - addTimeZoneNamePolyfill(html` - - `), - ); - await page.waitForChanges(); + describe("clearable", () => { + it("does not allow users to deselect a time zone value by default", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + addTimeZoneNamePolyfill(html` + + `), + ); + await page.waitForChanges(); + + let selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); + await selectedTimeZoneItem.click(); + await page.waitForChanges(); + + 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(`${testTimeZoneItems[1].offset}`); + expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label); + + input.setProperty("value", ""); + await page.waitForChanges(); + + selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); + expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`); + expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label); + }); - let selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); - await selectedTimeZoneItem.click(); - await page.waitForChanges(); + describe("clearing by value", () => { + let page: E2EPage; + let input: E2EElement; - selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]"); - const input = await page.find("calcite-input-time-zone"); + beforeEach(async () => { + page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + addTimeZoneNamePolyfill( + html` `, + ), + ); + input = await page.find("calcite-input-time-zone"); + }); + + it("empty string", async () => { + await input.setProperty("value", ""); + await page.waitForChanges(); + + expect(await input.getProperty("value")).toBe(""); + }); - expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`); - expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label); + it("null", async () => { + await input.setProperty("value", null); + await page.waitForChanges(); + + expect(await input.getProperty("value")).toBe(""); + }); + }); + + it("allows users to deselect a time zone value when clearable is enabled", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + addTimeZoneNamePolyfill( + html``, + ), + ); + + const input = await page.find("calcite-input-time-zone"); + await input.callMethod("setFocus"); + + expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`); + + await input.press("Escape"); + await page.waitForChanges(); + + expect(await input.getProperty("value")).toBe(""); + }); + + it("can be cleared on initialization when clearable is enabled", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + addTimeZoneNamePolyfill(html``), + ); + + const input = await page.find("calcite-input-time-zone"); + expect(await input.getProperty("value")).toBe(""); + }); + + it("selects user time zone value when value is not set and clearable is enabled", async () => { + const page = await newE2EPage(); + await page.emulateTimezone(testTimeZoneItems[0].name); + await page.setContent( + addTimeZoneNamePolyfill(html``), + ); + + const input = await page.find("calcite-input-time-zone"); + expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[0].offset}`); + }); }); describe("selection of subsequent items with the same offset", () => { @@ -392,21 +473,21 @@ describe("calcite-input-time-zone", () => { const inputTimeZone = await page.find("calcite-input-time-zone"); let prevComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); - await inputTimeZone.setProperty("lang", "es"); + inputTimeZone.setProperty("lang", "es"); await page.waitForChanges(); let currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); expect(currComboboxItem).not.toBe(prevComboboxItem); prevComboboxItem = currComboboxItem; - await inputTimeZone.setProperty("referenceDate", "2021-01-01"); + inputTimeZone.setProperty("referenceDate", "2021-01-01"); await page.waitForChanges(); currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); expect(currComboboxItem).not.toBe(prevComboboxItem); prevComboboxItem = currComboboxItem; - await inputTimeZone.setProperty("mode", "list"); + inputTimeZone.setProperty("mode", "list"); await page.waitForChanges(); currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item"); diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts b/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts index 258c358dc12..d531daaba8c 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts @@ -25,6 +25,18 @@ export const simple = (): string => html` > `; +export const clearable = (): string => html` + + + +
+ + + +`; + +clearable.parameters = { chromatic: { delay: 500 } }; + export const timeZoneNameMode_TestOnly = (): string => html` `; diff --git a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx index b63a8c38a2c..a1368270f3b 100644 --- a/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx +++ b/packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx @@ -77,6 +77,13 @@ export class InputTimeZone // //-------------------------------------------------------------------------- + /** + * When `true`, an empty value (`null`) will be allowed as a `value`. + * + * When `false`, an offset or name value is enforced, and clearing the input or blurring will restore the last valid `value`. + */ + @Prop({ reflect: true }) clearable = false; + /** * When `true`, interaction is prevented and the component is displayed with lower opacity. */ @@ -188,6 +195,14 @@ export class InputTimeZone @Watch("value") handleValueChange(value: string, oldValue: string): void { + value = this.normalizeValue(value); + + if (!value && this.clearable) { + this.value = value; + this.selectedTimeZoneItem = null; + return; + } + const timeZoneItem = this.findTimeZoneItem(value); if (!timeZoneItem) { @@ -302,7 +317,17 @@ export class InputTimeZone private onComboboxChange = (event: CustomEvent): void => { event.stopPropagation(); const combobox = event.target as HTMLCalciteComboboxElement; - const selected = this.findTimeZoneItemByLabel(combobox.selectedItems[0].textLabel); + const selectedItem = combobox.selectedItems[0]; + + if (!selectedItem) { + this.value = null; + this.selectedTimeZoneItem = null; + this.calciteInputTimeZoneChange.emit(); + return; + } + + const selected = this.findTimeZoneItemByLabel(selectedItem.textLabel); + const selectedValue = `${selected.value}`; if (this.value === selectedValue && selected.label === this.selectedTimeZoneItem.label) { @@ -326,25 +351,27 @@ export class InputTimeZone this.calciteInputTimeZoneOpen.emit(); }; - private findTimeZoneItem(value: number | string): TimeZoneItem { + private findTimeZoneItem(value: number | string | null): TimeZoneItem | null { return findTimeZoneItemByProp(this.timeZoneItems, "value", value); } - private findTimeZoneItemByLabel(label: string): TimeZoneItem { + private findTimeZoneItemByLabel(label: string | null): TimeZoneItem | null { return findTimeZoneItemByProp(this.timeZoneItems, "label", label); } private async updateTimeZoneItemsAndSelection(): Promise { this.timeZoneItems = await this.createTimeZoneItems(); + if (this.value === "" && this.clearable) { + this.selectedTimeZoneItem = null; + return; + } + const fallbackValue = this.mode === "offset" ? getUserTimeZoneOffset() : getUserTimeZoneName(); const valueToMatch = this.value ?? fallbackValue; - this.selectedTimeZoneItem = this.findTimeZoneItem(valueToMatch); - - if (!this.selectedTimeZoneItem) { - this.selectedTimeZoneItem = this.findTimeZoneItem(fallbackValue); - } + this.selectedTimeZoneItem = + this.findTimeZoneItem(valueToMatch) || this.findTimeZoneItem(fallbackValue); } private async createTimeZoneItems(): Promise { @@ -382,13 +409,18 @@ export class InputTimeZone disconnectMessages(this); } + private normalizeValue(value: string | null): string { + return value === null ? "" : value; + } + async componentWillLoad(): Promise { setUpLoadableComponent(this); await setUpMessages(this); + this.value = this.normalizeValue(this.value); await this.updateTimeZoneItemsAndSelection(); - const selectedValue = `${this.selectedTimeZoneItem.value}`; + const selectedValue = this.selectedTimeZoneItem ? `${this.selectedTimeZoneItem.value}` : null; afterConnectDefaultValueSet(this, selectedValue); this.value = selectedValue; } @@ -406,7 +438,7 @@ export class InputTimeZone - // intentional == to match string to number - item[prop] == valueToMatch, - ); + valueToMatch: string | number | null, +): TimeZoneItem | null { + return valueToMatch == null + ? null + : timeZoneItems.find( + (item) => + // intentional == to match string to number + item[prop] == valueToMatch, + ); } diff --git a/packages/calcite-components/src/demos/input-time-zone.html b/packages/calcite-components/src/demos/input-time-zone.html index 73b144fce2d..67ef2607bea 100644 --- a/packages/calcite-components/src/demos/input-time-zone.html +++ b/packages/calcite-components/src/demos/input-time-zone.html @@ -62,6 +62,21 @@

Select

+
+
Basic (offset mode, clearable)
+
+ +
+ +
+ +
+ +
+ +
+
+
Basic (offset mode) + readonly
@@ -136,6 +151,21 @@

Select

+ +
+
name mode (clearable)
+
+ +
+ +
+ +
+ +
+ +
+