solid-dismiss
Advanced tools
Comparing version 1.0.17 to 1.0.18
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
286390
3862