Skip to content

Commit

Permalink
feat(input-time-zone): allow clearing value (#9168)
Browse files Browse the repository at this point in the history
**Related Issue:** #9020

## Summary

Adds `clearable` property to allow `input-time-zone` to be cleared via
the UI or programmatically via `””` or `null`.
  • Loading branch information
jcfranco authored Apr 25, 2024
1 parent 4f781a0 commit 193bb7d
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 38 deletions.
8 changes: 8 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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`
<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" open></calcite-input-time-zone>
`),
);
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`
<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" open></calcite-input-time-zone>
`),
);
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` <calcite-input-time-zone value="${testTimeZoneItems[1].offset}" clearable></calcite-input-time-zone>`,
),
);
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`<calcite-input-time-zone value="${testTimeZoneItems[1].offset}" clearable></calcite-input-time-zone>`,
),
);

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`<calcite-input-time-zone value="" clearable></calcite-input-time-zone>`),
);

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`<calcite-input-time-zone clearable></calcite-input-time-zone>`),
);

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", () => {
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export const simple = (): string => html`
></calcite-input-time-zone>
`;

export const clearable = (): string => html`
<label>default</label>
<calcite-input-time-zone mode="offset" clearable></calcite-input-time-zone>
<calcite-input-time-zone mode="name" clearable></calcite-input-time-zone>
<br />
<label>initialized as empty</label>
<calcite-input-time-zone mode="offset" clearable value=""></calcite-input-time-zone>
<calcite-input-time-zone mode="name" clearable value=""></calcite-input-time-zone>
`;

clearable.parameters = { chromatic: { delay: 500 } };

export const timeZoneNameMode_TestOnly = (): string => html`
<calcite-input-time-zone mode="name" open></calcite-input-time-zone>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<void> {
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<TimeZoneItem[]> {
Expand Down Expand Up @@ -382,13 +409,18 @@ export class InputTimeZone
disconnectMessages(this);
}

private normalizeValue(value: string | null): string {
return value === null ? "" : value;
}

async componentWillLoad(): Promise<void> {
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;
}
Expand All @@ -406,7 +438,7 @@ export class InputTimeZone
<Host>
<InteractiveContainer disabled={this.disabled}>
<calcite-combobox
clearDisabled={true}
clearDisabled={!this.clearable}
disabled={this.disabled}
label={this.messages.chooseTimeZone}
lang={this.effectiveLocale}
Expand All @@ -418,9 +450,12 @@ export class InputTimeZone
onCalciteComboboxOpen={this.onComboboxOpen}
open={this.open}
overlayPositioning={this.overlayPositioning}
placeholder={
this.mode === "name" ? this.messages.namePlaceholder : this.messages.offsetPlaceholder
}
readOnly={this.readOnly}
scale={this.scale}
selectionMode="single-persist"
selectionMode={this.clearable ? "single" : "single-persist"}
status={this.status}
validation-icon={this.validationIcon}
validation-message={this.validationMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,13 @@ function getTimeZoneShortOffset(
export function findTimeZoneItemByProp(
timeZoneItems: TimeZoneItem[],
prop: string,
valueToMatch: string | number,
): TimeZoneItem {
return timeZoneItems.find(
(item) =>
// 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,
);
}
Loading

0 comments on commit 193bb7d

Please sign in to comment.