Skip to content

fix(autocomplete): handle focusing when item is clicked and reset inputValue on form reset #11099

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

Merged
merged 13 commits into from
Dec 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export class AutocompleteItem

// #region Private Methods

private handleClick(): void {
private handleClick(event: MouseEvent): void {
event.preventDefault();
this.calciteInternalAutocompleteItemSelect.emit();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { html } from "../../../support/formatting";
import { defaultMenuPlacement } from "../../utils/floating-ui";
import { Input } from "../input/input";
import { skipAnimations } from "../../tests/utils";
import { isElementFocused, skipAnimations } from "../../tests/utils";
import { CSS, SLOTS } from "./resources";
import { Autocomplete } from "./autocomplete";

Expand Down Expand Up @@ -552,6 +552,19 @@ describe("calcite-autocomplete", () => {
expect(await autocomplete.getProperty("open")).toBe(false);
});

it("should open when input is clicked", async () => {
const page = await newE2EPage();
await page.setContent(`${simpleHTML}<div id="test">test</div>`);

const input = await page.find("calcite-autocomplete >>> calcite-input");
await input.click();
await page.waitForChanges();

const autocomplete = await page.find("calcite-autocomplete");

expect(await autocomplete.getProperty("open")).toBe(true);
});

it("should set value, close, and emit calciteAutocompleteChange when item is selected via mouse", async () => {
const page = await newE2EPage();
await page.setContent(simpleHTML);
Expand All @@ -569,6 +582,7 @@ describe("calcite-autocomplete", () => {
expect(await autocomplete.getProperty("value")).toBe("two");
expect(await autocomplete.getProperty("open")).toBe(false);
expect(changeEvent).toHaveReceivedEventTimes(1);
expect(await isElementFocused(page, "#myAutocomplete")).toBe(true);
});

it("should set value, close, and emit calciteAutocompleteChange when item is selected via keyboard", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export class Autocomplete

defaultValue: Autocomplete["value"];

defaultInputValue: Autocomplete["inputValue"];

floatingEl: HTMLDivElement;

floatingLayout?: FloatingLayout;
Expand Down Expand Up @@ -396,6 +398,7 @@ export class Autocomplete
this.mutationObserver?.observe(this.el, { childList: true, subtree: true });
connectLabel(this);
connectForm(this);
this.defaultInputValue = this.inputValue || "";

this.getAllItemsDebounced();

Expand Down Expand Up @@ -456,6 +459,7 @@ export class Autocomplete

loaded(): void {
afterConnectDefaultValueSet(this, this.value || "");
this.defaultInputValue = this.inputValue || "";
setComponentLoaded(this);
connectFloatingUI(this);
}
Expand Down Expand Up @@ -516,10 +520,12 @@ export class Autocomplete
this.open = false;
}

private handleInternalAutocompleteItemSelect(event: Event): void {
private async handleInternalAutocompleteItemSelect(event: Event): Promise<void> {
this.value = (event.target as AutocompleteItem["el"]).value;
event.stopPropagation();
this.emitChange();
await this.setFocus();
this.open = false;
}

private mutationObserver = createObserver("mutation", () => this.getAllItemsDebounced());
Expand All @@ -532,6 +538,10 @@ export class Autocomplete
this.setFocus();
}

onFormReset(): void {
this.inputValue = this.defaultInputValue;
}

onBeforeOpen(): void {
this.calciteAutocompleteBeforeOpen.emit();
}
Expand All @@ -549,7 +559,6 @@ export class Autocomplete
}

private emitChange(): void {
this.open = false;
this.calciteAutocompleteChange.emit();
}

Expand Down Expand Up @@ -624,11 +633,6 @@ export class Autocomplete
this.resizeObserver?.observe(el);

connectFloatingUI(this);

// TODO: fixme when supported in jsx
// https://devtopia.esri.com/WebGIS/arcgis-web-components/issues/2694
const enterKeyHint = this.el.getAttribute("enterkeyhint");
el.enterKeyHint = enterKeyHint;
}

private keyDownHandler(event: KeyboardEvent): void {
Expand All @@ -654,6 +658,7 @@ export class Autocomplete
if (open && activeIndex > -1) {
this.value = enabledItems[activeIndex].value;
this.emitChange();
this.open = false;
event.preventDefault();
} else if (!event.defaultPrevented) {
if (submitForm(this)) {
Expand Down Expand Up @@ -692,6 +697,14 @@ export class Autocomplete
this.calciteAutocompleteTextChange.emit();
}

private inputClickHandler(event: MouseEvent): void {
if (event.defaultPrevented) {
return;
}

this.open = true;
}

private inputHandler(event: CustomEvent): void {
event.stopPropagation();
this.inputValue = (event.target as Input["el"]).value;
Expand All @@ -715,6 +728,7 @@ export class Autocomplete
const { disabled, listId, inputId, isOpen } = this;

const autofocus = this.el.autofocus || this.el.hasAttribute("autofocus") ? true : null;
const enterKeyHint = this.el.getAttribute("enterkeyhint");
const inputMode = this.el.getAttribute("inputmode") as
| "none"
| "text"
Expand All @@ -741,6 +755,7 @@ export class Autocomplete
class={CSS.input}
clearable={true}
disabled={disabled}
enterKeyHint={enterKeyHint}
form={this.form}
icon={this.getIcon()}
iconFlipRtl={this.iconFlipRtl}
Expand All @@ -752,6 +767,7 @@ export class Autocomplete
messageOverrides={this.messages}
minLength={this.minLength}
name={this.name}
onClick={this.inputClickHandler}
onKeyDown={this.keyDownHandler}
oncalciteInputChange={this.changeHandler}
oncalciteInputInput={this.inputHandler}
Expand Down
59 changes: 59 additions & 0 deletions packages/calcite-components/src/demos/validation.html
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,59 @@ <h1 style="margin: 0 auto; text-align: center">Form Validation</h1>
<calcite-rating required name="rating"></calcite-rating>
</calcite-label>
</li>

<!-- Autocomplete -->
<li>
<calcite-label>
Which framework do you use?
<calcite-autocomplete name="framework" required id="framework">
<calcite-autocomplete-item-group heading="Frameworks">
<calcite-autocomplete-item
label="VanillaJS"
value="vanilla"
heading="VanillaJS"
description="VanillaJS with @esri/calcite-components"
icon-start="ribbon"
></calcite-autocomplete-item>
<calcite-autocomplete-item
label="React"
value="react"
heading="React"
description="React with @esri/calcite-components-react"
icon-start="code"
></calcite-autocomplete-item>
<calcite-autocomplete-item
label="Vue"
value="vue"
heading="Vue"
description="Vue with @esri/calcite-components"
icon-start="package"
></calcite-autocomplete-item>
<calcite-autocomplete-item
label="Svelte"
value="svelte"
heading="Svelte"
description="Svelte with @esri/calcite-components"
icon-start="banana"
></calcite-autocomplete-item>
<calcite-autocomplete-item
label="Angular"
value="angular"
heading="Angular"
description="Angular with @esri/calcite-components"
icon-start="data"
></calcite-autocomplete-item>
<calcite-autocomplete-item
label="Ember"
value="ember"
heading="Ember"
description="Ember with @esri/calcite-components"
icon-start="trash"
></calcite-autocomplete-item>
</calcite-autocomplete-item-group>
</calcite-autocomplete>
</calcite-label>
</li>
</ol>

<!-- Button -->
Expand Down Expand Up @@ -457,6 +510,12 @@ <h1 style="margin: 0 auto; text-align: center">Form Validation</h1>
datePicker.maxAsDate = new Date();
datePicker.min = "2021-01-01";

// Autocomplete
const autocomplete = document.getElementById("framework");
autocomplete.addEventListener("calciteAutocompleteChange", (event) => {
autocomplete.inputValue = event.target.value;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for demonstration purposes only. We don't update the inputValue when a value has been selected. This is because we don't know what data would be best to put into the input. A value may be a unique id, a description or label may be better. Its best to leave this up to the end user to decide if they want to populate the input or not. In some cases, it may be best to leave the input as is with whatever the user has typed in so they can adjust their query.

});

// Form submission
const form = document.getElementById("form");
const submit = document.getElementById("submit");
Expand Down
4 changes: 3 additions & 1 deletion packages/calcite-components/src/utils/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export function connectForm<T>(component: FormComponent<T>): void {
component.defaultChecked = component.checked;
}

const boundOnFormReset = (component.onFormReset || onFormReset).bind(component);
const boundOnFormReset = onFormReset.bind(component);
associatedForm.addEventListener("reset", boundOnFormReset);
onFormResetMap.set(component.el, boundOnFormReset);
formComponentSet.add(el);
Expand Down Expand Up @@ -403,6 +403,8 @@ function onFormReset<T>(this: FormComponent<T>): void {
}

this.value = this.defaultValue;

this.onFormReset?.();
}

/**
Expand Down
Loading