Re: [whatwg/dom] Add an optional options dictionary to `closest` to allow jumping across shadow boundaries (Issue #1265)

GarrettS left a comment (whatwg/dom#1265)

You want to cross shadow roots all the way to the absolute top of the page, right?

You wanna find ancestors out in the main page. Method `node.getRootNode({ composed: true })` uses the word composed, so that's why you proposed it.

But the `ancestor` parameter would change closest to do a composed path walk PLUS a termination check:
```
while (currentNode) {
  if (currentNode.matches(selector)) return currentNode;
  if (currentNode === ancestor) break; // Stops it from going any further up
  currentNode = currentNode.getComposedParent();
}
```
That ancestor arg acts as a flag to traverse until it can't, across any shadow root, and to continue until ancestor matches that element, `currentNode === ancestor`. Double whammy.

Instead of an object with a property with a boolean value, you pass the element, which acts as the flag, and get MORE behavior.

The gate check is: "Is ancestor null or a node?". Nodes are nullable types, so it should work.What if you want it to stop at an ancestor that is still within the shadow root? 

Workaround 1: ID/Selector Swap Hack
```
const oldId = currentTarget.id; 
const tempId = "STOP-HERE";
ev.currentTarget.id = tempId; 

// Note: target.closest() is needed here instead of .matches() 
// so it finds buttons above the target element.
const match = target.closest(`#${tempId} button`); 
currentTarget.id = oldId;
```
Adding a boundary condition using selectors DOES NOT force a hard boundary. `closest` STILL climbs, to root. And it clunks.

Workaround 2: DOM Detachment Hack (The "Nuclear" Option)
Force a boundary by detaching the node. 
Because `closest()` only cares about the element it was called on, handles disconnected elements.
```
let next = currentTarget.nextElementSibling;
let parent = currentTarget.parentNode;
parent.removeChild(currentTarget); // Temporarily isolate the tree

let match = target.closest("button"); // Walk hits a dead end at currentTarget

parent.insertBefore(currentTarget, next); // Put it back
```
This DOES force a boundary, but it clunks. Try doing that on pointermove and see how well your app runs. Inefficient and clunks.

(And lookin back to my 2004 code, I see my ancestor traversal did not include the element itself — different from `.closest`, which does: (`document.body.closest("body")`)
https://web.archive.org/web/20040203053401/http://dhtmlkitchen.com/utils.js)


-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/1265#issuecomment-4651288465
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/dom/issues/1265/4651288465@github.com>

Received on Monday, 8 June 2026 16:49:19 UTC