Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/pat/close-panel/close-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down
112 changes: 89 additions & 23 deletions src/pat/close-panel/close-panel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div id="pat-modal" class="pat-modal">
<button id="close-modal" class="close-panel">close</button>
Expand Down Expand Up @@ -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 = `
<dialog open>
<button class="close-panel">close</button>
Expand All @@ -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 = `
<div class="pat-modal">
<form action="." class="pat-validation">
<input name="ok" required />
<button class="close-panel submit">submit</button>
<button class="close-panel cancel" type="button">cancel</button>
</form>
</div>
`;
const el = document.querySelector("form");

document.body.innerHTML = `
<div class="pat-modal">
<form action="." class="pat-validation">
<input name="ok" required />
<button class="close-panel submit">submit</button>
<button class="close-panel cancel" type="button">cancel</button>
</form>
</div>
`;
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 = `
<div class="pat-modal">
<form action="." class="pat-validation">
<input name="ok" required />
<button class="close-panel submit">submit</button>
<button class="close-panel cancel" formnovalidate>cancel</button>
</form>
</div>
`;
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 = `
<div class="pat-modal">
<form action="." class="pat-validation">
<input name="ok" required />
<button class="close-panel submit">submit</button>
<input type="submit" value="cancel" class="close-panel cancel" formnovalidate />
</form>
</div>
`;
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();
});
});
});
26 changes: 9 additions & 17 deletions src/pat/collapsible/collapsible.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,27 @@ 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",
selector: this.options.scroll.selector,
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();
Expand Down Expand Up @@ -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");
Expand Down
29 changes: 25 additions & 4 deletions src/pat/collapsible/collapsible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand Down Expand Up @@ -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 = `
<div id="id1" class="pat-collapsible closed" data-pat-collapsible="scroll-selector: self; transition: none">
<p>Collapsible content</p>
<div id="id2" class="pat-collapsible closed" data-pat-collapsible="scroll-selector: none">
<p>Collapsible content</p>
</div>
</div>
`;
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 () {
Expand Down
2 changes: 1 addition & 1 deletion src/pat/collapsible/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
27 changes: 20 additions & 7 deletions src/pat/validation/documentation.md
Original file line number Diff line number Diff line change
@@ -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 |
| ------------- | -------------------------- | ------------------------------------------------------------ |
Expand All @@ -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.
Expand Down Expand Up @@ -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 |
Expand Down