From 31c7c42027266c1a05db286cf2facb224cb3abc2 Mon Sep 17 00:00:00 2001 From: sebnitu Date: Tue, 24 Sep 2024 22:27:51 -0700 Subject: [PATCH] Create ModalEntry and DrawerEntry classes --- packages/core/src/js/Collection.js | 8 +- packages/drawer/index.js | 2 +- .../drawer/src/js/{index.js => Drawer.js} | 17 +-- packages/drawer/src/js/DrawerEntry.js | 134 ++++++++++++++++++ packages/drawer/src/js/deregister.js | 12 -- packages/drawer/src/js/register.js | 122 ---------------- packages/modal/index.js | 2 +- packages/modal/src/js/{index.js => Modal.js} | 17 +-- packages/modal/src/js/ModalEntry.js | 70 +++++++++ packages/modal/src/js/deregister.js | 9 -- packages/modal/src/js/register.js | 65 --------- packages/popover/src/js/PopoverEntry.js | 23 +-- 12 files changed, 238 insertions(+), 243 deletions(-) rename packages/drawer/src/js/{index.js => Drawer.js} (83%) create mode 100644 packages/drawer/src/js/DrawerEntry.js delete mode 100644 packages/drawer/src/js/deregister.js delete mode 100644 packages/drawer/src/js/register.js rename packages/modal/src/js/{index.js => Modal.js} (85%) create mode 100644 packages/modal/src/js/ModalEntry.js delete mode 100644 packages/modal/src/js/deregister.js delete mode 100644 packages/modal/src/js/register.js diff --git a/packages/core/src/js/Collection.js b/packages/core/src/js/Collection.js index 9124eb914..039fb8adc 100644 --- a/packages/core/src/js/Collection.js +++ b/packages/core/src/js/Collection.js @@ -38,9 +38,7 @@ export class Collection { } async deregister(id) { - const index = this.collection.findIndex((entry) => { - return (entry.id === id); - }); + const index = this.collection.findIndex((entry) => entry.id === id); if (~index) { // Get the collection entry object from the collection and unmount it. const entry = this.collection[index]; @@ -87,6 +85,8 @@ export class Collection { if ("afterMount" in this && typeof this.afterMount == "function") { await this.afterMount(); } + + return this; } async unmount() { @@ -104,5 +104,7 @@ export class Collection { if ("afterUnmount" in this && typeof this.afterUnmount == "function") { await this.afterUnmount(); } + + return this; } } diff --git a/packages/drawer/index.js b/packages/drawer/index.js index 5c9e845d8..4206cf87b 100644 --- a/packages/drawer/index.js +++ b/packages/drawer/index.js @@ -1,3 +1,3 @@ -import Drawer from "./src/js"; +import { Drawer } from "./src/js/Drawer"; export default Drawer; diff --git a/packages/drawer/src/js/index.js b/packages/drawer/src/js/Drawer.js similarity index 83% rename from packages/drawer/src/js/index.js rename to packages/drawer/src/js/Drawer.js index b77f1fcd3..2325e49ef 100644 --- a/packages/drawer/src/js/index.js +++ b/packages/drawer/src/js/Drawer.js @@ -1,14 +1,13 @@ import { Collection, FocusTrap, localStore } from "@vrembem/core"; import defaults from "./defaults"; +import { DrawerEntry } from "./DrawerEntry"; import { handleClick, handleKeydown } from "./handlers"; -import { register } from "./register"; -import { deregister } from "./deregister"; import { open } from "./open"; import { close } from "./close"; import { toggle } from "./toggle"; -export default class Drawer extends Collection { +export class Drawer extends Collection { #handleClick; #handleKeydown; @@ -28,6 +27,10 @@ export default class Drawer extends Collection { }); } + async createEntry(context, el, config) { + return new DrawerEntry(context, el, config); + } + async open(id, transition, focus) { return open.call(this, id, transition, focus); } @@ -40,14 +43,6 @@ export default class Drawer extends Collection { return toggle.call(this, id, transition, focus); } - async beforeRegister(entry, config) { - return register.call(this, entry, config); - } - - async beforeDeregister(entry) { - return deregister.call(this, entry); - } - async afterMount() { document.addEventListener("click", this.#handleClick, false); document.addEventListener("keydown", this.#handleKeydown, false); diff --git a/packages/drawer/src/js/DrawerEntry.js b/packages/drawer/src/js/DrawerEntry.js new file mode 100644 index 000000000..f17dc66fd --- /dev/null +++ b/packages/drawer/src/js/DrawerEntry.js @@ -0,0 +1,134 @@ +import { Entry, Breakpoint } from "@vrembem/core"; +import { switchMode } from "./switchMode"; +import { applyInitialState } from "./helpers"; +import { getBreakpoint } from "./helpers"; + +export class DrawerEntry extends Entry { + #mode; + #state; + #breakpoint; + + constructor(context, query) { + super(context, query); + this.dialog = null; + this.trigger = null; + // Create an instance of the Breakpoint class. + this.#breakpoint = new Breakpoint(); + // Set indeterminate values of mode, state and inlineState. + this.#mode, this.#state, this.inlineState = "indeterminate"; + } + + get breakpoint() { + return getBreakpoint.call(this.context, this.el); + } + + get store() { + return this.context.store.get(this.id); + } + + get mode() { + return this.#mode; + } + + set mode(value) { + this.#mode = value; + switchMode.call(this.context, this); + } + + get state() { + return this.#state; + } + + set state(value) { + this.#state = value; + + // If mode is inline and not in a transitioning state... + if (this.mode === "inline" && value != "opening" && value != "closing") { + // Save the inline state. + this.inlineState = value; + + // Save the store state if enabled. + if (this.getSetting("store")) { + this.context.store.set(this.id, value); + } + } + + // If state is indeterminate, remove the state classes. + if (value === "indeterminate") { + this.el.classList.remove(this.getSetting("stateOpened")); + this.el.classList.remove(this.getSetting("stateOpening")); + this.el.classList.remove(this.getSetting("stateClosed")); + this.el.classList.remove(this.getSetting("stateClosing")); + } + } + + async beforeMount() { + // Set the dialog element. If none is found, use the root element. + const dialog = this.el.querySelector(this.getSetting("selectorDialog")); + this.dialog = (dialog) ? dialog : this.el; + + // Set tabindex="-1" so dialog is focusable via JS or click. + if (this.getSetting("setTabindex")) { + this.dialog.setAttribute("tabindex", "-1"); + } + + // Set both the initial state and inline state. + applyInitialState(this); + + // Set the inline state. + this.inlineState = this.state; + + // Set the initial mode. + this.mode = (this.el.classList.contains(this.getSetting("classModal"))) ? "modal" : "inline"; + + if (this.breakpoint) { + this.mountBreakpoint(); + } + } + + async beforeUnmount(close = true) { + // If entry is in the opened state, close it. + if (close && this.state === "opened") { + await this.close(false); + } + + // Remove entry from local store. + this.store.set(this.id); + + // Unmount the MatchMedia functionality. + this.unmountBreakpoint(); + } + + async open(transition, focus) { + return this.context.open(this, transition, focus); + } + + async close(transition, focus) { + return this.context.close(this, transition, focus); + } + + async toggle(transition, focus) { + return this.context.toggle(this, transition, focus); + } + + async deregister() { + return this.context.deregister(this.id); + } + + mountBreakpoint() { + const value = this.breakpoint; + const handler = this.handleBreakpoint.bind(this); + this.#breakpoint.mount(value, handler); + } + + unmountBreakpoint() { + this.#breakpoint.unmount(); + } + + handleBreakpoint(event) { + const bpMode = (event.matches) ? "inline" : "modal"; + if (this.mode != bpMode) { + this.mode = bpMode; + } + } +} diff --git a/packages/drawer/src/js/deregister.js b/packages/drawer/src/js/deregister.js deleted file mode 100644 index e06eda830..000000000 --- a/packages/drawer/src/js/deregister.js +++ /dev/null @@ -1,12 +0,0 @@ -export async function deregister(entry, close = true) { - // If entry is in the opened state, close it. - if (close && entry.state === "opened") { - await entry.close(false); - } - - // Remove entry from local store. - this.store.set(entry.id); - - // Unmount the MatchMedia functionality. - entry.unmountBreakpoint(); -} diff --git a/packages/drawer/src/js/register.js b/packages/drawer/src/js/register.js deleted file mode 100644 index 32667eead..000000000 --- a/packages/drawer/src/js/register.js +++ /dev/null @@ -1,122 +0,0 @@ -import { Breakpoint } from "@vrembem/core"; -import { switchMode } from "./switchMode"; -import { applyInitialState } from "./helpers"; -import { getBreakpoint } from "./helpers"; - -export async function register(entry, config = {}) { - // Save root this for use inside methods API. - const root = this; - - // Create an instance of the Breakpoint class. - const breakpoint = new Breakpoint(); - - // Setup private variables and their default values if any. - let _mode, _state = "indeterminate"; - - // Build on the entry object. - Object.assign(entry, { - dialog: null, - trigger: null, - inlineState: "indeterminate", - open(transition, focus) { - return root.open(this, transition, focus); - }, - close(transition, focus) { - return root.close(this, transition, focus); - }, - toggle(transition, focus) { - return root.toggle(this, transition, focus); - }, - deregister() { - return root.deregister(this); - }, - mountBreakpoint() { - const value = this.breakpoint; - const handler = this.handleBreakpoint.bind(this); - breakpoint.mount(value, handler); - return this; - }, - unmountBreakpoint() { - breakpoint.unmount(); - return this; - }, - handleBreakpoint(event) { - const bpMode = (event.matches) ? "inline" : "modal"; - if (this.mode != bpMode) { - this.mode = bpMode; - } - return this; - } - }); - - // Create getters and setters. - Object.defineProperties(entry, Object.getOwnPropertyDescriptors({ - get breakpoint() { - return getBreakpoint.call(root, this.el); - }, - get store() { - return root.store.get(this.id); - }, - get mode() { - return _mode; - }, - set mode(value) { - _mode = value; - switchMode.call(root, this); - }, - get state() { - return _state; - }, - set state(value) { - _state = value; - - // If mode is inline and not in a transitioning state... - if (this.mode === "inline" && value != "opening" && value != "closing") { - // Save the inline state. - this.inlineState = value; - - // Save the store state if enabled. - if (this.getSetting("store")) { - root.store.set(this.id, value); - } - } - - // If state is indeterminate, remove the state classes. - if (value === "indeterminate") { - this.el.classList.remove(this.getSetting("stateOpened")); - this.el.classList.remove(this.getSetting("stateOpening")); - this.el.classList.remove(this.getSetting("stateClosed")); - this.el.classList.remove(this.getSetting("stateClosing")); - } - }, - })); - - // Build the setting objects. - entry.applySettings(config); - entry.getDataConfig(); - - // Set the dialog element. If none is found, use the root element. - const dialog = entry.el.querySelector(entry.getSetting("selectorDialog")); - entry.dialog = (dialog) ? dialog : entry.el; - - // Set tabindex="-1" so dialog is focusable via JS or click. - if (entry.getSetting("setTabindex")) { - entry.dialog.setAttribute("tabindex", "-1"); - } - - // Set both the initial state and inline state. - applyInitialState(entry); - - // Set the inline state. - entry.inlineState = entry.state; - - // Set the initial mode. - entry.mode = (entry.el.classList.contains(entry.getSetting("classModal"))) ? "modal" : "inline"; - - if (entry.breakpoint) { - entry.mountBreakpoint(); - } - - // Return the registered entry. - return entry; -} diff --git a/packages/modal/index.js b/packages/modal/index.js index 375d40ed6..084608959 100644 --- a/packages/modal/index.js +++ b/packages/modal/index.js @@ -1,3 +1,3 @@ -import Modal from "./src/js"; +import { Modal } from "./src/js/Modal"; export default Modal; diff --git a/packages/modal/src/js/index.js b/packages/modal/src/js/Modal.js similarity index 85% rename from packages/modal/src/js/index.js rename to packages/modal/src/js/Modal.js index 655847761..8906cc1d2 100644 --- a/packages/modal/src/js/index.js +++ b/packages/modal/src/js/Modal.js @@ -1,9 +1,8 @@ import { Collection, FocusTrap } from "@vrembem/core"; import defaults from "./defaults"; +import { ModalEntry } from "./ModalEntry"; import { handleClick, handleKeydown } from "./handlers"; -import { register } from "./register"; -import { deregister } from "./deregister"; import { open } from "./open"; import { close } from "./close"; import { closeAll } from "./closeAll"; @@ -11,7 +10,7 @@ import { replace } from "./replace"; import { stack } from "./stack"; import { updateFocusState } from "./helpers"; -export default class Modal extends Collection { +export class Modal extends Collection { #handleClick; #handleKeydown; @@ -30,6 +29,10 @@ export default class Modal extends Collection { return this.stack.top; } + async createEntry(context, el, config) { + return new ModalEntry(context, el, config); + } + async open(id, transition, focus) { return open.call(this, id, transition, focus); } @@ -51,14 +54,6 @@ export default class Modal extends Collection { return result; } - async beforeRegister(entry, config) { - return register.call(this, entry, config); - } - - async beforeDeregister(entry) { - return deregister.call(this, entry); - } - async afterMount() { document.addEventListener("click", this.#handleClick, false); document.addEventListener("keydown", this.#handleKeydown, false); diff --git a/packages/modal/src/js/ModalEntry.js b/packages/modal/src/js/ModalEntry.js new file mode 100644 index 000000000..48c5d4544 --- /dev/null +++ b/packages/modal/src/js/ModalEntry.js @@ -0,0 +1,70 @@ +import { Entry } from "@vrembem/core"; + +export class ModalEntry extends Entry { + constructor(context, query) { + super(context, query); + this.state = "closed"; + this.dialog = null; + } + + get isRequired() { + return this.dialog.matches(this.getSetting("selectorRequired")); + } + + async beforeMount() { + // Set the dialog element. If none is found, use the root element. + const dialog = this.el.querySelector(this.getSetting("selectorDialog")); + this.dialog = (dialog) ? dialog : this.el; + + // Set aria-modal attribute to true. + this.dialog.setAttribute("aria-modal", "true"); + + // If a role attribute is not set, set it to "dialog" as the default. + if (!this.dialog.hasAttribute("role")) { + this.dialog.setAttribute("role", "dialog"); + } + + // Set tabindex="-1" so dialog is focusable via JS or click. + if (this.getSetting("setTabindex")) { + this.dialog.setAttribute("tabindex", "-1"); + } + + // Setup initial state. + if (this.el.classList.contains(this.getSetting("stateOpened"))) { + // Open entry with transitions disabled. + await this.open(false); + } else { + // Remove transition state classes. + this.el.classList.remove(this.getSetting("stateOpening")); + this.el.classList.remove(this.getSetting("stateClosing")); + // Add closed state class. + this.el.classList.add(this.getSetting("stateClosed")); + } + } + + async beforeUnmount(close = true) { + // If entry is in the opened state, close it. + if (close && this.state === "opened") { + await this.close(false); + } else { + // Remove modal from stack. + this.stack.remove(this); + } + } + + async open(transition, focus) { + return this.context.open(this, transition, focus); + } + + async close(transition, focus) { + return this.context.close(this, transition, focus); + } + + async replace(transition, focus) { + return this.context.replace(this, transition, focus); + } + + async deregister() { + return this.context.deregister(this.id); + } +} diff --git a/packages/modal/src/js/deregister.js b/packages/modal/src/js/deregister.js deleted file mode 100644 index 123f7f520..000000000 --- a/packages/modal/src/js/deregister.js +++ /dev/null @@ -1,9 +0,0 @@ -export async function deregister(entry, close = true) { - // If entry is in the opened state, close it. - if (close && entry.state === "opened") { - await entry.close(false); - } else { - // Remove modal from stack. - this.stack.remove(entry); - } -} diff --git a/packages/modal/src/js/register.js b/packages/modal/src/js/register.js deleted file mode 100644 index a0734823b..000000000 --- a/packages/modal/src/js/register.js +++ /dev/null @@ -1,65 +0,0 @@ -export async function register(entry, config = {}) { - // Save root this for use inside methods API. - const root = this; - - // Build on the entry object. - Object.assign(entry, { - state: "closed", - dialog: null, - open(transition, focus) { - return root.open(this, transition, focus); - }, - close(transition, focus) { - return root.close(this, transition, focus); - }, - replace(transition, focus) { - return root.replace(this, transition, focus); - }, - deregister() { - return root.deregister(this); - } - }); - - // Create getters and setters. - Object.defineProperties(entry, Object.getOwnPropertyDescriptors({ - get isRequired() { - return this.dialog.matches(this.getSetting("selectorRequired")); - }, - })); - - // Build the setting objects. - entry.applySettings(config); - entry.getDataConfig(); - - // Set the dialog element. If none is found, use the root element. - const dialog = entry.el.querySelector(entry.getSetting("selectorDialog")); - entry.dialog = (dialog) ? dialog : entry.el; - - // Set aria-modal attribute to true. - entry.dialog.setAttribute("aria-modal", "true"); - - // If a role attribute is not set, set it to "dialog" as the default. - if (!entry.dialog.hasAttribute("role")) { - entry.dialog.setAttribute("role", "dialog"); - } - - // Set tabindex="-1" so dialog is focusable via JS or click. - if (entry.getSetting("setTabindex")) { - entry.dialog.setAttribute("tabindex", "-1"); - } - - // Setup initial state. - if (entry.el.classList.contains(entry.getSetting("stateOpened"))) { - // Open entry with transitions disabled. - await entry.open(false); - } else { - // Remove transition state classes. - entry.el.classList.remove(entry.getSetting("stateOpening")); - entry.el.classList.remove(entry.getSetting("stateClosing")); - // Add closed state class. - entry.el.classList.add(entry.getSetting("stateClosed")); - } - - // Return the registered entry. - return entry; -} diff --git a/packages/popover/src/js/PopoverEntry.js b/packages/popover/src/js/PopoverEntry.js index e7f9c615b..a3317bac7 100644 --- a/packages/popover/src/js/PopoverEntry.js +++ b/packages/popover/src/js/PopoverEntry.js @@ -14,23 +14,25 @@ export class PopoverEntry extends Entry { constructor(context, query) { super(context, query); - this.floatingCleanup = () => {}; this.state = "closed"; this.toggleDelayId = null; - this.trigger = document.querySelector( - `[aria-controls="${this.id}"], [aria-describedby="${this.id}"]` - ); + this.trigger = null; this.#eventListeners = null; this.#isHovered = { el: false, trigger: false }; + this.floatingCleanup = () => {}; } get isTooltip() { return !!this.el.closest(this.getSetting("selectorTooltip")) || this.el.getAttribute("role") == "tooltip"; } + get isHovered() { + return this.#isHovered.el || this.#isHovered.trigger; + } + set isHovered(event) { // The state can either be true, false or undefined based on event type. const state = (event.type == "mouseenter") ? true : (event.type == "mouseleave") ? false : undefined; @@ -47,11 +49,12 @@ export class PopoverEntry extends Entry { } } - get isHovered() { - return this.#isHovered.el || this.#isHovered.trigger; - } - async beforeMount() { + // Get the trigger element. + this.trigger = document.querySelector( + `[aria-controls="${this.id}"], [aria-describedby="${this.id}"]` + ); + // If it's a tooltip... if (this.isTooltip) { // Set the event to hover role="tooltip" attribute. @@ -61,8 +64,10 @@ export class PopoverEntry extends Entry { // Set aria-expanded to false if trigger has aria-controls attribute. this.trigger.setAttribute("aria-expanded", "false"); } + // Setup event listeners. this.registerEventListeners(); + // Set initial state based on the presence of the active class. if (this.el.classList.contains(this.settings.stateActive)) { this.open(); @@ -77,8 +82,10 @@ export class PopoverEntry extends Entry { if (this.state === "opened") { this.close(); } + // Clean up the floating UI instance. this.floatingCleanup(); + // Remove event listeners. this.deregisterEventListeners(); }