Skip to content

fix(input, input-number, input-text): restore focus on input after browser validation error is displayed and user continues typing #8563

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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { getElementRect, getElementXY, selectText } from "../../tests/utils";
import { letterKeys, numberKeys } from "../../utils/key";
import { locales, numberStringFormatter } from "../../utils/locale";
import { testPostValidationFocusing } from "../input/common/tests";

describe("calcite-input-number", () => {
const delayFor2UpdatesInMs = 200;
Expand Down Expand Up @@ -1716,6 +1717,8 @@ describe("calcite-input-number", () => {
submitsOnEnter: true,
inputType: "number",
});

testPostValidationFocusing("calcite-input-number");
});

describe("translation support", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
disconnectForm,
FormComponent,
HiddenFormInputSlot,
internalHiddenInputChangeEvent,
submitForm,
} from "../../utils/form";
import {
Expand Down Expand Up @@ -433,7 +434,7 @@ export class InputNumber
});
this.mutationObserver?.observe(this.el, { childList: true });
this.setDisabledAction();
this.el.addEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.addEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

componentDidLoad(): void {
Expand All @@ -448,7 +449,7 @@ export class InputNumber
disconnectMessages(this);

this.mutationObserver?.disconnect();
this.el.removeEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.removeEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

async componentWillLoad(): Promise<void> {
Expand Down Expand Up @@ -785,13 +786,14 @@ export class InputNumber
input.max = this.max?.toString(10) ?? "";
}

hiddenInputChangeHandler = (event: Event): void => {
private onHiddenFormInputChange = (event: Event): void => {
if ((event.target as HTMLInputElement).name === this.name) {
this.setNumberValue({
value: (event.target as HTMLInputElement).value,
origin: "direct",
});
}
this.setFocus();
event.stopPropagation();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
t9n,
} from "../../tests/commonTests";
import { selectText } from "../../tests/utils";
import { testPostValidationFocusing } from "../input/common/tests";

describe("calcite-input-text", () => {
describe("labelable", () => {
Expand Down Expand Up @@ -450,6 +451,8 @@ describe("calcite-input-text", () => {

describe("is form-associated", () => {
formAssociated("calcite-input-text", { testValue: "test", submitsOnEnter: true });

testPostValidationFocusing("calcite-input-text");
});

describe("translation support", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
disconnectForm,
FormComponent,
HiddenFormInputSlot,
internalHiddenInputChangeEvent,
submitForm,
} from "../../utils/form";
import {
Expand Down Expand Up @@ -335,7 +336,7 @@ export class InputText
connectForm(this);
this.mutationObserver?.observe(this.el, { childList: true });
this.setDisabledAction();
this.el.addEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.addEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

disconnectedCallback(): void {
Expand All @@ -346,7 +347,7 @@ export class InputText
disconnectMessages(this);

this.mutationObserver?.disconnect();
this.el.removeEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.removeEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

async componentWillLoad(): Promise<void> {
Expand Down Expand Up @@ -516,13 +517,14 @@ export class InputText
}
}

hiddenInputChangeHandler = (event: Event): void => {
private onHiddenFormInputChange = (event: Event): void => {
if ((event.target as HTMLInputElement).name === this.name) {
this.setValue({
value: (event.target as HTMLInputElement).value,
origin: "direct",
});
}
this.setFocus();
event.stopPropagation();
};

Expand Down
48 changes: 48 additions & 0 deletions packages/calcite-components/src/components/input/common/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { newE2EPage } from "@stencil/core/testing";
import { isElementFocused } from "../../../tests/utils";
import { hiddenFormInputSlotName } from "../../../utils/form";
import { html } from "../../../../support/formatting";
import { JSX } from "../../../components";

export function testPostValidationFocusing(
inputTag: Extract<keyof JSX.IntrinsicElements, "calcite-input" | "calcite-input-text" | "calcite-input-number">,
): void {
it("restores focus on invalid input if user continues typing", async () => {
const page = await newE2EPage();
const inputName = "test";

await page.setContent(html`
<form>
<${inputTag} type="text" required name="${inputName}"></${inputTag}>
</form>
<script>
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault();
});
</script>
`);

const input = await page.find(inputTag);

await input.callMethod("setFocus");
await input.press("Enter");
await page.waitForChanges();

const hiddenInputSelector = `input[slot=${hiddenFormInputSlotName}]`;
const inputSelector = `${inputTag}[name=${inputName}]`;

expect(await isElementFocused(page, hiddenInputSelector)).toBe(true);
expect(await isElementFocused(page, inputSelector)).toBe(false);
expect(await input.getProperty("value")).toBe("");

const expectedValue = "12345"; // number works for both text and number types

await page.keyboard.type(expectedValue);
await page.waitForChanges();

expect(await isElementFocused(page, hiddenInputSelector)).toBe(false);
expect(await isElementFocused(page, inputSelector)).toBe(true);
expect(await input.getProperty("value")).toBe(expectedValue);
});
}
3 changes: 3 additions & 0 deletions packages/calcite-components/src/components/input/input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { letterKeys, numberKeys } from "../../utils/key";
import { locales, numberStringFormatter } from "../../utils/locale";
import { getElementRect, getElementXY, selectText } from "../../tests/utils";
import { KeyInput } from "puppeteer";
import { testPostValidationFocusing } from "./common/tests";

describe("calcite-input", () => {
const delayFor2UpdatesInMs = 200;
Expand Down Expand Up @@ -2021,6 +2022,8 @@ describe("calcite-input", () => {
inputType: type,
});
}

testPostValidationFocusing("calcite-input");
});

describe("translation support", () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/calcite-components/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
disconnectForm,
FormComponent,
HiddenFormInputSlot,
internalHiddenInputChangeEvent,
submitForm,
} from "../../utils/form";
import {
Expand Down Expand Up @@ -499,7 +500,7 @@ export class Input
this.mutationObserver?.observe(this.el, { childList: true });

this.setDisabledAction();
this.el.addEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.addEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

disconnectedCallback(): void {
Expand All @@ -510,7 +511,7 @@ export class Input
disconnectMessages(this);

this.mutationObserver?.disconnect();
this.el.removeEventListener("calciteInternalHiddenInputChange", this.hiddenInputChangeHandler);
this.el.removeEventListener(internalHiddenInputChangeEvent, this.onHiddenFormInputChange);
}

async componentWillLoad(): Promise<void> {
Expand Down Expand Up @@ -886,13 +887,14 @@ export class Input
}
}

hiddenInputChangeHandler = (event: Event): void => {
private onHiddenFormInputChange = (event: Event): void => {
if ((event.target as HTMLInputElement).name === this.name) {
this.setValue({
value: (event.target as HTMLInputElement).value,
origin: "direct",
});
}
this.setFocus();
event.stopPropagation();
};

Expand Down
8 changes: 4 additions & 4 deletions packages/calcite-components/src/utils/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,10 @@ export function afterConnectDefaultValueSet<T>(component: FormComponent<T>, valu
component.defaultValue = value;
}

export const internalHiddenInputChangeEvent = "calciteInternalHiddenChangeInput";

const hiddenInputChangeHandler = (event: Event) => {
event.target.dispatchEvent(
new CustomEvent("calciteInternalHiddenInputChange", { bubbles: true }),
);
event.target.dispatchEvent(new CustomEvent(internalHiddenInputChangeEvent, { bubbles: true }));
};

const removeHiddenInputChangeEventListener = (input: HTMLInputElement) =>
Expand Down Expand Up @@ -334,7 +334,7 @@ function syncHiddenFormInput(component: FormComponent): void {
docFrag.append(input);

// emits when hidden input is autofilled
input.addEventListener("change", hiddenInputChangeHandler);
input.addEventListener("input", hiddenInputChangeHandler);

defaultSyncHiddenFormInput(component, input, value);
});
Expand Down