Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

solid-dismiss

Package Overview
Dependencies
Maintainers
1
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

solid-dismiss - npm Package Compare versions

Comparing version 1.0.17 to 1.0.18

761

dist/esm/index.js

@@ -21,2 +21,14 @@ import { insert, template, delegateEvents, addEventListener, effect, setAttribute, classList, createComponent, mergeProps } from 'solid-js/web';

const dismissStack = [];
const addDismissStack = props => {
dismissStack.push(props);
};
const removeDismissStack = id => {
const foundIdx = dismissStack.findIndex(item => item.uniqueId === id);
if (foundIdx === -1) return;
const foundStack = dismissStack[foundIdx];
dismissStack.splice(foundIdx, 1);
return foundStack;
};
const _tabbableSelectors = ["a[href]", "area[href]", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "button:not([disabled])", "iframe", "[tabindex]", "[contentEditable=true]"].reduce((a, c, idx) => `${a}${idx ? "," : ""}${c}:not([tabindex="-1"])`, "");

@@ -208,169 +220,307 @@

/**
* Iterate stack backwards, checks item, pass it close callback. First falsy value breaks iteration.
*/
let scrollEventAddedViaTouch = false;
let scrollEventAdded = false;
let pollTimeoutId = null;
let timestampOfTabkey = 0;
let cachedScrollTarget = null;
let cachedPolledElement = null;
const globalState = {
closeByFocusSentinel: false,
closedBySetOpen: false,
addedDocumentClick: false,
documentClickTimeout: null,
closedByEvents: false,
focusedMenuBtns: new Set()
};
const onDocumentClick = e => {
const target = e.target;
checkThenClose(dismissStack, item => {
if (item.overlay || item.overlayElement || getMenuButton(item.menuBtnEls).contains(target) || item.containerEl.contains(target)) return;
return item;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
globalState.addedDocumentClick = false;
};
const onWindowBlur = e => {
const item = dismissStack[dismissStack.length - 1]; // menuPopup item was the last tabbable item in the document and current focused item is outside of document, such as browser URL bar, then menuPopup/stacks will close
const checkThenClose = (arr, checkCb, destroyCb) => {
for (let i = arr.length - 1; i >= 0; i--) {
const item = checkCb(arr[i]);
setTimeout(() => {
const difference = e.timeStamp - timestampOfTabkey;
if (item) {
destroyCb(item);
continue;
if (!document.hasFocus()) {
if (difference < 50) {
checkThenClose(dismissStack, item => item, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
return;
}
}
});
return;
}
};
const camelize = s => s.replace(/-./g, x => x.toUpperCase()[1]);
const matchByFirstChild = ({
parent,
matchEl
}) => {
if (parent === matchEl) return true;
const onBlurWindow = item => {
if (item.overlay || item.overlayEl) return;
if (!item.closeWhenDocumentBlurs) return;
const menuBtnEl = getMenuButton(item.menuBtnEls);
menuBtnEl.focus();
globalState.closedByEvents = true;
item.setOpen(false);
};
const query = el => {
if (!el) return false;
const child = el.children[0];
if (item.overlay) return;
setTimeout(() => {
const activeElement = document.activeElement;
if (child === matchEl) {
return true;
if (!activeElement || activeElement.tagName !== "IFRAME") {
checkThenClose(dismissStack, item => item, item => onBlurWindow(item));
return;
}
return query(child);
};
checkThenClose(dismissStack, item => {
const {
containerEl
} = item;
return query(parent);
if (containerEl.contains(activeElement)) {
cachedPolledElement = activeElement;
pollingIframe();
document.addEventListener("visibilitychange", onVisibilityChange);
return;
}
return item;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
});
};
const queryElement = (state, {
inputElement,
type,
subType
}) => {
if (inputElement === "menuPopup") {
return state.menuPopupEl;
const onKeyDown = e => {
const {
focusedMenuBtn,
setOpen,
menuBtnEls,
cursorKeys,
closeWhenEscapeKeyIsPressed,
focusElementOnClose,
timeouts
} = dismissStack[dismissStack.length - 1];
if (e.key === "Tab") {
timestampOfTabkey = e.timeStamp;
}
if (type === "focusElementOnOpen") {
if (inputElement === "firstChild") {
return getNextTabbableElement({
from: state.focusSentinelBeforeEl,
stopAtElement: state.containerEl
if (cursorKeys) {
onCursorKeys(e);
}
if (e.key !== "Escape" || !closeWhenEscapeKeyIsPressed) return;
const menuBtnEl = getMenuButton(menuBtnEls);
const el = queryElement({}, {
inputElement: focusElementOnClose,
type: "focusElementOnClose",
subType: "escapeKey"
}) || menuBtnEl;
if (el) {
el.focus();
if (el === menuBtnEl) {
markFocusedMenuButton({
focusedMenuBtn,
timeouts,
el
});
}
}
if (typeof inputElement === "string") {
return state.containerEl?.querySelector(inputElement);
globalState.closedByEvents = true;
setOpen(false);
};
const onScrollClose = e => {
const target = e.target;
if (cachedScrollTarget === target) return;
checkThenClose(dismissStack, item => {
const {
menuPopupEl
} = item;
if (menuPopupEl.contains(target)) {
cachedScrollTarget = target;
return null;
}
if (inputElement instanceof Element) {
return inputElement;
return item;
}, item => {
const {
setOpen,
focusElementOnClose,
menuBtnEls
} = item;
const menuBtnEl = getMenuButton(menuBtnEls);
globalState.closedByEvents = true;
setOpen(false);
const el = queryElement({}, {
inputElement: focusElementOnClose,
type: "focusElementOnClose",
subType: "scrolling"
}) || menuBtnEl;
if (el) {
el.focus();
}
});
};
const addGlobalEvents = closeWhenScrolling => {
cachedScrollTarget = null;
return inputElement();
if (!scrollEventAdded && closeWhenScrolling) {
scrollEventAdded = false;
window.addEventListener("wheel", onScrollClose, {
capture: true,
passive: true
});
document.body.addEventListener("touchmove", onTouchMove);
}
if (inputElement == null && type === "menuPopup") {
if (!state.containerEl) return null;
if (state.menuPopupEl) return state.menuPopupEl;
return state.containerEl.children[1];
}
if (dismissStack.length) return;
document.addEventListener("keydown", onKeyDown);
window.addEventListener("blur", onWindowBlur);
};
const removeGlobalEvents = () => {
if (dismissStack.length) return;
scrollEventAdded = false;
globalState.addedDocumentClick = false; // globalState.menuBtnEl = null;
if (typeof inputElement === "string" && type === "menuButton") {
return document.querySelector(inputElement);
}
window.clearTimeout(globalState.documentClickTimeout);
globalState.documentClickTimeout = null;
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("click", onDocumentClick);
window.removeEventListener("blur", onWindowBlur);
window.removeEventListener("wheel", onScrollClose, {
capture: true
});
document.body.removeEventListener("touchmove", onTouchMove);
};
if (typeof inputElement === "string" && type === "closeButton") {
if (!state.containerEl) return null;
return state.containerEl.querySelector(inputElement);
}
const onTouchMove = () => {
if (scrollEventAddedViaTouch) return;
scrollEventAddedViaTouch = true;
document.body.addEventListener("touchend", () => {
scrollEventAddedViaTouch = false;
}, {
once: true
});
window.addEventListener("scroll", onScrollClose, {
capture: true,
passive: true,
once: true
});
};
if (typeof inputElement === "string") {
return document.querySelector(inputElement);
const onCursorKeys = e => {
const keys = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"];
const horizontalKeys = ["ArrowLeft", "ArrowRight"];
if (!keys.includes(e.key)) return;
e.preventDefault();
if (horizontalKeys.includes(e.key)) return;
const {
menuBtnEls,
menuPopupEl,
containerEl,
focusSentinelBeforeEl
} = dismissStack[dismissStack.length - 1];
const menuBtnEl = getMenuButton(menuBtnEls);
let activeElement = document.activeElement;
let direction;
if (e.key === "ArrowDown") {
direction = "forwards";
} else {
direction = "backwards";
}
if (inputElement instanceof Element) {
return inputElement;
if (activeElement === menuBtnEl || activeElement === menuPopupEl || activeElement === containerEl) {
direction = "forwards";
activeElement = focusSentinelBeforeEl;
}
if (typeof inputElement === "function") {
const result = inputElement();
const el = getNextTabbableElement({
from: activeElement,
direction,
stopAtElement: menuPopupEl
});
if (result instanceof Element) {
return result;
}
if (el) {
el.focus();
}
};
if (type === "closeButton") {
if (!state.containerEl) return null;
return state.containerEl.querySelector(result);
}
const onVisibilityChange = () => {
if (document.visibilityState === "visible" && pollTimeoutId != null) {
pollingIframe();
return;
}
if (type === "focusElementOnClose") {
if (!inputElement) return null;
clearTimeout(pollTimeoutId);
}; // polls iframe to deal with edge case if menuPopup item selected is an iframe and then select another iframe that is "outside" of menuPopup
switch (subType) {
case "tabForwards":
return queryElement(state, {
inputElement: inputElement.tabForwards
});
case "tabBackwards":
return queryElement(state, {
inputElement: inputElement.tabBackwards
});
const pollingIframe = () => {
// worst case scenerio is user has to wait for up to 250ms for menuPopup to close, while average case is 125ms
const duration = 250;
case "click":
return queryElement(state, {
inputElement: inputElement.click
});
const poll = () => {
const activeElement = document.activeElement;
case "escapeKey":
return queryElement(state, {
inputElement: inputElement.escapeKey
});
if (!activeElement) {
return;
}
case "scrolling":
return queryElement(state, {
inputElement: inputElement.scrolling
});
if (cachedPolledElement === activeElement) {
pollTimeoutId = window.setTimeout(poll, duration);
return;
}
}
if (inputElement == null) return null;
checkThenClose(dismissStack, item => {
const {
containerEl
} = item;
if (Array.isArray(inputElement)) {
return inputElement.map(el => queryElement(state, {
inputElement: el,
type
}));
}
if (activeElement.tagName === "IFRAME") {
if (containerEl && !containerEl.contains(activeElement)) {
return item;
}
for (const key in inputElement) {
const item = inputElement[key];
return queryElement(state, {
inputElement: item
cachedPolledElement = activeElement;
pollTimeoutId = window.setTimeout(poll, duration);
}
return;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
cachedPolledElement = null;
pollTimeoutId = null;
document.removeEventListener("visibilitychange", onVisibilityChange);
});
}
};
return null;
pollTimeoutId = window.setTimeout(poll, duration);
};
/**
* Why this might be better than direct check of CSS display property? Because you do not need to check all parent elements. If some parent element has display: none, its children are hidden too but still has `element.style.display !== 'none'`
*/
const hasDisplayNone = el => el.offsetHeight === 0 && el.offsetWidth === 0;
const dismissStack = [];
const addDismissStack = props => {
dismissStack.push(props);
};
const removeDismissStack = id => {
const foundIdx = dismissStack.findIndex(item => item.uniqueId === id);
if (foundIdx === -1) return;
const foundStack = dismissStack[foundIdx];
dismissStack.splice(foundIdx, 1);
return foundStack;
};
const onFocusFromOutsideAppOrTab = (state, e) => {

@@ -664,307 +814,156 @@ const {

let scrollEventAddedViaTouch = false;
let scrollEventAdded = false;
let pollTimeoutId = null;
let timestampOfTabkey = 0;
let cachedScrollTarget = null;
let cachedPolledElement = null;
const globalState = {
closeByFocusSentinel: false,
closedBySetOpen: false,
addedDocumentClick: false,
documentClickTimeout: null,
closedByEvents: false,
focusedMenuBtns: new Set()
};
const onDocumentClick = e => {
const target = e.target;
checkThenClose(dismissStack, item => {
if (item.overlay || item.overlayElement || getMenuButton(item.menuBtnEls).contains(target) || item.containerEl.contains(target)) return;
return item;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
globalState.addedDocumentClick = false;
};
const onWindowBlur = e => {
const item = dismissStack[dismissStack.length - 1]; // menuPopup item was the last tabbable item in the document and current focused item is outside of document, such as browser URL bar, then menuPopup/stacks will close
/**
* Iterate stack backwards, checks item, pass it close callback. First falsy value breaks iteration.
*/
setTimeout(() => {
const difference = e.timeStamp - timestampOfTabkey;
const checkThenClose = (arr, checkCb, destroyCb) => {
for (let i = arr.length - 1; i >= 0; i--) {
const item = checkCb(arr[i]);
if (!document.hasFocus()) {
if (difference < 50) {
checkThenClose(dismissStack, item => item, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
return;
}
if (item) {
destroyCb(item);
continue;
}
});
const onBlurWindow = item => {
if (item.overlay || item.overlayEl) return;
if (!item.closeWhenDocumentBlurs) return;
const menuBtnEl = getMenuButton(item.menuBtnEls);
menuBtnEl.focus();
globalState.closedByEvents = true;
item.setOpen(false);
};
return;
}
};
const camelize = s => s.replace(/-./g, x => x.toUpperCase()[1]);
const matchByFirstChild = ({
parent,
matchEl
}) => {
if (parent === matchEl) return true;
if (item.overlay) return;
setTimeout(() => {
const activeElement = document.activeElement;
const query = el => {
if (!el) return false;
const child = el.children[0];
if (!activeElement || activeElement.tagName !== "IFRAME") {
checkThenClose(dismissStack, item => item, item => onBlurWindow(item));
return;
if (child === matchEl) {
return true;
}
checkThenClose(dismissStack, item => {
const {
containerEl
} = item;
return query(child);
};
if (containerEl.contains(activeElement)) {
cachedPolledElement = activeElement;
pollingIframe();
document.addEventListener("visibilitychange", onVisibilityChange);
return;
}
return item;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
});
});
return query(parent);
};
const onKeyDown = e => {
const {
focusedMenuBtn,
setOpen,
menuBtnEls,
cursorKeys,
closeWhenEscapeKeyIsPressed,
focusElementOnClose,
timeouts
} = dismissStack[dismissStack.length - 1];
if (e.key === "Tab") {
timestampOfTabkey = e.timeStamp;
const queryElement = (state, {
inputElement,
type,
subType
}) => {
if (inputElement === "menuPopup") {
return state.menuPopupEl;
}
if (cursorKeys) {
onCursorKeys(e);
if (inputElement === "menuButton") {
return getMenuButton(state.menuBtnEls);
}
if (e.key !== "Escape" || !closeWhenEscapeKeyIsPressed) return;
const menuBtnEl = getMenuButton(menuBtnEls);
const el = queryElement({}, {
inputElement: focusElementOnClose,
type: "focusElementOnClose",
subType: "escapeKey"
}) || menuBtnEl;
if (el) {
el.focus();
if (el === menuBtnEl) {
markFocusedMenuButton({
focusedMenuBtn,
timeouts,
el
if (type === "focusElementOnOpen") {
if (inputElement === "firstChild") {
return getNextTabbableElement({
from: state.focusSentinelBeforeEl,
stopAtElement: state.containerEl
});
}
}
globalState.closedByEvents = true;
setOpen(false);
};
const onScrollClose = e => {
const target = e.target;
if (cachedScrollTarget === target) return;
checkThenClose(dismissStack, item => {
const {
menuPopupEl
} = item;
if (menuPopupEl.contains(target)) {
cachedScrollTarget = target;
return null;
if (typeof inputElement === "string") {
return state.containerEl?.querySelector(inputElement);
}
return item;
}, item => {
const {
setOpen,
focusElementOnClose,
menuBtnEls
} = item;
const menuBtnEl = getMenuButton(menuBtnEls);
globalState.closedByEvents = true;
setOpen(false);
const el = queryElement({}, {
inputElement: focusElementOnClose,
type: "focusElementOnClose",
subType: "scrolling"
}) || menuBtnEl;
if (el) {
el.focus();
if (inputElement instanceof Element) {
return inputElement;
}
});
};
const addGlobalEvents = closeWhenScrolling => {
cachedScrollTarget = null;
if (!scrollEventAdded && closeWhenScrolling) {
scrollEventAdded = false;
window.addEventListener("wheel", onScrollClose, {
capture: true,
passive: true
});
document.body.addEventListener("touchmove", onTouchMove);
return inputElement();
}
if (dismissStack.length) return;
document.addEventListener("keydown", onKeyDown);
window.addEventListener("blur", onWindowBlur);
};
const removeGlobalEvents = () => {
if (dismissStack.length) return;
scrollEventAdded = false;
globalState.addedDocumentClick = false; // globalState.menuBtnEl = null;
if (inputElement == null && type === "menuPopup") {
if (!state.containerEl) return null;
if (state.menuPopupEl) return state.menuPopupEl;
return state.containerEl.children[1];
}
window.clearTimeout(globalState.documentClickTimeout);
globalState.documentClickTimeout = null;
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("click", onDocumentClick);
window.removeEventListener("blur", onWindowBlur);
window.removeEventListener("wheel", onScrollClose, {
capture: true
});
document.body.removeEventListener("touchmove", onTouchMove);
};
if (typeof inputElement === "string" && type === "menuButton") {
return document.querySelector(inputElement);
}
const onTouchMove = () => {
if (scrollEventAddedViaTouch) return;
scrollEventAddedViaTouch = true;
document.body.addEventListener("touchend", () => {
scrollEventAddedViaTouch = false;
}, {
once: true
});
window.addEventListener("scroll", onScrollClose, {
capture: true,
passive: true,
once: true
});
};
const onCursorKeys = e => {
const keys = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"];
const horizontalKeys = ["ArrowLeft", "ArrowRight"];
if (!keys.includes(e.key)) return;
e.preventDefault();
if (horizontalKeys.includes(e.key)) return;
const {
menuBtnEls,
menuPopupEl,
containerEl,
focusSentinelBeforeEl
} = dismissStack[dismissStack.length - 1];
const menuBtnEl = getMenuButton(menuBtnEls);
let activeElement = document.activeElement;
let direction;
if (e.key === "ArrowDown") {
direction = "forwards";
} else {
direction = "backwards";
if (typeof inputElement === "string") {
return document.querySelector(inputElement);
}
if (activeElement === menuBtnEl || activeElement === menuPopupEl || activeElement === containerEl) {
direction = "forwards";
activeElement = focusSentinelBeforeEl;
if (inputElement instanceof Element) {
return inputElement;
}
const el = getNextTabbableElement({
from: activeElement,
direction,
stopAtElement: menuPopupEl
});
if (typeof inputElement === "function") {
const result = inputElement();
if (el) {
el.focus();
}
};
if (result instanceof Element) {
return result;
}
const onVisibilityChange = () => {
if (document.visibilityState === "visible" && pollTimeoutId != null) {
pollingIframe();
return;
if (type === "closeButton") {
if (!state.containerEl) return null;
return state.containerEl.querySelector(result);
}
}
clearTimeout(pollTimeoutId);
}; // polls iframe to deal with edge case if menuPopup item selected is an iframe and then select another iframe that is "outside" of menuPopup
if (type === "focusElementOnClose") {
if (!inputElement) return null;
switch (subType) {
case "tabForwards":
return queryElement(state, {
inputElement: inputElement.tabForwards
});
const pollingIframe = () => {
// worst case scenerio is user has to wait for up to 250ms for menuPopup to close, while average case is 125ms
const duration = 250;
case "tabBackwards":
return queryElement(state, {
inputElement: inputElement.tabBackwards
});
const poll = () => {
const activeElement = document.activeElement;
case "click":
return queryElement(state, {
inputElement: inputElement.click
});
if (!activeElement) {
return;
}
case "escapeKey":
return queryElement(state, {
inputElement: inputElement.escapeKey
});
if (cachedPolledElement === activeElement) {
pollTimeoutId = window.setTimeout(poll, duration);
return;
case "scrolling":
return queryElement(state, {
inputElement: inputElement.scrolling
});
}
}
checkThenClose(dismissStack, item => {
const {
containerEl
} = item;
if (inputElement == null) return null;
if (activeElement.tagName === "IFRAME") {
if (containerEl && !containerEl.contains(activeElement)) {
return item;
}
if (Array.isArray(inputElement)) {
return inputElement.map(el => queryElement(state, {
inputElement: el,
type
}));
}
cachedPolledElement = activeElement;
pollTimeoutId = window.setTimeout(poll, duration);
}
return;
}, item => {
const {
setOpen
} = item;
globalState.closedByEvents = true;
setOpen(false);
cachedPolledElement = null;
pollTimeoutId = null;
document.removeEventListener("visibilitychange", onVisibilityChange);
for (const key in inputElement) {
const item = inputElement[key];
return queryElement(state, {
inputElement: item
});
};
}
pollTimeoutId = window.setTimeout(poll, duration);
return null;
};
/**
* Why this might be better than direct check of CSS display property? Because you do not need to check all parent elements. If some parent element has display: none, its children are hidden too but still has `element.style.display !== 'none'`
*/
const hasDisplayNone = el => el.offsetHeight === 0 && el.offsetWidth === 0;
const addMenuPopupEl = state => {

@@ -971,0 +970,0 @@ const {

@@ -0,1 +1,2 @@

import { getMenuButton } from "../local/menuButton";
import { getNextTabbableElement } from "./tabbing";

@@ -50,2 +51,5 @@ /**

}
if (inputElement === "menuButton") {
return getMenuButton(state.menuBtnEls);
}
if (type === "focusElementOnOpen") {

@@ -76,7 +80,2 @@ if (inputElement === "firstChild") {

}
if (typeof inputElement === "string" && type === "closeButton") {
if (!state.containerEl)
return null;
return state.containerEl.querySelector(inputElement);
}
if (typeof inputElement === "string") {

@@ -83,0 +82,0 @@ return document.querySelector(inputElement);

@@ -11,3 +11,3 @@ import { TLocalState } from "./localState";

el: HTMLElement;
} & Pick<TLocalState, "focusedMenuBtn" | "timeouts">) => void;
} & Pick<TLocalState, "timeouts" | "focusedMenuBtn">) => void;
export declare const removeMenuButtonEvents: (state: TLocalState, onCleanup?: boolean) => void;
{
"name": "solid-dismiss",
"version": "1.0.17",
"version": "1.0.18",
"homepage": "https://aquaductape.github.io/solid-dismiss/",

@@ -5,0 +5,0 @@ "description": "Handles \"click outside\" behavior for popup menu. Closing is triggered by click/focus outside of popup element or pressing \"Escape\" key.",

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc