// Forked from NPM stacking-order@2.0.0 // Background at https://github.com/Rich-Harris/stacking-order/issues/3 // Background at https://github.com/Rich-Harris/stacking-order/issues/6 import { assert } from ".."; /** * Determine which of two nodes appears in front of the other — * if `a` is in front, returns 1, otherwise returns -1 * @param {HTMLElement} a * @param {HTMLElement} b */ export function compare(a: HTMLElement, b: HTMLElement): number { if (a === b) throw new Error("Cannot compare node with itself"); const ancestors = { a: get_ancestors(a), b: get_ancestors(b), }; let common_ancestor; // remove shared ancestors while (ancestors.a.at(-1) === ancestors.b.at(-1)) { a = ancestors.a.pop() as HTMLElement; b = ancestors.b.pop() as HTMLElement; common_ancestor = a; } assert( common_ancestor, "Stacking order can only be calculated for elements with a common ancestor" ); const z_indexes = { a: get_z_index(find_stacking_context(ancestors.a)), b: get_z_index(find_stacking_context(ancestors.b)), }; if (z_indexes.a === z_indexes.b) { const children = common_ancestor.childNodes; const furthest_ancestors = { a: ancestors.a.at(-1), b: ancestors.b.at(-1), }; let i = children.length; while (i--) { const child = children[i]; if (child === furthest_ancestors.a) return 1; if (child === furthest_ancestors.b) return -1; } } return Math.sign(z_indexes.a - z_indexes.b); } const props = /\b(?:position|zIndex|opacity|transform|webkitTransform|mixBlendMode|filter|webkitFilter|isolation)\b/; /** @param {HTMLElement} node */ function is_flex_item(node: HTMLElement) { // @ts-ignore const display = getComputedStyle(get_parent(node) ?? node).display; return display === "flex" || display === "inline-flex"; } /** @param {HTMLElement} node */ function creates_stacking_context(node: HTMLElement) { const style = getComputedStyle(node); // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context if (style.position === "fixed") return true; // Forked to fix upstream bug https://github.com/Rich-Harris/stacking-order/issues/3 // if ( // (style.zIndex !== "auto" && style.position !== "static") || // is_flex_item(node) // ) if ( style.zIndex !== "auto" && (style.position !== "static" || is_flex_item(node)) ) return true; if (+style.opacity < 1) return true; if ("transform" in style && style.transform !== "none") return true; if ("webkitTransform" in style && style.webkitTransform !== "none") return true; if ("mixBlendMode" in style && style.mixBlendMode !== "normal") return true; if ("filter" in style && style.filter !== "none") return true; if ("webkitFilter" in style && style.webkitFilter !== "none") return true; if ("isolation" in style && style.isolation === "isolate") return true; if (props.test(style.willChange)) return true; // @ts-expect-error if (style.webkitOverflowScrolling === "touch") return true; return false; } /** @param {HTMLElement[]} nodes */ function find_stacking_context(nodes: HTMLElement[]) { let i = nodes.length; while (i--) { const node = nodes[i]; assert(node, "Missing node"); if (creates_stacking_context(node)) return node; } return null; } /** @param {HTMLElement} node */ function get_z_index(node: HTMLElement | null) { return (node && Number(getComputedStyle(node).zIndex)) || 0; } /** @param {HTMLElement} node */ function get_ancestors(node: HTMLElement | null) { const ancestors = []; while (node) { ancestors.push(node); // @ts-ignore node = get_parent(node); } return ancestors; // [ node, ... , , document ] } /** @param {HTMLElement} node */ function get_parent(node: HTMLElement) { const { parentNode } = node; if (parentNode && parentNode instanceof ShadowRoot) { return parentNode.host } return parentNode; }