Versatile DOM element interactivity checks.
npm install webfuse-com/is-interactiveimport { checkInteractivity, filterInteractive } from "@webfuse-com/is-interactive";
// Check if an element is interactive:
const click = targetElement => {
const { isInteractive } = checkInteractivity(targetElement, {
invisible: false // disables invisible check
});
if(!isInteractive) return;
automation.click(button);
};
// Create a DOM snapshot (weakly corresponding to the GUI):
const surfaceDOMSnapshot = selector => {
return filterInteractive(document, {
offViewport: true
}).outerHTML;
};// Toggle checks to perform (default: all enabled, except 'offViewport')
interface InteractivityChecks {
clipped: boolean; // true
collapsed: boolean; // true
disabled: boolean; // true
disconnected: boolean; // true
hidden: boolean; // true
inert: boolean; // true
invisible: boolean; // true
modalBlocked: boolean; // true
occluded: boolean; // true
unclickable: boolean; // true
unrendered: boolean; // true
ariaHidden: boolean; // false
offScrolled: boolean; // false
offViewport: boolean; // false
}
// Additional options (default: all disabled)
interface InteractivityOptions {
// Skip checks inherited from parents ('hidden', 'inert', 'ariaHidden', 'unrendered'),
// e.g., when parents are known to be checked already
skipParentChecks: boolean; // false
}
/**
* Check whether an element is interactive.
*/
function checkInteractivity(
element: Element,
checks?: InteractivityChecks,
options?: InteractivityOptions
): {
isInteractive: boolean;
reason?:
| "notElement"
| "clipped"
| "collapsed"
| "disabled"
| "disconnected"
| "hidden"
| "inert"
| "invisible"
| "modalBlocked"
| "occluded"
| "unclickable"
| "unrendered"
| "ariaHidden"
| "offScrolled"
| "offViewport"
}
/**
* Filter a DOM (sub)tree for interactive elements.
* Create a 'surface'-only DOM.
*/
function filterInteractive(
dom: Document | Element,
checks?: InteractivityChecks,
elementOverrideChecks:
{ [ key: string ]: Partial<InteractivityChecks> }
| Map<string, Partial<InteractivityChecks>>
): Element // virtual DOMBy default,
filterInteractivesetselementOverrideChecksto:{ "input": { clipped: false, collapsed: false, invisible: false, occluded: false, unrendered: false, }, "select": { clipped: false, collapsed: false, invisible: false, occluded: false, unrendered: false, } }This accounts for custom-style input patterns (e.g., occluded by click-receiver element).
The
occludedcheck temporarily scrolls elements into view (restored synchronously; the resulting scroll events are suppressed). WithoffViewportoroffScrolledenabled, the window or scroll containers, respectively, are not scrolled: elements that pass these checks are partially in view already.
Checks follow the rendered (flat) tree: slotted elements are assessed within their slot, and filterInteractive keeps open shadow roots in the virtual DOM. Closed shadow roots cannot be inspected: their contents are not filtered, and content slotted into them is assessed as if placed in the host. Closed <details> are handled explicitly.
Open demo/demo.html in a browser.
Actionable ⊂ Interactive ⊂ All Elements
From within a web application, an element is considered interactive when it is surfaced by the UI. That means a user can perceive it and potentially act on it. Elements that are not surfaced are therefore not interactive, for instance, an input declared as hidden.
Whether an interactive element is truly actionable, however, depends on the presence of an actuation listener. Actuation listeners, including browser-native effect, such as those associated with links or checkboxes.