Skip to content

Commit 193bb7d

Browse files
authored
feat(input-time-zone): allow clearing value (#9168)
**Related Issue:** #9020 ## Summary Adds `clearable` property to allow `input-time-zone` to be cleared via the UI or programmatically via `””` or `null`.
1 parent 4f781a0 commit 193bb7d

File tree

8 files changed

+210
-38
lines changed

8 files changed

+210
-38
lines changed

packages/calcite-components/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,10 @@ export namespace Components {
26152615
"value": string;
26162616
}
26172617
interface CalciteInputTimeZone {
2618+
/**
2619+
* 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`.
2620+
*/
2621+
"clearable": boolean;
26182622
/**
26192623
* When `true`, interaction is prevented and the component is displayed with lower opacity.
26202624
*/
@@ -10091,6 +10095,10 @@ declare namespace LocalJSX {
1009110095
"value"?: string;
1009210096
}
1009310097
interface CalciteInputTimeZone {
10098+
/**
10099+
* 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`.
10100+
*/
10101+
"clearable"?: boolean;
1009410102
/**
1009510103
* When `true`, interaction is prevented and the component is displayed with lower opacity.
1009610104
*/

packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"chooseTimeZone": "Choose time zone.",
3+
"offsetPlaceholder": "Search by city, region or offset",
4+
"namePlaceholder": "Search by time zone",
35
"timeZoneLabel": "({offset}) {cities}",
46
"Africa/Abidjan": "Abidjan",
57
"Africa/Accra": "Accra",

packages/calcite-components/src/components/input-time-zone/assets/input-time-zone/t9n/messages_en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"chooseTimeZone": "Choose time zone.",
3+
"offsetPlaceholder": "Search by city, region or offset",
4+
"namePlaceholder": "Search by time zone",
35
"timeZoneLabel": "({offset}) {cities}",
46
"Africa/Abidjan": "Abidjan",
57
"Africa/Accra": "Accra",

packages/calcite-components/src/components/input-time-zone/input-time-zone.e2e.ts

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { newE2EPage } from "@stencil/core/testing";
1+
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
22
import { html } from "../../../support/formatting";
33
import {
44
accessible,
@@ -304,25 +304,106 @@ describe("calcite-input-time-zone", () => {
304304
});
305305
});
306306

307-
it("does not allow users to deselect a time zone offset", async () => {
308-
const page = await newE2EPage();
309-
await page.emulateTimezone(testTimeZoneItems[0].name);
310-
await page.setContent(
311-
addTimeZoneNamePolyfill(html`
312-
<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" open></calcite-input-time-zone>
313-
`),
314-
);
315-
await page.waitForChanges();
307+
describe("clearable", () => {
308+
it("does not allow users to deselect a time zone value by default", async () => {
309+
const page = await newE2EPage();
310+
await page.emulateTimezone(testTimeZoneItems[0].name);
311+
await page.setContent(
312+
addTimeZoneNamePolyfill(html`
313+
<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" open></calcite-input-time-zone>
314+
`),
315+
);
316+
await page.waitForChanges();
317+
318+
let selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
319+
await selectedTimeZoneItem.click();
320+
await page.waitForChanges();
321+
322+
selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
323+
const input = await page.find("calcite-input-time-zone");
324+
325+
expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`);
326+
expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label);
327+
328+
input.setProperty("value", "");
329+
await page.waitForChanges();
330+
331+
selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
332+
expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`);
333+
expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label);
334+
});
316335

317-
let selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
318-
await selectedTimeZoneItem.click();
319-
await page.waitForChanges();
336+
describe("clearing by value", () => {
337+
let page: E2EPage;
338+
let input: E2EElement;
320339

321-
selectedTimeZoneItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item[selected]");
322-
const input = await page.find("calcite-input-time-zone");
340+
beforeEach(async () => {
341+
page = await newE2EPage();
342+
await page.emulateTimezone(testTimeZoneItems[0].name);
343+
await page.setContent(
344+
addTimeZoneNamePolyfill(
345+
html` <calcite-input-time-zone value="${testTimeZoneItems[1].offset}" clearable></calcite-input-time-zone>`,
346+
),
347+
);
348+
input = await page.find("calcite-input-time-zone");
349+
});
350+
351+
it("empty string", async () => {
352+
await input.setProperty("value", "");
353+
await page.waitForChanges();
354+
355+
expect(await input.getProperty("value")).toBe("");
356+
});
323357

324-
expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`);
325-
expect(await selectedTimeZoneItem.getProperty("textLabel")).toMatch(testTimeZoneItems[1].label);
358+
it("null", async () => {
359+
await input.setProperty("value", null);
360+
await page.waitForChanges();
361+
362+
expect(await input.getProperty("value")).toBe("");
363+
});
364+
});
365+
366+
it("allows users to deselect a time zone value when clearable is enabled", async () => {
367+
const page = await newE2EPage();
368+
await page.emulateTimezone(testTimeZoneItems[0].name);
369+
await page.setContent(
370+
addTimeZoneNamePolyfill(
371+
html`<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" clearable></calcite-input-time-zone>`,
372+
),
373+
);
374+
375+
const input = await page.find("calcite-input-time-zone");
376+
await input.callMethod("setFocus");
377+
378+
expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[1].offset}`);
379+
380+
await input.press("Escape");
381+
await page.waitForChanges();
382+
383+
expect(await input.getProperty("value")).toBe("");
384+
});
385+
386+
it("can be cleared on initialization when clearable is enabled", async () => {
387+
const page = await newE2EPage();
388+
await page.emulateTimezone(testTimeZoneItems[0].name);
389+
await page.setContent(
390+
addTimeZoneNamePolyfill(html`<calcite-input-time-zone value="" clearable></calcite-input-time-zone>`),
391+
);
392+
393+
const input = await page.find("calcite-input-time-zone");
394+
expect(await input.getProperty("value")).toBe("");
395+
});
396+
397+
it("selects user time zone value when value is not set and clearable is enabled", async () => {
398+
const page = await newE2EPage();
399+
await page.emulateTimezone(testTimeZoneItems[0].name);
400+
await page.setContent(
401+
addTimeZoneNamePolyfill(html`<calcite-input-time-zone clearable></calcite-input-time-zone>`),
402+
);
403+
404+
const input = await page.find("calcite-input-time-zone");
405+
expect(await input.getProperty("value")).toBe(`${testTimeZoneItems[0].offset}`);
406+
});
326407
});
327408

328409
describe("selection of subsequent items with the same offset", () => {
@@ -392,21 +473,21 @@ describe("calcite-input-time-zone", () => {
392473
const inputTimeZone = await page.find("calcite-input-time-zone");
393474

394475
let prevComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item");
395-
await inputTimeZone.setProperty("lang", "es");
476+
inputTimeZone.setProperty("lang", "es");
396477
await page.waitForChanges();
397478

398479
let currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item");
399480
expect(currComboboxItem).not.toBe(prevComboboxItem);
400481

401482
prevComboboxItem = currComboboxItem;
402-
await inputTimeZone.setProperty("referenceDate", "2021-01-01");
483+
inputTimeZone.setProperty("referenceDate", "2021-01-01");
403484
await page.waitForChanges();
404485

405486
currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item");
406487
expect(currComboboxItem).not.toBe(prevComboboxItem);
407488

408489
prevComboboxItem = currComboboxItem;
409-
await inputTimeZone.setProperty("mode", "list");
490+
inputTimeZone.setProperty("mode", "list");
410491
await page.waitForChanges();
411492

412493
currComboboxItem = await page.find("calcite-input-time-zone >>> calcite-combobox-item");

packages/calcite-components/src/components/input-time-zone/input-time-zone.stories.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ export const simple = (): string => html`
2525
></calcite-input-time-zone>
2626
`;
2727

28+
export const clearable = (): string => html`
29+
<label>default</label>
30+
<calcite-input-time-zone mode="offset" clearable></calcite-input-time-zone>
31+
<calcite-input-time-zone mode="name" clearable></calcite-input-time-zone>
32+
<br />
33+
<label>initialized as empty</label>
34+
<calcite-input-time-zone mode="offset" clearable value=""></calcite-input-time-zone>
35+
<calcite-input-time-zone mode="name" clearable value=""></calcite-input-time-zone>
36+
`;
37+
38+
clearable.parameters = { chromatic: { delay: 500 } };
39+
2840
export const timeZoneNameMode_TestOnly = (): string => html`
2941
<calcite-input-time-zone mode="name" open></calcite-input-time-zone>
3042
`;

packages/calcite-components/src/components/input-time-zone/input-time-zone.tsx

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export class InputTimeZone
7777
//
7878
//--------------------------------------------------------------------------
7979

80+
/**
81+
* When `true`, an empty value (`null`) will be allowed as a `value`.
82+
*
83+
* When `false`, an offset or name value is enforced, and clearing the input or blurring will restore the last valid `value`.
84+
*/
85+
@Prop({ reflect: true }) clearable = false;
86+
8087
/**
8188
* When `true`, interaction is prevented and the component is displayed with lower opacity.
8289
*/
@@ -188,6 +195,14 @@ export class InputTimeZone
188195

189196
@Watch("value")
190197
handleValueChange(value: string, oldValue: string): void {
198+
value = this.normalizeValue(value);
199+
200+
if (!value && this.clearable) {
201+
this.value = value;
202+
this.selectedTimeZoneItem = null;
203+
return;
204+
}
205+
191206
const timeZoneItem = this.findTimeZoneItem(value);
192207

193208
if (!timeZoneItem) {
@@ -302,7 +317,17 @@ export class InputTimeZone
302317
private onComboboxChange = (event: CustomEvent): void => {
303318
event.stopPropagation();
304319
const combobox = event.target as HTMLCalciteComboboxElement;
305-
const selected = this.findTimeZoneItemByLabel(combobox.selectedItems[0].textLabel);
320+
const selectedItem = combobox.selectedItems[0];
321+
322+
if (!selectedItem) {
323+
this.value = null;
324+
this.selectedTimeZoneItem = null;
325+
this.calciteInputTimeZoneChange.emit();
326+
return;
327+
}
328+
329+
const selected = this.findTimeZoneItemByLabel(selectedItem.textLabel);
330+
306331
const selectedValue = `${selected.value}`;
307332

308333
if (this.value === selectedValue && selected.label === this.selectedTimeZoneItem.label) {
@@ -326,25 +351,27 @@ export class InputTimeZone
326351
this.calciteInputTimeZoneOpen.emit();
327352
};
328353

329-
private findTimeZoneItem(value: number | string): TimeZoneItem {
354+
private findTimeZoneItem(value: number | string | null): TimeZoneItem | null {
330355
return findTimeZoneItemByProp(this.timeZoneItems, "value", value);
331356
}
332357

333-
private findTimeZoneItemByLabel(label: string): TimeZoneItem {
358+
private findTimeZoneItemByLabel(label: string | null): TimeZoneItem | null {
334359
return findTimeZoneItemByProp(this.timeZoneItems, "label", label);
335360
}
336361

337362
private async updateTimeZoneItemsAndSelection(): Promise<void> {
338363
this.timeZoneItems = await this.createTimeZoneItems();
339364

365+
if (this.value === "" && this.clearable) {
366+
this.selectedTimeZoneItem = null;
367+
return;
368+
}
369+
340370
const fallbackValue = this.mode === "offset" ? getUserTimeZoneOffset() : getUserTimeZoneName();
341371
const valueToMatch = this.value ?? fallbackValue;
342372

343-
this.selectedTimeZoneItem = this.findTimeZoneItem(valueToMatch);
344-
345-
if (!this.selectedTimeZoneItem) {
346-
this.selectedTimeZoneItem = this.findTimeZoneItem(fallbackValue);
347-
}
373+
this.selectedTimeZoneItem =
374+
this.findTimeZoneItem(valueToMatch) || this.findTimeZoneItem(fallbackValue);
348375
}
349376

350377
private async createTimeZoneItems(): Promise<TimeZoneItem[]> {
@@ -382,13 +409,18 @@ export class InputTimeZone
382409
disconnectMessages(this);
383410
}
384411

412+
private normalizeValue(value: string | null): string {
413+
return value === null ? "" : value;
414+
}
415+
385416
async componentWillLoad(): Promise<void> {
386417
setUpLoadableComponent(this);
387418
await setUpMessages(this);
419+
this.value = this.normalizeValue(this.value);
388420

389421
await this.updateTimeZoneItemsAndSelection();
390422

391-
const selectedValue = `${this.selectedTimeZoneItem.value}`;
423+
const selectedValue = this.selectedTimeZoneItem ? `${this.selectedTimeZoneItem.value}` : null;
392424
afterConnectDefaultValueSet(this, selectedValue);
393425
this.value = selectedValue;
394426
}
@@ -406,7 +438,7 @@ export class InputTimeZone
406438
<Host>
407439
<InteractiveContainer disabled={this.disabled}>
408440
<calcite-combobox
409-
clearDisabled={true}
441+
clearDisabled={!this.clearable}
410442
disabled={this.disabled}
411443
label={this.messages.chooseTimeZone}
412444
lang={this.effectiveLocale}
@@ -418,9 +450,12 @@ export class InputTimeZone
418450
onCalciteComboboxOpen={this.onComboboxOpen}
419451
open={this.open}
420452
overlayPositioning={this.overlayPositioning}
453+
placeholder={
454+
this.mode === "name" ? this.messages.namePlaceholder : this.messages.offsetPlaceholder
455+
}
421456
readOnly={this.readOnly}
422457
scale={this.scale}
423-
selectionMode="single-persist"
458+
selectionMode={this.clearable ? "single" : "single-persist"}
424459
status={this.status}
425460
validation-icon={this.validationIcon}
426461
validation-message={this.validationMessage}

packages/calcite-components/src/components/input-time-zone/utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,13 @@ function getTimeZoneShortOffset(
175175
export function findTimeZoneItemByProp(
176176
timeZoneItems: TimeZoneItem[],
177177
prop: string,
178-
valueToMatch: string | number,
179-
): TimeZoneItem {
180-
return timeZoneItems.find(
181-
(item) =>
182-
// intentional == to match string to number
183-
item[prop] == valueToMatch,
184-
);
178+
valueToMatch: string | number | null,
179+
): TimeZoneItem | null {
180+
return valueToMatch == null
181+
? null
182+
: timeZoneItems.find(
183+
(item) =>
184+
// intentional == to match string to number
185+
item[prop] == valueToMatch,
186+
);
185187
}

0 commit comments

Comments
 (0)