diff --git a/src/pat/close-panel/close-panel.js b/src/pat/close-panel/close-panel.js index a46e49dad..2f8436ea4 100644 --- a/src/pat/close-panel/close-panel.js +++ b/src/pat/close-panel/close-panel.js @@ -25,6 +25,7 @@ export default Base.extend({ await utils.timeout(0); // Wait for other patterns, like pat-validation. if ( + e.target.matches(":not([formnovalidate])") && e.target.matches("[type=submit], button:not([type=button])") && this.el.closest("form")?.checkValidity() === false ) { diff --git a/src/pat/close-panel/close-panel.test.js b/src/pat/close-panel/close-panel.test.js index b202d46c0..2444b6756 100644 --- a/src/pat/close-panel/close-panel.test.js +++ b/src/pat/close-panel/close-panel.test.js @@ -10,7 +10,7 @@ describe("pat close-panel", function () { document.body.innerHTML = ""; }); - it("Closes a modal's panel.", async function () { + it("1 - Closes a modal's panel.", async function () { document.body.innerHTML = `
@@ -42,7 +42,7 @@ describe("pat close-panel", function () { expect(document.querySelectorAll(".pat-modal").length).toBe(0); }); - it("Closes a dialog's panel.", async function () { + it("2 - Closes a dialog's panel.", async function () { document.body.innerHTML = ` @@ -61,32 +61,98 @@ describe("pat close-panel", function () { expect(dialog.open).toBe(false); }); - it("Prevents closing a panel with an invalid form when submitting but allow to cancel and close.", async function () { - const spy_destroy_modal = jest.spyOn(pat_modal.prototype, "destroy"); + describe("3 - Prevents closing a panel with an invalid form when submitting but allow to cancel and close.", function () { + it("3.1 - ... when the cancel button is not a submit button", async function () { + const spy_destroy_modal = jest.spyOn(pat_modal.prototype, "destroy"); + + document.body.innerHTML = ` +
+
+ + + +
+
+ `; + const el = document.querySelector("form"); - document.body.innerHTML = ` -
-
- - - -
-
- `; - const el = document.querySelector("form"); + registry.scan(document.body); + await utils.timeout(1); // wait a tick for async to settle. + + el.querySelector("button.submit").click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(spy_destroy_modal).not.toHaveBeenCalled(); + + // A non-submit close-panel button does not check for validity. + el.querySelector("button.cancel").click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(spy_destroy_modal).toHaveBeenCalled(); + + spy_destroy_modal.mockRestore(); + }); + + it("3.2 - ... when the cancel button is a submit button but has the formnovalidate attribute set", async function () { + const spy_destroy_modal = jest.spyOn(pat_modal.prototype, "destroy"); + + document.body.innerHTML = ` +
+
+ + + +
+
+ `; + const el = document.querySelector("form"); + + registry.scan(document.body); + await utils.timeout(1); // wait a tick for async to settle. + + el.querySelector("button.submit").click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(spy_destroy_modal).not.toHaveBeenCalled(); + + // A non-submit close-panel button does not check for validity. + el.querySelector("button.cancel").click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(spy_destroy_modal).toHaveBeenCalled(); + + spy_destroy_modal.mockRestore(); + }); + + it("3.3 - ... when the cancel button is a submit input but has the formnovalidate attribute set", async function () { + const spy_destroy_modal = jest.spyOn(pat_modal.prototype, "destroy"); + + document.body.innerHTML = ` +
+
+ + + +
+
+ `; + const el = document.querySelector("form"); + + registry.scan(document.body); + await utils.timeout(1); // wait a tick for async to settle. - registry.scan(document.body); - await utils.timeout(1); // wait a tick for async to settle. + el.querySelector("button.submit").click(); + await utils.timeout(1); // wait a tick for async to settle. - el.querySelector("button.submit").click(); - await utils.timeout(1); // wait a tick for async to settle. + expect(spy_destroy_modal).not.toHaveBeenCalled(); - expect(spy_destroy_modal).not.toHaveBeenCalled(); + // A non-submit close-panel button does not check for validity. + el.querySelector("input.cancel").click(); + await utils.timeout(1); // wait a tick for async to settle. - // A non-submit close-panel button does not check for validity. - el.querySelector("button.cancel").click(); - await utils.timeout(1); // wait a tick for async to settle. + expect(spy_destroy_modal).toHaveBeenCalled(); - expect(spy_destroy_modal).toHaveBeenCalled(); + spy_destroy_modal.mockRestore(); + }); }); }); diff --git a/src/pat/collapsible/collapsible.js b/src/pat/collapsible/collapsible.js index 484dd1f44..32a160564 100644 --- a/src/pat/collapsible/collapsible.js +++ b/src/pat/collapsible/collapsible.js @@ -106,15 +106,8 @@ class Pattern extends BasePattern { $(document).on("click", this.options.openTrigger, this.open.bind(this)); } - // scroll debouncer for later use. - this.debounce_scroll = utils.debounce( - this._scroll.bind(this), - 10, - debounce_scroll_timer - ); - // pat-scroll support - if (this.options.scroll?.selector) { + if (this.options.scroll?.selector && this.options.scroll.selector !== "none") { const Scroll = (await import("../scroll/scroll")).default; this.scroll = new Scroll(this.el, { trigger: "manual", @@ -122,19 +115,18 @@ class Pattern extends BasePattern { offset: this.options.scroll?.offset, }); await events.await_pattern_init(this.scroll); + + // scroll debouncer for later use. + this.debounce_scroll = utils.debounce( + this.scroll.scrollTo.bind(this.scroll), + 10, + debounce_scroll_timer + ); } return $el; } - async _scroll() { - const scroll_selector = this.options.scroll?.selector; - if (!scroll_selector) { - return; - } - await this.scroll.scrollTo(); - } - open() { if (!this.$el.hasClass("open")) { this.toggle(); @@ -196,7 +188,7 @@ class Pattern extends BasePattern { if (new_state === "open") { this.$el.trigger("patterns-collapsible-open"); this._transit(this.$el, "closed", "open"); - this.debounce_scroll(); + this.debounce_scroll?.(); // debounce scroll, if available. } else { this.$el.trigger("patterns-collapsible-close"); this._transit(this.$el, "open", "closed"); diff --git a/src/pat/collapsible/collapsible.test.js b/src/pat/collapsible/collapsible.test.js index 6e1c2a2fe..0647cdfbb 100644 --- a/src/pat/collapsible/collapsible.test.js +++ b/src/pat/collapsible/collapsible.test.js @@ -147,13 +147,12 @@ describe("pat-collapsible", function () { `; const collapsible = document.querySelector(".pat-collapsible"); const instance = new Pattern(collapsible, { transition: "none" }); - const spy_scroll = jest.spyOn(instance, "_scroll"); await events.await_pattern_init(instance); instance.toggle(); await utils.timeout(10); - expect(spy_scroll).toHaveBeenCalledTimes(1); + expect(this.spy_scrollTo).toHaveBeenCalledTimes(1); }); it("8.2 - does not scroll when being closed.", async function () { @@ -164,13 +163,12 @@ describe("pat-collapsible", function () { `; const collapsible = document.querySelector(".pat-collapsible"); const instance = new Pattern(collapsible, { transition: "none" }); - const spy_scroll = jest.spyOn(instance, "_scroll"); await events.await_pattern_init(instance); instance.toggle(); await utils.timeout(10); - expect(spy_scroll).not.toHaveBeenCalled(); + expect(this.spy_scrollTo).not.toHaveBeenCalled(); }); it("8.3 - only scrolls once even if multiple collapsible are opened at once.", async function () { @@ -238,6 +236,29 @@ describe("pat-collapsible", function () { const arg_1 = this.spy_scrollTo.mock.calls[0][0]; expect(arg_1.top).toBe(40); // the offset is substracted from the scroll position, so a negative offset is added to the scroll position and stops AFTER the target position. }); + + it("8.6 - disables scrolling if a parent pat-collapsible has enabled it.", async function () { + document.body.innerHTML = ` +
+

Collapsible content

+
+

Collapsible content

+
+
+ `; + const collapsible_1 = document.querySelector("#id1"); + const instance_1 = new Pattern(collapsible_1); + await events.await_pattern_init(instance_1); + + const collapsible_2 = document.querySelector("#id2"); + const instance_2 = new Pattern(collapsible_2); + await events.await_pattern_init(instance_2); + + instance_2.toggle(); + await utils.timeout(10); + + expect(this.spy_scrollTo).not.toHaveBeenCalled(); + }); }); it("9 - triggers the pat-update event.", async function () { diff --git a/src/pat/collapsible/documentation.md b/src/pat/collapsible/documentation.md index 93fbafa88..7870318c1 100644 --- a/src/pat/collapsible/documentation.md +++ b/src/pat/collapsible/documentation.md @@ -169,5 +169,5 @@ attribute. The available options are: | `transition` | `slide` | Transition effect when opening or closing a collapsinble. Must be one of `none`, `css`, `fade`, `slide` or `slide-horizontal`. | | `effect-duration` | `fast` | Duration of transition. This is ignored if the transition is `none` or `css`. | | `effect-easing` | `swing` | Easing to use for the open/close animation. This must be a known jQuery easing method. jQuery includes `swing` and `linear`, but more can be included via jQuery UI. | -| `scroll-selector` | | CSS selector or `self`. Defines which element will be scrolled into view. `self` if it is the collapsible element itself. | +| `scroll-selector` | | CSS selector, `self` or `none`. Defines which element will be scrolled into view. `self` if it is the collapsible element itself. `none` to disable scrolling if a scrolling selector is inherited from a parent pat-collapsible element. | | `scroll-offset` | | `offset` in pixels to stop scrolling before the target position defines by `scroll-selector`. Can also be a negative number. | diff --git a/src/pat/validation/documentation.md b/src/pat/validation/documentation.md index 82f00c353..394e82c65 100644 --- a/src/pat/validation/documentation.md +++ b/src/pat/validation/documentation.md @@ -1,18 +1,22 @@ ## Description -This pattern provides a simple but powerful form validation beyond what HTML5 offers. +This pattern provides form validation based on the HTML standard and offers extended functionality like custom error messages and extra validation rules. + ## Documentation -The validation pattern is triggered by a single class `pat-validation` on the form tag. The rest is handled mostly with standard HTML5 validation attributes. +The validation pattern is triggered by a single class `pat-validation` on the form tag. +The rest is handled mostly with standard HTML validation attributes. + +This patterns offers: + +- extra validation rules like checking for equality or checking is one date it after another. +- custom error messages. -This pattern has several advantages over standard HTML 5 form validation: +Since it is based on the HTML standard you can still use the `:valid`, `:invalid` and `:out-of-range` CSS pseudo classes. -- it supports older browsers -- it uses simple documented HTML markup to allow non-browser-specific styling of error messages -- it supports extra validation rules +You can use any HTML form validation attributes but here are some examples: -### The following attributes may be used. | Name | Syntax | Description | | ------------- | -------------------------- | ------------------------------------------------------------ | @@ -23,6 +27,10 @@ This pattern has several advantages over standard HTML 5 form validation: | Maximum value | `type="number" max="10"` | Check if a number is less than or equal to a given value. | | Real number | `type="number" step="any"` | Check if a number is less than or equal to a given value. | + +> **_NOTE:_** The form inputs must have a `name` attribute, otherwise the validation would not happen. + + ### Error messages Error messages are inserted into the DOM as `em` elements with a `message warning` class. @@ -65,6 +73,11 @@ Error messages can also be overridden on a per-field basis, for example: ### Options reference +> **_NOTE:_** The form inputs must have a `name` attribute, otherwise the validation would not happen. + +> **_NOTE:_** If you need to exclude a submit button from form validation - like a cancel button which actually submits - add the `formnovalidate` attribute to the button. + + | Property | Description | Default | Type | | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------- | | disable-selector | A selector for elements that should be disabled when there are errors in the form. | | CSS Selector |