Description
What is the relationship between Element
type IDL attributes and Shadow DOM encapsulation?
(Previous general discussion, and concepts and terminology)
For example, suppose an author wants to set an aria-activedescendant
-associated element, descendant
, on an element host
.
host.ariaActiveDescendantElement = descendant;
To what extent, for what reasons, and at what cost, should the user agent prevent the author from setting this relationship if descendant
is, say, closed-shadow-hidden[1] from host
?
Why prevent referring to closed-shadow-hidden elements?
In general, shadow DOM is intended to hide implementation details in order to prevent authors depending on those details, and thus causing things to break if those implementation details change.
From https://gist.github.com/alice/174ae481dacdae9c934e3ecb2f752ccb:
- May cause problems if scripts "accidentally" walk into deeper shadow content via known properties
e.g.// lightEl.ariaActiveDescendantElement was set by the component to be // an element within the component's shadow DOM lightEl.ariaActiveDescendantElement.textContent = "new text"; // ** Now it is possible to access the entire shadow tree for the component! ** lightEl.ariaActiveDescendantElement .parent .appendChild(document.createTextNode("🆕"));
- (Note that extension scripts often already have access to Shadow DOM in any case; the concern here would be page scripts.)
This roughly corresponds to "type 1 encapsulation", as described by @othermaciej and @annevk: "Encapsulation against accidental exposure — DOM nodes from the shadow tree are not leaked via pre-existing generic APIs — for example, events flowing out of a shadow tree don't expose shadow nodes as the event target."
The caveat is that in this case, an author would have to have explicitly set the ariaLabelledByElement
property to be a closed-shadow-hidden element; that is, the author would have to already have access to those nodes, rather than the nodes being leaked with no action from the author.
However, the author did not deliberately provide access to every element inside shadow DOM.
- Provides a surface for developers to "hack" their way into depending on implementation details of components, if components expose elements within shadow DOM, e.g.
// In this method, component sets `for` on lightDOMElement // to an <input> inside Shadow DOM. // The author uses this to get access to elements inside Shadow DOM, // creating an implicit dependency on Shadow DOM internals. component.setLabel(lightDOMLabelElement); lightDOMLabelElement.for.style.backgroundColor = "papayawhip"; lightDOMLabelElement.for = null;
This corresponds to "type 2 encapsulation" in the same framework: "Encapsulation against deliberate access — no API is provided which lets code outside the component poke at the shadow DOM. Only internals that the component chooses to expose are exposed."
The same caveat from above applies.
- May be cited as a precedent for other APIs which may also be construed as weakening Shadow DOM encapsulation.
This might be called the "rule of inference" problem: the potential to open the door to other APIs which allow access to closed-shadow-hidden elements.
Possible solutions and trade-offs
0. Do nothing/authoring advice
Advise authors against setting closed-shadow-hidden elements as attr-associated elements for elements in light DOM, but do nothing to prevent them.
Optional (0.1): allow (but do not require) using opaque references in place of element references. This allows authors to preserve their own encapsulation in the case where they wish to make a semantic connection between two elements where the connection would otherwise leak implementation details. (Obviously, this requires setting up the spec and implementation machinery for opaque references.)
1. Allow access but prevent tree walking
Allow setting references to closed-shadow-hidden elements, but prevent using them to access the rest of the shadow tree.
This might look something like: at get
time, check whether the attr-associated element is closed-shadow-hidden relative to the host element, and if so, do not return it (i.e. return null
instead).
It might make sense to return something like an opaque reference instead, to avoid confusion as to whether a reference has been set.
lightEl.ariaActiveDescendantElement = shadowHiddenElement;
assertEquals(lightEl.ariaActiveDescendantElement,
document.getOpaqueReference(shadowHiddenElement));
Variations:
-
(1.1) Always return an opaque reference, rather than only when the attr-associated element is closed-shadow-hidden. This would avoid having a polymorphic getter.
lightEl.ariaActiveDescendantElement = lightSibling; assertEquals(lightEl.ariaActiveDescendantElement, document.getOpaqueRef(lightSibling));
-
(1.2) Make the API fully opaque reference based, requiring authors to generate an opaque reference before setting an explicitly-set attr-associated element, e.g.
lightEl.ariaActiveDescendantElement = document.getOpaqueRef(lightSibling);
2. Check on setting
Fail silently by setting the reference to null
if the given value is not a descendant of any of the host element's shadow-including ancestors. (This is what is in the current spec PR.)
lightEl.ariaActiveDescendantElement = shadowHiddenElement;
assertEquals(lightEl.ariaActiveDescendantElement, null);
This does not prevent authors from removing elements from the light DOM and re-adding them into shadow DOM, however.
lightEl.ariaActiveDescendantElement = lightSibling;
lightEl.shadowRoot.appendChild(lightSibling)
assertEquals(lightEl.ariaActiveDescendantElement, lightSibling);
3. Check on getting
This is similar to (1), but also affects the computed attr-associated element, impacting accessibility APIs etc.
That being the case, this runs into issues since some of those APIs need timely updates when attr-associated elements change (for example, setting aria-activedescendant
is equivalent to a focus change for some API consumers) so if an element were to be re-parented causing the association to be effectively lost, there would not be a timely update since there is no "get" in that case.
4. Check on setting and during the "adopt node" algorithm
In addition to preventing attr-associations being created where the attr-associated element is closed-shadow-hidden relative to the host node, add steps to the "adopt a node" algorithm to check that for each attr-association an element is participating in (either a host or as an attr-associated element), the attr-associated element isn't currently closed-shadow-hidden relative to the host.
This potentially involves a significant run-time cost, as checking whether one element is closed shadow-hidden relative to another can involve an ancestor walk.
5. Check on setting, and disallow references to elements which are not connected
This would effectively prevent references being created to closed-shadow-hidden elements, but at the cost of not being able to create associations to elements which are not yet inserted into the DOM, which was one of the requirements described by @zcorpan at TPAC.
[1] The same logic probably applies to open shadow roots, in general, but there doesn't seem to be a named concept for "open shadow hidden".