diff --git a/src/main/javascript/bundles/user-common.ts b/src/main/javascript/bundles/user-common.ts index b32331764..46b82123d 100644 --- a/src/main/javascript/bundles/user-common.ts +++ b/src/main/javascript/bundles/user-common.ts @@ -3,6 +3,7 @@ import "../components/details-dropdown"; import "../components/feedback-form"; import "../components/navigation"; import "../components/time-clock"; +import { initPreventDoubleClickSubmit } from "../components/form"; import { initFeedbackHeartView } from "../components/feedback-heart"; const showFeedbackKudo = @@ -15,3 +16,5 @@ initFeedbackHeartView({ showFeedbackKudo: showFeedbackKudo, }, }); + +initPreventDoubleClickSubmit(); diff --git a/src/main/javascript/components/form/autosubmit.spec.ts b/src/main/javascript/components/form/autosubmit.spec.ts new file mode 100644 index 000000000..655804fe8 --- /dev/null +++ b/src/main/javascript/components/form/autosubmit.spec.ts @@ -0,0 +1,81 @@ +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "vitest"; +import { initAutosubmit } from "./autosubmit"; + +describe("autosubmit", () => { + beforeAll(() => { + initAutosubmit(); + }); + + beforeEach(() => { + // prevent HTMLFormElement.prototype.requestSubmit is not implemented log. + // + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + window._virtualConsole.emit = () => {}; + }); + + afterEach(() => { + while (document.body.firstChild) { + document.body.firstChild.remove(); + } + }); + + test("auto-submits text input", async () => { + document.body.innerHTML = ` +
+ `; + + let submitter; + + document.querySelector("form").addEventListener("submit", function (event) { + submitter = event.submitter; + }); + + const inputElement = document.querySelector("input"); + inputElement.value = "awesome text"; + inputElement.dispatchEvent(new InputEvent("input", { bubbles: true })); + + await wait(); + + expect(submitter).toBe(document.querySelector("button")); + }); + + test("auto-submits text input with custom delay", async () => { + document.body.innerHTML = ` + + `; + + let submitter; + + document.querySelector("form").addEventListener("submit", function (event) { + submitter = event.submitter; + }); + + const inputElement = document.querySelector("input"); + inputElement.value = "awesome text"; + inputElement.dispatchEvent(new InputEvent("input", { bubbles: true })); + + await wait(); + expect(submitter).toBeUndefined(); + + await wait(100); + expect(submitter).toBe(document.querySelector("button")); + }); +}); + +function wait(delay = 0) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} diff --git a/src/main/javascript/components/form/autosubmit.ts b/src/main/javascript/components/form/autosubmit.ts index 480f5382e..587a31ec7 100644 --- a/src/main/javascript/components/form/autosubmit.ts +++ b/src/main/javascript/components/form/autosubmit.ts @@ -1,3 +1,12 @@ +/** + * Adds `input` and `change` event listeners and submits forms automatically. + * + *Autosubmit can be configured with the `data-autosubmit` attribute on HTML elements. + * ```html + * + * + * ``` + */ export function initAutosubmit() { let keyupSubmit; diff --git a/src/main/javascript/components/form/index.ts b/src/main/javascript/components/form/index.ts index e3705972e..4ad61b22c 100644 --- a/src/main/javascript/components/form/index.ts +++ b/src/main/javascript/components/form/index.ts @@ -1 +1,2 @@ export * from "./autosubmit"; +export * from "./prevent-double-click-submit"; diff --git a/src/main/javascript/components/form/prevent-double-click-submit.spec.ts b/src/main/javascript/components/form/prevent-double-click-submit.spec.ts new file mode 100644 index 000000000..42ec0b718 --- /dev/null +++ b/src/main/javascript/components/form/prevent-double-click-submit.spec.ts @@ -0,0 +1,67 @@ +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "vitest"; +import { initPreventDoubleClickSubmit } from "./prevent-double-click-submit"; + +describe("DoubleClickSubmitGuard", () => { + beforeAll(() => { + initPreventDoubleClickSubmit(); + }); + + beforeEach(() => { + // prevent HTMLFormElement.prototype.requestSubmit is not implemented log. + // + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + window._virtualConsole.emit = () => {}; + }); + + afterEach(() => { + while (document.body.firstChild) { + document.body.firstChild.remove(); + } + }); + + test("disables submitter on form submit", () => { + document.body.innerHTML = ` +
+ `; + + const button = document.querySelector("button"); + + expect(button.getAttribute("disabled")).toBeNull(); + + button.click(); + + expect(button.getAttribute("disabled")).toBe(""); + }); + + test("does not disable submitter on form submit when defaultPrevented", () => { + document.body.innerHTML = ` + + `; + + let called = false; + + document.querySelector("form").addEventListener("submit", function (event) { + event.preventDefault(); + called = true; + }); + + const button = document.querySelector("button"); + + button.click(); + + expect(button.getAttribute("disabled")).toBeNull(); + expect(called).toBe(true); + }); +}); diff --git a/src/main/javascript/components/form/prevent-double-click-submit.ts b/src/main/javascript/components/form/prevent-double-click-submit.ts new file mode 100644 index 000000000..0038bd013 --- /dev/null +++ b/src/main/javascript/components/form/prevent-double-click-submit.ts @@ -0,0 +1,13 @@ +/** + * Adds a global `submit` listener and disables the submitter. + */ +export function initPreventDoubleClickSubmit() { + window.addEventListener("submit", function (event) { + if (!event.defaultPrevented) { + event.submitter?.setAttribute("disabled", ""); + // full page reload renders the form again with enabled submitter. + // maybe @hotwired/turbo is enabled somewhere. in this case, however, turbo + // handles the disabled attribute of the submitter. + } + }); +}