Skip to content

Commit

Permalink
fix(hover): handle cancelling hover events when re-entering menu items
Browse files Browse the repository at this point in the history
See #299

---------

Co-authored-by: Kenji Garland <[email protected]>
  • Loading branch information
NickDJM and synthetiv authored Jun 24, 2024
1 parent a01b53c commit a999f22
Show file tree
Hide file tree
Showing 23 changed files with 1,616 additions and 445 deletions.
19 changes: 18 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
"request": "launch",
"reAttach": true,
"file": "${workspaceFolder}/test/index.html"
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": [
"run",
"${relativeFile}"
],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
}
4 changes: 2 additions & 2 deletions dist/accessible-menu.cjs.js

Large diffs are not rendered by default.

214 changes: 134 additions & 80 deletions dist/accessible-menu.es.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/accessible-menu.iife.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/disclosure-menu.cjs.js

Large diffs are not rendered by default.

71 changes: 46 additions & 25 deletions dist/disclosure-menu.es.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var F = Object.defineProperty;
var K = (n, e, t) => e in n ? F(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
var r = (n, e, t) => (K(n, typeof e != "symbol" ? e + "" : e, t), t);
var r = (n, e, t) => K(n, typeof e != "symbol" ? e + "" : e, t);
function f(n, e) {
typeof n == "string" ? e.classList.add(n) : e.classList.add(...n);
}
Expand Down Expand Up @@ -354,17 +354,17 @@ class I {
* Sets unique IDs for the toggle and controlled menu.
*
* If the toggle and controlled menu do not have IDs, the following steps take place:
* - Generate a random 10 character string,
* - Generate a random string 1-10 characters long,
* - Get the innerText of the toggle,
* - Set the toggle's ID to: `${toggle-inner-text}-${the-random-string}-menu-button`
* - Set the menu's ID to: `${toggle-inner-text}-${the-random-string}-menu`
* - Set the toggle's ID to: `menu-button-${toggle-inner-text}-${the-random-string}`
* - Set the menu's ID to: `menu-${toggle-inner-text}-${the-random-string}`
*
* @protected
*/
_setIds() {
var e;
if (this.dom.toggle.id === "" || this.elements.controlledMenu.dom.menu.id === "") {
const t = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(0, 10);
const t = Math.random().toString(36).replace(/[^a-z]+/g, "").substring(0, 10);
let s = ((e = this.dom.toggle.innerText) == null ? void 0 : e.replace(/[^a-zA-Z0-9\s]/g, "")) || "", i = t;
!s.replace(/\s/g, "").length && this.dom.toggle.getAttribute("aria-label") && (s = this.dom.toggle.getAttribute("aria-label").replace(/[^a-zA-Z0-9\s]/g, "")), s.replace(/\s/g, "").length > 0 && (s = s.toLowerCase().replace(/\s+/g, "-"), s.startsWith("-") && (s = s.substring(1)), s.endsWith("-") && (s = s.slice(0, -1)), i = `${s}-${i}`), this.dom.toggle.id = this.dom.toggle.id || `menu-button-${i}`, this.elements.controlledMenu.dom.menu.id = this.elements.controlledMenu.dom.menu.id || `menu-${i}`;
}
Expand Down Expand Up @@ -454,7 +454,7 @@ class I {
* @public
*/
open() {
this.elements.controlledMenu.focusState = "self", this._expand(), this.isOpen = !0;
this.elements.controlledMenu.focusState = "self", this.isOpen || (this._expand(), this.isOpen = !0);
}
/**
* Opens the controlled menu without the current focus entering it.
Expand All @@ -466,7 +466,7 @@ class I {
* @public
*/
preview() {
this.elements.parentMenu && (this.elements.parentMenu.focusState = "self"), this._expand(), this.isOpen = !0;
this.elements.parentMenu && (this.elements.parentMenu.focusState = "self"), this.isOpen || (this._expand(), this.isOpen = !0);
}
/**
* Closes the controlled menu.
Expand Down Expand Up @@ -631,7 +631,7 @@ class L {
this.elements.parentMenu.shouldFocus && this.dom.link.blur();
}
}
function _(n) {
function y(n) {
try {
const e = n.key || n.keyCode, t = {
Enter: e === "Enter" || e === 13,
Expand Down Expand Up @@ -690,7 +690,7 @@ class b {
openClass: u = "show",
closeClass: C = "hide",
transitionClass: E = "transitioning",
isTopLevel: y = !0,
isTopLevel: _ = !0,
parentMenu: M = null,
hoverType: T = "off",
hoverDelay: v = 250,
Expand Down Expand Up @@ -890,7 +890,7 @@ class b {
* @type {string[]}
*/
r(this, "_errors", []);
this._dom.menu = e, this._dom.controller = c, this._dom.container = m, this._selectors.menuItems = t, this._selectors.menuLinks = s, this._selectors.submenuItems = i, this._selectors.submenuToggles = o, this._selectors.submenus = l, this._elements.menuItems = [], this._elements.submenuToggles = [], this._elements.controller = null, this._elements.parentMenu = M, this._elements.rootMenu = y ? this : null, this._openClass = u || "", this._closeClass = C || "", this._transitionClass = E || "", this._root = y, this._hoverType = T, this._hoverDelay = v, this._enterDelay = w, this._leaveDelay = S;
this._dom.menu = e, this._dom.controller = c, this._dom.container = m, this._selectors.menuItems = t, this._selectors.menuLinks = s, this._selectors.submenuItems = i, this._selectors.submenuToggles = o, this._selectors.submenus = l, this._elements.menuItems = [], this._elements.submenuToggles = [], this._elements.controller = null, this._elements.parentMenu = M, this._elements.rootMenu = _ ? this : null, this._openClass = u || "", this._closeClass = C || "", this._transitionClass = E || "", this._root = _, this._hoverType = T, this._hoverDelay = v, this._enterDelay = w, this._leaveDelay = S;
}
/**
* Initializes the menu.
Expand Down Expand Up @@ -1391,6 +1391,25 @@ class b {
this._elements.menuItems.push(t);
});
}
/**
* Clears the hover timeout.
*
* @protected
*/
_clearTimeout() {
clearTimeout(this._hoverTimeout);
}
/**
* A wrapper for setTimeout that stores the timeout in the menu.
*
* @protected
*
* @param {Function} callback - The callback function to execute.
* @param {number} delay - The delay time in milliseconds.
*/
_setTimeout(e, t) {
this._hoverTimeout = setTimeout(e, t);
}
/**
* Handles focus events throughout the menu for proper menu use.
*
Expand Down Expand Up @@ -1429,7 +1448,7 @@ class b {
t.dom.link.addEventListener(
"pointerdown",
() => {
this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusChild(s);
this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this._clearTimeout(), this.focusChild(s);
},
{ passive: !0 }
), t.isSubmenuItem && t.elements.toggle.dom.toggle.addEventListener(
Expand Down Expand Up @@ -1493,25 +1512,27 @@ class b {
e.dom.link.addEventListener("pointerenter", (s) => {
if (!(s.pointerType === "pen" || s.pointerType === "touch")) {
if (this.hoverType === "on")
this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusChild(t), e.isSubmenuItem && (this.enterDelay > 0 ? this._hoverTimeout = setTimeout(() => {
this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusChild(t), e.isSubmenuItem && (this.enterDelay > 0 ? (this._clearTimeout(), this._setTimeout(() => {
e.elements.toggle.preview();
}, this.enterDelay) : e.elements.toggle.preview());
}, this.enterDelay)) : e.elements.toggle.preview());
else if (this.hoverType === "dynamic") {
const i = this.elements.submenuToggles.some(
(o) => o.isOpen
);
this.currentChild = t, (!this.isTopLevel || this.focusState !== "none") && (this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusCurrentChild()), e.isSubmenuItem && (!this.isTopLevel || i) && (this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusCurrentChild(), this.enterDelay > 0 ? this._hoverTimeout = setTimeout(() => {
this.currentChild = t, (!this.isTopLevel || this.focusState !== "none") && (this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusCurrentChild()), e.isSubmenuItem && (!this.isTopLevel || i) && (this.currentEvent = "mouse", this.elements.rootMenu.blurChildren(), this.focusCurrentChild(), this.enterDelay > 0 ? (this._clearTimeout(), this._setTimeout(() => {
e.elements.toggle.preview();
}, this.enterDelay) : e.elements.toggle.preview());
}, this.enterDelay)) : e.elements.toggle.preview());
}
}
}), e.isSubmenuItem && e.dom.item.addEventListener("pointerleave", (s) => {
s.pointerType === "pen" || s.pointerType === "touch" || (this.hoverType === "on" ? this.leaveDelay > 0 ? (clearTimeout(this._hoverTimeout), setTimeout(() => {
}), e.isSubmenuItem && (e.dom.item.addEventListener("pointerleave", (s) => {
s.pointerType === "pen" || s.pointerType === "touch" || (this.hoverType === "on" ? this.leaveDelay > 0 ? (this._clearTimeout(), this._setTimeout(() => {
this.currentEvent = "mouse", e.elements.toggle.close();
}, this.leaveDelay)) : (this.currentEvent = "mouse", e.elements.toggle.close()) : this.hoverType === "dynamic" && (this.isTopLevel || (this.leaveDelay > 0 ? (clearTimeout(this._hoverTimeout), setTimeout(() => {
}, this.leaveDelay)) : (this.currentEvent = "mouse", e.elements.toggle.close()) : this.hoverType === "dynamic" && (this.isTopLevel || (this.leaveDelay > 0 ? (this._clearTimeout(), this._setTimeout(() => {
this.currentEvent = "mouse", e.elements.toggle.close(), this.focusCurrentChild();
}, this.leaveDelay)) : (this.currentEvent = "mouse", e.elements.toggle.close(), this.focusCurrentChild()))));
});
}), e.dom.item.addEventListener("pointerenter", (s) => {
s.pointerType === "pen" || s.pointerType === "touch" || e.isSubmenuItem && (this.hoverType === "on" || this.hoverType === "dynamic") && this.leaveDelay > 0 && this._clearTimeout();
}));
});
}
/**
Expand All @@ -1529,7 +1550,7 @@ class b {
"keydown",
(e) => {
this.currentEvent = "keyboard";
const t = _(e);
const t = y(e);
(t === "Space" || t === "Enter") && h(e);
}
);
Expand All @@ -1545,7 +1566,7 @@ class b {
_handleKeyup() {
this.isTopLevel && this.elements.controller && this.elements.controller.dom.toggle.addEventListener("keyup", (e) => {
this.currentEvent = "keyboard";
const t = _(e);
const t = y(e);
(t === "Space" || t === "Enter") && (h(e), this.elements.controller.toggle(), this.elements.controller.isOpen && this.focusFirstChild());
});
}
Expand Down Expand Up @@ -1793,7 +1814,7 @@ class A extends b {
containerElement: u = null,
openClass: C = "show",
closeClass: E = "hide",
transitionClass: y = "transitioning",
transitionClass: _ = "transitioning",
isTopLevel: M = !0,
parentMenu: T = null,
hoverType: v = "off",
Expand All @@ -1814,7 +1835,7 @@ class A extends b {
containerElement: u,
openClass: C,
closeClass: E,
transitionClass: y,
transitionClass: _,
isTopLevel: M,
parentMenu: T,
hoverType: v,
Expand Down Expand Up @@ -1946,7 +1967,7 @@ class A extends b {
_handleKeydown() {
super._handleKeydown(), this.dom.menu.addEventListener("keydown", (t) => {
this.currentEvent = "keyboard";
const s = _(t);
const s = y(t);
if (this.focusState === "self") {
const i = ["Space", "Enter"], o = ["Escape"], l = ["Escape"];
this.optionalKeySupport ? [
Expand Down Expand Up @@ -1985,7 +2006,7 @@ class A extends b {
_handleKeyup() {
super._handleKeyup(), this.dom.menu.addEventListener("keyup", (t) => {
this.currentEvent = "keyboard";
const s = _(t);
const s = y(t);
this.focusState === "self" && (s === "Space" || s === "Enter" ? this.currentMenuItem.isSubmenuItem ? (h(t), this.currentMenuItem.elements.toggle.isOpen ? this.currentMenuItem.elements.toggle.close() : this.currentMenuItem.elements.toggle.preview()) : this.currentMenuItem.dom.link.click() : s === "Escape" ? this.elements.submenuToggles.some(
(o) => o.isOpen
) ? (h(t), this.closeChildren()) : this.elements.parentMenu ? (h(t), this.elements.parentMenu.currentEvent = this.currentEvent, this.elements.parentMenu.closeChildren(), this.elements.parentMenu.focusCurrentChild()) : this.isTopLevel && this.elements.controller && this.elements.controller.isOpen && (this.elements.controller.close(), this.focusController()) : this.optionalKeySupport && (s === "ArrowDown" || s === "ArrowRight" ? (h(t), this.currentMenuItem.isSubmenuItem && this.currentMenuItem.elements.toggle.isOpen ? (this.currentMenuItem.elements.childMenu.currentEvent = "keyboard", this.currentMenuItem.elements.childMenu.focusFirstChild()) : this.focusNextChild()) : s === "ArrowUp" || s === "ArrowLeft" ? (h(t), this.focusPreviousChild()) : s === "Home" ? (h(t), this.focusFirstChild()) : s === "End" && (h(t), this.focusLastChild())));
Expand Down
Loading

0 comments on commit a999f22

Please sign in to comment.