Html: Add element reflection counterparts to id-reference counterparts

Created on 27 Feb 2018  路  92Comments  路  Source: whatwg/html

HTML has several content attributes which associate two elements with each other:

  • label's for (single element)
  • output's for (multiple elements)
  • button/fieldset/input/object/output/select/textarea's form (single element)
  • input's list (single element)

Right now these are reflected into IDL attributes in a variety of ways:

  • label's for: DOMString IDL attribute directly reflecting the attribute
  • output's for: DOMTokenList IDL attribute directly reflecting the attribute
  • button/fieldset/input/object/output/select/textarea's form: readonly HTMLFormElement? IDL attribute that computes the form element in question
  • input's list: readonly HTMLElement? IDL attribute that computes the datalist element in question

We'd like to add a way for these kind of element associations to be done across shadow trees, and without depending on IDs. The basic idea is that we'd like to add a read-write labelEl.htmlForElement property; by setting it, you can create the label association for any element.

The details of this are a bit tricky. We used to have one model for this, used by the contextMenu global IDL attribute, but that was only ever implemented in one browser, and removed in https://github.com/whatwg/html/commit/f0f7a14c4eed844d6e099731e17dd993d626059a. Nevertheless, it provides a good precedent. Some questions to answer when developing such a model (not exhaustive):

  • What happens to inputEl.htmlFor and the for="" content attribute if inputEl.htmlForElement is set to...

    • ... an element with an ID...

    • ... an element with no ID...

    • ... and the for="" content attribute was previously set

    • ... and the for="" content attribute was not previously set

  • Similar questions for outputEl.for, complicated by it being a DOMTokenList instead of a DOMString

I believe @alice and others had a proposed model for this. It's worth comparing it with the model in the commit that was removed.

The other tricky part is figuring out how to integrate with the readonly variants currently in place for button/fieldset/input/object/output/select/textarea's form and input's list. Can we extend them to be read-write with general semantics? I'm not sure. Also they don't end in Element :-/. Maybe they aren't as important to worry about, compared to label's for?

One big motivation for this is that ARIA would like to add cross-shadow-root-capable, non-ID-based element reflection for many of its properties. This will require both single-element (input's for) and list-of-elements (output's for) reflection.

accessibility additioproposal impacts documentation shadow

Most helpful comment

@jnurthen Right, that was my hypothesis. I'm of the opinion, based on Steve's reasoning, that we shouldn't block features based on that fact. What do you think?

All 92 comments

Our model is to have a pattern where any attribute which can take an IDREF has a corresponding property which can take an element reference, and the element reference property takes precedence while not interfering with the IDREF property:

// Reflects to "aria-labelledby" DOM attribute
button.ariaLabelledBy = "id1";

// DOM result: <button aria-labelledby="id1">

// Takes precedence; does not affect reflected attribute value
el.ariaLabelledByElements = [el2, el3];

// DOM remains unchanged: <button aria-labelledby="id1">

This is fleshed out in detail in this pull request to the AOM explainer.

Great. Would you be up for contributing spec text to HTML's reflection section for those kind of semantics? And perhaps working through the questions with regard to how this integrates with the existing attributes I listed in the OP?

Absolutely! Where is best to start?

I did some thinking and I think the best plan is the following:

  • Work on the general framework for element reflection
  • Use it for input.htmlForElement and output.htmlForElements in this spec to validate that it works well
  • Leave the other cases in the OP for later:

    • We may be able to make input.list use the general framework, but it's not trivial because currently it only ever returns datalist elements; if the ID is set to a non-datalist element, it returns null. That's not needed for the htmlFor and ARIA cases so let's leave it out for now.

    • Given the complicated definitions of the .form attribute getters, the best we'll be able to do is add setters that behave similar to our general framework, leaving the getters with their current complex behavior.

Given this, we should be able to dive right in and just define the element reflection. To do that, write spec text in the similar style to all of the other reflection paragraphs in https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect. The deleted https://github.com/whatwg/html/commit/f0f7a14c4eed844d6e099731e17dd993d626059a is probably a reasonable guide, although it has some style issues that we could deal with in review. Also the semantics there are probably different than the ones you're suggesting, but just in terms of paragraph structure etc. it can be helpful.

If you can get a pull request started with that kind of definition, we can collaborate on polishing it and getting it merged. Feel free to reach out if anything's unclear!

Hm - I don't think we're defining a type of reflection here at all. The DOMString IDREF attribute will continue to reflect to a string; a non-reflected _property_ will sit alongside and take precedence.

I will try and make a PR to that effect...

Anything that ties together a property (IDL attribute) and an attribute (content attribute) is reflection. See the other examples in that section.

It's definitely possible for there to be multiple properties reflecting a single attribute, as long as their interaction is well-defined. For example, rel and relList, or className and classList.

The only interaction is precedence. The value of one does not in any way affect the value of the other, in either direction.

For example:

// Reflects to "aria-labelledby" DOM attribute
button.ariaLabelledBy = "id1";

// DOM result: <button aria-labelledby="id1">

console.log(el.ariaLabelledByElements);  // prints "[]"

Oh... That seems like unfortunate design :/. I was hoping there would be a way to get the element using the element API even when people use markup to create the associations, like was done in the contextMenu commit referenced above.

@cookiecrook was opposed to the reflection piece - perhaps he has more to say about it?

Meanwhile I could try and pull something together to describe the reflection situation we had discussed earlier to see how it flies 馃槃

This seems like a superset of #3219. For label in particular I still think

An alternative that might make sense and preserves the encapsulation boundary is that we allow a shadow host to be labeled, and that it can decide somehow who gets focus.

makes a lot of sense.

In fact, anything that crosses the shadow boundary without preserving encapsulation is likely a non-starter given past positions on that topic from Apple and Mozilla.

Encapsulation is preserved; you can only access elements you're given access to. Nothing "leaks" out of the shadow root; you can only assign htmlForElement to something you already have a reference to, i.e. not things in closed shadow roots (unless they are explicitly re-exposed by the author).

That is fair, but I think a design where you reference the shadow host and its shadow root gets to delegate is a lot cleaner and makes it easier to reuse components.

I disagree; I think that's a lot of extra machinery to add to the straightforward label association stuff in existence. If we want to make an element (which may or may not be a shadow host) labelable that is not currently one of the labelable ones, that should require opt-in of the sort being discussed in the web components repo right now, so that it can also get the many other properties that come along with it (e.g. being form associated or having form data).

I think all of those make sense on top of shadow roots. And allow for an experience that is equivalent to built-in elements. Which might also consist of multiple elements, and focus one of them when label-clicked, but don't reveal any of that to the outside.

They make sense on top of shadow roots, yes, but only because they also make sense on top of any element. (Or at least, any element which is designated to be form-associated; currently we do that by designating a certain class of elements, by local name, as form-associated. We don't decide that by whether or not they have a shadow root.)

lets root our unidentified element with a mirror shadow and bifurcate that on a link to another dns using 8.8.8.8. convert ipv4 syntax to ipv6=8.8.8.8 & 8.8.4.4=2001:4860:4860::8888
2001:4860:4860::8844...

@alice wrote:

@cookiecrook was opposed to the reflection piece - perhaps he has more to say about it?

Clarifying:

I'm fine with string attribute reflection and with defining element references. I was opposed to specifying _element reference reflection_ outside the context of the larger web platform context. In other words, I am opposed to defining it in the ARIA spec, and even the the AOM spec, because the general pattern belongs in HTML, not in an accessibility-specific module.

I'm not opposed to element ref reflection if we can get implementors to agree on how it should work in the broader context.

This took me way too long, but I made a start on trying to write up the Element/IDREF reflection piece: https://github.com/whatwg/html/compare/master...alice:idref-reflection?expand=1

@domenic Could you possibly take a look and let me know if I'm on the right track? If so, I'll tackle the sequence<Element> case.

@alice probably best to send a pull request so I can do inline comments. High level comments:

  • No need to talk about the relationship between _attr_ and _attrElement_. It's a bit confusing, because stating that something is done "as normal" makes one wonder---are there other parts of the spec that might not behave as normal, unless explicitly stated?
  • You can just define it based on type, not name, like all the other reflections are. So anything that does a reflection on an IDL attribute whose type is HTMLElement, just like the other sections talk about reflections on IDL attributes whose types are DOMString or similar.
  • State the "on getting" and "on setting" steps as an algorithm. In particular you can use things like "return", instead of "(stopping at the first point where a value is returned)" and "the IDL attribute attrElement must return null".
  • When there is no matching element with id, do you want to set the id content attribute to the empty string, or remove it entirely?

probably best to send a pull request
Will do.

No need to talk about the relationship between attr and attrElement.

To be clear, I'm describing a three way relationship between:

  • attr content attribute
  • attr IDL attribute (converted from snake-case to camelCase, which is not obvious here)
  • attrElement IDL attribute

Is there a better way to describe what's going on?

It's a bit confusing, because stating that something is done "as normal" makes one wonder---are there other parts of the spec that might not behave as normal, unless explicitly stated?

Should I just remove "as normal"?

You can just define it based on type, not name, like all the other reflections are. So anything that does a reflection on an IDL attribute whose type is HTMLElement, just like the other sections talk about reflections on IDL attributes whose types are DOMString or similar.

Right, but there are three different things here, so it's not like the other reflections.

State the "on getting" and "on setting" steps as an algorithm. In particular you can use things like "return", instead of "(stopping at the first point where a value is returned)" and "the IDL attribute attrElement must return null".

Will do - this language was copied from the original language, happy to bring it up to date.

When there is no matching element with id, do you want to set the id content attribute to the empty string, or remove it entirely?

Good question. The original spec (https://github.com/whatwg/html/commit/f0f7a14c4eed844d6e099731e17dd993d626059a) had it being set to the empty string, but we needn't go with that. I think probably the empty string at least indicates that a value might exist.

Either way this is going to mess with everyone's accessibility linters, but perhaps the empty string will mess with them slightly less.

There are not three interrelated things here. There are two separate IDL attributes, both of which affect and are affected by the underlying content attribute. The description of the HTMLElement-typed one should not reference the other one. It should stand independently on top of the content attribute, just like the existing reflection algorithms do.

In other words, there's no need to mention the existing IDL attributes, because they already work as expected, via the existing definition plus the various sentences that say things like "the htmlFor IDL attribute reflects the for content attribute."

Your goal is to be able to insert a sentence like "the htmlForElement IDL attribute reflects the for content attribute", and have that be meaningful, because you've defined what "reflects" means for an IDL attribute whose type is HTMLElement.

Hmm, sorry, upon re-reviewing, I realized there's a deeper issue here. Which is that, as specced in your PR (either with or without with my suggested edits), the following will not work:

labelEl.htmlForElement = document.createElement("input");

That is:

  • Since the value being set doesn't have an ID attribute, what will happen if you follow the "on setting" algorithm in the PR is that it sets labelEl's for content attribute to the empty string.
  • Then, if you do console.log(labelEl.htmlForElement), following the "on getting" algorithm in the PR will return null, since there is no element with id equal to the empty string.

I assume the goal was to allow association beyond just with an ID, right? For shadow DOM and such.

In that case, we'd need some deeper surgery.

Here's one approach:

  • We want to make htmlForElement actually impact the element's labeled control computation. We choose to not have any impact on the for="" content attribute.
  • Thus, we add a new concept, e.g. "label elements can have an explicitly-set labeled control, initially null."
  • We define the getter for htmlForElement to return this label element's explicitly-set labeled control. And we define the setter to set its explicitly-set labeled control. We don't use any notion of "reflection" to define htmlForElement at all.
  • We update the algorithm for computing the "labeled control" (currently specified, somewhat confusingly, by the two paragraphs following "Except where otherwise specified by the following rules, a label element has no labeled control."). In particular we just add as a first step something like "If the label element has a non-null explicitly-set labeled control, then its labeled control is given by its explicitly-set labeled control."

I'm not sure this is the best approach, though. It has the following drawbacks:

  • Doesn't easily generalize, like we were hoping. Although in theory every attribute like this should have a counterpart concept (like "labeled control"), in practice editing all such concept-computation algorithms is annoying, assuming they exist at all.
  • Totally ignores the for="" content attribute, so even if you set htmlForElement to something that does have an ID, the for="" content attribute doesn't get updated. (And thus the htmlFor IDL attribute also doesn't get updated.)

I can try to think of something better tomorrow. It indeed may require intertwingling all three concepts as you were envisioning. Or maybe just generalizing the above framework to e.g. install an "explicitly set X-attribute element" on the element whenever such an _attr_Element IDL attribute exists. Thoughts and suggestions welcome, if you're able to get to this before me :).

I'd repeat what I stated during an in-person meeting a while ago.

We'd be opposed to proposals in which elements inside a shadow tree would be exposed when using one of these script-based associations. That's a stop stopper issue for us.

Thanks rniwa. It's good to know not every browser would implement this feature.

@rniwa To clarify, I recall you saying that setting a relationship in the other direction - i.e. from inside a shadow root to an element in the host's tree - was ok. Is that still the case, and/or did I mis-remember?

Yes, if a node A is in a shadow tree of shadow host B, then accessing any node C which is in the same tree of B or its ancestor tree (i.e. only getting out of a shadow tree but not getting in) is okay.

To be clear, I think Mozilla would also not be okay with exposing shadow tree elements from the outside. (I'm also not quite sure about linking ancestor trees as currently these kind of relationships are always same-tree, but from an encapsulation-perspective that seems fine.)

cc @smaug----

@annevk Right, the current "same-tree" behaviour is problematic:

Imagine you're implementing a custom combo box (analogous to <input list=foo>), following the ARIA 1.1 Authoring Practices guidelines for the combobox pattern:

  • This widget consists of a single line text box and an associated pop-up list of autocomplete options.
  • When the combobox popup is visible, the textbox element has aria-controls set to a value that refers to the combobox popup element.

The textbox element is likely to be inside the shadow root, unless this is a customized built in element. However, the pop-up options will likely need to be provided by the page author using the element, so the aria-controls relationship will need to cross the shadow boundary in order not to create a broken experience.

Things like <label> are unfortunately broken in the other direction as well (e.g. in the above case, a <label> could not be used to label the internal <input>, although an aria-labelledby relationship would work) and may need an alternative solution.

It's not clear to me what the objection to tree-crossing would be here. The code hooking the two elements up has full access to both elements, and can easily make them aware of each other in various ways. That is, this restriction isn't preserving any encapsulation; it's only preventing someone who was already granted access to two elements, from hooking them up.

If the code creating closed shadow root somehow lets other code which is using the host element to access any shadow DOM, all shadow DOM is exposed and the existence of closed shadow DOM is rather pointless.
But this issue isn't too clear... what are the use cases and some concrete example would be nice.

@smaug---- Were you able to follow the example I outlined above, or should I add some example code as well?

I don't see any example. I mean something which shows what would be inside shadow DOM and what outside and how one would use such host element.

Yeah, that is not an example, but high level description or some such.
I'd like to see some concrete examples here, so that it is crystal clear what all we're discussing about. (It happens quite often in the spec issues that we end up discussing about several different things.)

@smaug---- The AOM explainer has the same example (more or less) written out as code.

ok, that example uses light DOM reference inside Shadow DOM, which should be fine.
(so, I guess the referenced element should be always in ancestor tree, not in shadow tree under the element which got the reference).

But it isn't clear to me what code sets activeDescendantElement.
Some other code somewhere might add and remove elements under custom-optionlist - what thing would update activeDescendantElement?
With IDREFs all the updates happen automatically inside browser engine, but what is the expected programming model with these referenced elements? MutationObserver somewhere? Custom Element callbacks? something else?

@smaug---- In general, it's up to the author to make sure the relationship is specified correctly - whether that is the custom element author or the page author, or whether these are the same person, doesn't obviously impact the design of this feature in my view.

The same issue would happen if using an IDREF without a shadow root and the element with the ID referred to by the IDREF was removed from the DOM - the update which occurs automatically in this case is that the IDREF is no longer valid. The other details remain the same.

I think the relevant question for this discussion is what happens when an element reference referred to in a property is removed from the DOM.

So, for example, we have

<custom-combobox>
  #shadow-root (open)
  |  <input></input>
  |  <slot></slot>
  <custom-optionlist>
    <x-option>Option 1</x-option>
    <x-option>Option 2</x-option>
    <x-option>Option 3</x-option>
 </custom-optionlist>
</custom-combobox>
var selectedOption = optionList.firstChild;
input.ariaActiveDescendantElement = selectedOption;

And then optionList mutates, and selectedOption is removed from the DOM.

I think the two questions here are:

  1. From the point of view of an author who has access to input, what should happen when they try to access input.ariaActiveDescendantElement?
  2. From the point of view of browser code computing the accessibility tree mapping, and/or from the point of view of an assistive technology user, what should happen in this situation?

For (1), I think it's consistent that input.ariaActiveDescendantElement still return selectedOption, since a reference is still valid even though the element is no longer in the DOM. It is up to the author to ensure that input has a valid active descendant, regardless of implementation details.

For (2), the advice in the Core Accessibility API Mappings has the following advice:

When the aria-activedescendant attribute changes on an element that currently has DOM focus, remove the focused state from the previously focused object and fire an accessibility API desktop focus event on the new active descendant. If aria-activedescendant is cleared or does not point to an element in the current document, fire a desktop focus event for the container object that had the attribute change.

I think this advice still applies, although it would need to be updated to refer to the ariaActiveDescendantElement property and explain the precedence between the two. This means that, since ariaActiveDescendantElement has not changed, no focus event would be fired - however, since the node was removed from the DOM, a tree change event for that node would already have been fired, so accessibility focus should already have moved, likely to input since it would have DOM focus.

The superset of that question, actually, is what happens to the foo IDL/content attribute if the element referred to by fooElement changes in such a way that foo gets out of date:

  • the element referred to by fooElement gains, changes or loses its id attribute
  • the element referred to by fooElement has an id attribute and is added to or removed from the node tree where the element with the content attribute lives

On further reflection, I think these are the problems @smaug---- was referring to?

It seems like our two options are:

  • on any mutation of the element referred to by fooElement, update foo to match, or
  • allow foo to get out of date, developers must always refer to fooElement for the ground truth.

Good catch, yeah, that is tricky.

The "on any mutation" option seems expensive for minor benefit.

The "allow foo to get out of date" model is a bit weird. I could live with it; not sure about others.

A third option would be a different model that doesn't try to keep foo in sync with fooElement, but instead sets up a cascade where they vary completely independently and the browser consults them in order (either first the foo="" content attribute/foo IDL attribute, and if that's null, then it uses the fooElement IDL attribute; or first the fooElement IDL attribute, and if that's null, it uses the foo="" content attribute/foo IDL attribute).

Seems like across two PRs we've come full circle to my original proposal.

Should we discuss that here before I write up a third spec?

At this point, I don't have a strong preference. Here's my rather long summary of where I think we are:

The primary issue we want to solve in AOM is that of non-tree (i.e. something other than parent/child) relationships which cross shadow boundaries. This means that IDREFs alone are insufficient,
because they are intrinsically limited to a same tree relationship.

IDREFs are also frustrating as they require a tree-unique ID for any element which needs to be the target of a relationship, which can add noise to the DOM and requires either careful bookkeeping or the use of GUIDs.

These two problems are felt more acutely when using ARIA attributes, since the only other non-tree relationship in the DOM is label/for, and that has a built-in workaround to IDREF usage with the label wrapping behaviour.

So, we've discussed three (or four, or five) options to allow element reflection as a solution to these problems here.

For IDREF-based content attribute, attr (only considering IDREFs, and not IDREF lists, for now):

  1. Have two "intertwingled" IDL attributes to which it is said to reflect:

    • attr

    • attrElement

The relationship between attr and attrElement is essentially:

  • Where possible, attr is equal to the ID of attrElement.
  • If attrElement is set to something which makes that not possible, e.g. an element with no ID, attr is the empty string.
  • If attr is set to something which makes that not possible, e.g. an ID which is not in use, attrElement is null

In terms of a code example:
html <div id="el" attr="related-el"></div> <div id="related-el"></div> <div class="no-id"></div>
```js
console.log(el.attr); // "related-el"
console.log(el.attrElement); // equivalent to $("#related-el")

el.attrElement = $(".no-id");
console.log(el.attr); // empty string
console.log(el.attrElement); // logs reference to the ".no-id" element

el.attr = "garbage";
console.log(el.attr); // "garbage"
console.log(el.attrElement); // null

el.removeAttribute("attr"); // OR el.attrElement = null;
console.log(el.attr); // null
console.log(el.attrElement); // null

el.attrElement = $("#related-el");
console.log(el.attr); // "related-el"
console.log(el.attrElement); // equivalent to $("#related-el")
```

  1. Have one IDL attribute, attr to which it is said to reflect, with the type (DOMString or Element)
    (or a subclass of Element).

    • If attr was set as a DOMString, either via IDL or via the content attribute, it's a DOMString.
    • If attr was set as an Element, it's an element.
    <div id="el" attr="related-el"></div>
    <div id="related-el"></div>
    <div class="no-id"></div>
    

    ```js
    console.log(el.attr); // "related-el"

    el.attr = $(".no-id");
    console.log(el.attr); // logs reference to the ".no-id" element
    console.log(el.getAttribute("attr")); // empty string

    el.attr = "garbage";
    console.log(el.attr); // "garbage"

    el.attr = $("#related-el");
    console.log(el.attr); // equivalent to $("#related-el")
    console.log(el.getAttribute("attr")); // "related-el"
    ```

    1. (option 2a) Or, attr IDL attribute is always an Element, and we don't use this system for for/htmlFor.

      console.log(el.attr);         // logs reference to "#related-el" element
      
      el.attr = $(".no-id");
      console.log(el.attr);         // logs reference to the ".no-id" element
      console.log(el.getAttribute("attr"));  // empty string
      
      el.attr = "related-el";       // throws exception
      el.attr = "garbage";          // throws exception
      
  2. Have two non-interacting IDL attributes, one which reflects and one which doesn't:

    • attr, which reflects
    • attrElement, which doesn't reflect, and doesn't affect attr in any way
    <div id="el" attr="related-el"></div>
    <div id="related-el"></div>
    <div class="no-id"></div>   
    

    ```js
    console.log(el.attr); // "related-el"
    console.log(el.attrElement); // null

    el.attrElement = $(".no-id");
    console.log(el.attr); // "related-el"
    console.log(el.attrElement); // logs reference to the ".no-id" element

    el.attrElement = null;
    console.log(el.attr); // "related-el"
    console.log(el.attrElement); // null
    ```
    One of these needs to take precedence when computing the element which takes part in the relationship. EITHER:

    1. attr takes precedence, since it reflects

      console.log(el.attr);         // "related-el"
      console.log(el.attrElement);  // null
      // computed related element is "#related-el"
      
      el.attrElement = $(".no-id");
      console.log(el.attr);         // "related-el"
      console.log(el.attrElement);  // logs reference to the ".no-id" element
      // computed related element is "#related-el"
      
      el.attr = null;
      console.log(el.attr);         // null
      console.log(el.attrElement);  // logs reference to the ".no-id" element
      // computed related element is ".no-id"
      

      OR

    2. attrElement takes precedence, since it's more powerful.

      console.log(el.attr);         // "related-el"
      console.log(el.attrElement);  // null
      // computed related element is "#related-el" because attrElement has never been set
      
      el.attrElement = $(".no-id");
      console.log(el.attr);         // "related-el"
      console.log(el.attrElement);  // logs reference to the ".no-id" element
      // computed related element is ".no-id"
      
      el.attrElement = null;
      console.log(el.attr);         // "related-el"
      console.log(el.attrElement);  // null
      // computed related element is null because attrElement was explicitly set to null
      

      As I see it, no matter which way we slice this, there's going to be something weird and/or annoying for developers.

| Option | Good | Weird/annoying |
|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | attrElement always sets/gets the related element, whether it's computed from the ID or set as an Element ref. Determining the computed value is trivial. | attr may get out of date under some circumstances which are unlikely to be common in practice[1] |
| | Similarly, whatever you set, however you set it, will automatically update all the other things. There is no question of precedence under most circumstances[2]. | A bit of a mouthful to explain how these things relate to one another |
| 2 | Only one IDL attribute, no weird intertwingling or precedence questions. Easier to explain. | attr may still get out of date. |
| | | If no DOMString option, or if getting attr always returns Element, won't be backwards-compatible with for/htmlFor |
| | | If a DOMString option for getting attr IDL attribute, will always have to check type of attr IDL attribute when getting. |
| 3 | No need to explain how things relate. You just have two things which affect the same thing. Simple. | If attr takes precedence, need to delete attr to get attrElement to work. Getting attrElement won't guarantee you the actual truth. |
| | Nothing can get out of date, ever, because they are disconnected[3]. | If attrElement takes precedence, attr may be "wrong", like value with a dirty flag. |
||| Always have to check two things to figure out the attr-associated element: attrElement and attr. |

[1] an element's ID value changes, or it is added to or removed from the tree

[2] i.e. unless one of the above scenarios has occurred

[3] however, attr may still be misleading if it doesn't take precedence

Thanks so much for that comprehensive summary. Reading it I lean toward 3(ii). The downside is the smallest, in my mind. Possibly because I'm used to content attributes being a bit misleading.

I'll also note that for 1,

Determining the computed value is trivial.

is a bit misleading, because it depends on what you mean by "computed". For example with .htmlForElement, it still won't return the labeled control that comes from being a descendant. (For that you have to use .control.) Similarly I recall talks around AOM about how the "computed accessibility tree" is fairly complex, so it seems likely that although .ariaX may represent the "computed ARIA _X_, as determined by those two properties", it doesn't necessarily represent "the computed accessibility _X_".

That said, the only one I really don't like is 2 non-a. I think branching on the types is fatal. (2a) is suboptimal because I would really like to have a system that works across all existing IDREFs (listed in the OP of this long thread). But if people like it a lot more than (3) or (1), I could live with (2a).

That all seems reasonable to me. What I'm curious about is the interaction with shadow trees. In particular inadvertently exposing shadow trees through this attribute. Ideally in such cases setting is a no-op. (The only way to allow that and preserve encapsulation would be through ElementInternals, but that's not really workable given the elements this is intended for.)

@annevk In my second PR, I used this wording:

If newElement is not in the same tree as hostElement, nor the same tree as hostElement's shadow-including root, then remove the content attribute and return.

This would limit it to the element's shadow tree and light tree. Would that work for you?

To rephrase, if you do

  • newElement's root is not hostElement's root, and
  • newElement's root is not hostElement's shadow-including root

it works, but only for one nesting level. I guess the second needs to be changed to

  • newElement's root is not a shadow-including inclusive ancestor of hostElement

That works for me, thanks.

Regarding "determining the computed value", I should have explained it in terms of the converse:

Say we use option 3, and you want to figure out what the "ariaActiveDescendant-associated element" is for a particular element. You'll have to write something like this:

var activeDescendant = el.ariaActiveDescendantElement;
if (!activeDescendant)
  activeDescendant = el.ownerDocument.getElementById(el.ariaActiveDescendant);

There's no way to simply query what the element is.

Going to add this to the matrix for the benefit of future archaeologists.

Yeah, that's true. It's just unclear whether that's valuable, given that the actual "accessibility active descendant" might be something else entirely. (Disclaimer: I am not familiar with how active descendants work.)

I think it's reasonable to expect to want to check what it was set to, even if maybe 0.5% of the time it may not be honoured for some reason or another.

The most interesting proposal to me as a developer is one where I can _always_ get an Element reference, as the possibility of falling back to the IDREF and searching the Document (or ShadowRoot) container feels cumbersome.

In my mind, that makes Option 2 the winner.

Option 1 feels similar, but doubles the number of properties, and I'll probably never use the property that returns an IDREF string.

Option 3 means that I may have to fall back to the IDREF if the attrElement property isn't set, and then I'm not sure if I should set the attrElement property with the Element reference I find.

I also have no strong opinion about whether the attribute value is reflected, as I will always use the element reference if it is available.

As for backwards compatibility with <label>.htmlFor, perhaps that property could be left as a legacy wart and have <label>.control could be made writable.

Yeah, I'm feeling like some version of Option 2 should be good, with a special case for for (which is unfortunate since it's the only non-ARIA relationship in HTML right now but oh well, hopefully there will be more).

I'm thinking something which has a polymorphic setter, but whose getter always gets you an Element reference.

Then the question would be whether to use a "dirty flag" style system, or to use the system of attempted reflection I outlined.

My preference would be "attempted reflection". I think the dirty value flag thing makes more sense for value, which will change frequently and without anything else changing in the DOM, but I believe these relationships will change seldom enough (relatively speaking, compared to e.g. typing in a text field), and likely with some other significant DOM mutation, that the DOM mutation impact will be minimal. I think that empty string value for the content attribute (where the ID can't be used) should give enough indication that a relationship exists and that the element IDL attribute should be consulted, and per the system I outlined removing the empty attribute will also remove the element relationship altogether.

For is not the only such relationship in HTML. We are using it as an example. But the OP lists 4 (or 10, depending on how you count). 4 legacy warts is much less appealing than 1.

Yeah, we don't need ID-fallback for new features, but for the existing ones 3 seems preferable.

I'm also a little bit confused what type of developer @azakus is representing here. It seems if you're a web developer you can just ignore the ID-fallback stuff and only use the element-reflecting attributes.

It seems if you're a web developer you can just ignore the ID-fallback stuff and only use the element-reflecting attributes.

I'm confused by this statement. It seems to me that Option 3 would not set any element references, only provide a mechanism for a developer to set an element to use instead of an IDREF.
In that case, getting an element reference would always require a stanza such as the one in https://github.com/whatwg/html/issues/3515#issuecomment-413943197 somewhere in user code.

Is it actually the case that attrElement references would be provided by setting attr?

You'd only have to write that code if you use a mixture of new-style and old-style, right?

However, it seems reasonable to me that we'd consider a fourth alternative. For legacy features:

  • _attr_ sets and gets this element's "reference ID"
  • _attrElement_'s setter sets this element's "reference Element"
  • _attrElement_'s getter returns this element's "current reference Element"
  • This element's "current reference Element" is this element's "reference Element", unless it's not set, in which case it returns the element referenced by "reference ID", unless it's not set or cannot be found, in which case it returns null.

For new features that have such a relationship and can be done purely imperatively we'd omit the _attr_ feature.

You'd only have to write that code if you use a mixture of new-style and old-style, right?

I think in practice this will be ~100% of code, since things will migrate gradually, or want to do some server-side rendering of initial state via the IDREF attribute.

This element's "current reference Element" is this element's "reference Element", unless it's not set, in which case it returns the element referenced by "reference ID", unless it's not set or cannot be found, in which case it returns null.

Does this imply a "dirty value flag" style system? i.e. once you set attrElement, attr is ignored from that point forward?

For new features that have such a relationship and can be done purely imperatively we'd omit the attr feature.

i.e. both content and IDL attribute?

@alice I don't really remember how dirty values work in the style system (I thought those were mostly optimization annotations). I think if we allow setting _attrElement_ to null, and we probably should allow unsetting it somehow, then _attr_ would end up taking effect again per the algorithm I laid out.

And yeah, new features that are done purely imperatively wouldn't need a "reference ID" attribute, be it content or IDL.

Apologies - I meant a system like the "dirty value flag" system for <input> - nothing to do with style, though my wording was confusing.

I see, I guess it's not quite like that then, unless we don't allow severing the relationship.

At what point would you revert to the IDREF? When you set attrElement to null?

Right, when there's no "reference Element", "current reference Element" would look at the "reference ID". And "current reference Element" can be lazily computed by the user agent whenever there's a need.

Ah yes, I see.

One slight hassle with that is that if you are trying to remove the relationship by setting attrElement to null, it might be surprising that this IDREF attribute you might have forgotten to clear out suddenly resurfaces.

Yeah, I'd be okay with making the _attrElement_ setter handle that case and remove _attr_ in that case too.

I didn鈥檛 quite follow that last comment, sorry - could you give an example?

Let me rephrase: if you set _attrElement_ to null, it removes the _attr_ content attribute.

I see. So it sounds like a kind of semi-reflection from attrElement to attr in that case.

I'm just trying to figure out a solution that meets all the various requirements and shouldn't be too hard to implement or use. It might be a little tricky to fit it in the general reflection framework, but it seems doable.

Right, makes sense to work through all the options.

When you say "seems doable", what are you referring to exactly?

Never mind, it doesn't make sense to define _attrElement_ in terms of reflect, as there's no content attribute, so that doesn't apply.

Let me rephrase: if you set attrElement to null, it removes the attr content attribute.

@annevk would that trigger a DOM mutation event?

Mutation events aren't spec'ed, but yes, changing a DOM attribute should fire a mutation event and create a MutationRecord too (for MutationObserver).

@alice I'm having a hard time understanding what attrElement refers to. Could you add an example to the summary?

@jessebeach Updated the long "summary", please take a look!

https://github.com/whatwg/html/issues/3515#issuecomment-413716944

Reading through the summary (thank you Alice!) I think I'm leaning toward option 1.

I can use attr for any SSR work. But once my client side js has booted I can use attrElement both for querying (since it is a source of truth) and for setting (since it takes Element refs).

Like @azakus, I don't trust attributes on the client and I'd always refer to the property. I only think attributes are useful for initial configuration, which option 1 seems to solve.

The legacy warts are a huge bummer... I guess I lean toward not compromising the design of the new thing for the sake of those warts.

Option 1 would work with the legacy attributes as well, e.g.

console.log(el.htmlFor);         // logs the string
console.log(el.htmlForElement);  // logs the element

Agree with @alice and @robdodson (and others in the backscroll who condone Option 1). We know not to trust HTML attributes once they're been parsed to DOM.

One snafu that just came to mind though. Arg, I'm bummed to even bring this up.

Not all desktop assistive tech software interacts with the platform APIs. Some, like JAWS + IE, still scrape the HTML for information. At least, I think they scrape the HTML and not the DOM. @mcking65 might now the specifics of this behavior.

I'm confused as to whether you mean option 1 or option 2a.

  • Option 1 has both string and element reflection.
  • Both write back to the content attribute where possible.

Could you possibly clarify the comments around trusting content attributes, since it seems to affect any of the options I outlined?

I spoke with Alice about option 2a and I think it's what I want.

I can set attr to a string for initial SSR, and after my JS boots, I can work with it like a regular javascript property. So long as el.attr is always an element reference or null on the client then I think I'd be happy.

Should we use option 1 for existing attributes, and 2a for new attributes, then?

That seems pretty harmonious to me.

In particular, if you ignore the presence of the .attr IDL attribute, the interaction between the attr="" content attribute and the .attrElement IDL attribute in (1) is the same as the interaction in (2a). Right?

Yup, exactly.

I guess a tiny question remains of whether the IDL attribute in 2a should be called .attr or .attrElement. I'm happy with either.

(Oh no, this thread got long enough that the most important post in it got hidden by default.)

Taking into account how we want to treat the existing 4 cases from the OP, I see a few options.

  • (A) attr for new things; attrElement for existing things (alongside their existing attrs)
  • (B) attrElement for new things and existing things (alongside existing attrs for existing things)
  • (C) attr for new things; try to be clever for existing things:

    • for="" (both label and output): Introduce a new for IDL attribute with the (1) semantics. Then we have for (new style) and htmlFor (old style)

    • form="": introduce a new setter, whose algorithm is the same as (1)'s setter. The getter behaves the same as it currently does, which is a lot more complicated than (1).

    • list="": introduce a new setter, whose algorithm is the same as (1)'s setter. The getter behaves the same as it currently does, which is a tiny bit more complicated than (1)---it returns null if the target element is not a <datalist>. (Maybe we even want this return-null-when-target-is-invalid semantic more generally, so this would no longer be a special case?)

(C) has the advantage of surface simplicity and teachability: every one of these element-association content attributes has a corresponding IDL attribute which is named the same as the content attribute and works on elements, not strings. This is a nice story, with only a bit of lurking complexity. You have to ignore the existence of htmlFor, and ignore some of the edge-case complexities of the .form and .list getters, but that's not too bad.

Note that if we did (C) I think we'd only create a general framework for new things, and we'd just add custom in-place spec text for the getters/setters of the existing things.

So output is a slightly different case, because it's a list, not a single IDREF. My suggestion for lists is to set an empty string for the content attribute unless (at setting time) every single element has a usable ID (i.e. exists and is correctly scoped). Sound ok?

Also, I thought .for was ruled out because for is a JS keyword?

Other than those things, all of those options sound fine to me, so happy to go with your preference (I guess (C)?)

My suggestion for lists is to set an empty string for the content attribute unless (at setting time) every single element has a usable ID (i.e. exists and is correctly scoped). Sound ok?

Sounds good!

Also, I thought .for was ruled out because for is a JS keyword?

Nope. That was a restriction back in the 90s when the DOM API was first created, but hasn't been a problem for many years now.

Other than those things, all of those options sound fine to me, so happy to go with your preference (I guess (C)?)

Yeah, I like (C), pending others' feedback. I can try to help with the one-offs it entails. It seems like the core is speccing the (2a) framework for new stuff. We'll lose our guinea pig of for="" in the HTML spec, and instead need to use new stuff (ARIA stuff, I assume) to prove out the general framework. But that seems fine.

To @jessebeach's earlier point - I think we just have to live with working around legacy tools' behaviour. Steve Faulkner had some thoughts on this topic.

I'd be interested to hear from @mcking65 which, if any, tools scrape content (surely they break badly with Shadow DOM as well, in that case?)

ChromeVox classic/legacy was one example of a DOM-based tool, but it's now deprecated and the new version of ChromeVox on Chrome OS operates via the accessibility tree.

I seem to recall Dragon Naturally Speaking was slow to adopt ARIA, indicating they are also scraping the DOM - but they did eventually add support.

@alice as far as I'm aware Dragon still doesn't use the accessibility tree - it uses a browser extension to scrape the DOM.

@jnurthen Right, that was my hypothesis. I'm of the opinion, based on Steve's reasoning, that we shouldn't block features based on that fact. What do you think?

I updated the PR to just (for now) deal with reflection of Elements and sequences of Elements, in general.
It's still a bit of a work in progress - I think I still need some way of referring to the Element/Elements set via the setting steps; in particular, to disallow setting the value if it would break encapsulation.

@domenic, @annevk and/or @rniwa, would you care to take a look at what is there so far? https://github.com/whatwg/html/pull/3917/files

Had an offline discussion with @alice spurred by the latest review in https://github.com/whatwg/html/pull/3917#pullrequestreview-189973949.

To recap, in #3917 she's speccing 2(a) from her post at https://github.com/whatwg/html/issues/3515#issuecomment-413716944. This involves only a single reflecting Element-valued property, call it el.attrElement; we're setting aside any interaction with a DOMString-valued property for now.

We discovered something a bit unexpected about that, which I want to document the reasoning for here, either for discussion or just for the record.

The question is, what happens in these scenarios?

// (1)
el.setAttribute("attr", "some-id");
el.attrElement = $(".no-id");
// (2)
el.attrElement = $(".no-id");
el.setAttribute("attr", "some-id");

The claim is that we expect "last one wins" behavior:



    • The attr="" content attribute gets reset to the empty string (or deleted?)

    • The el.attrElement getter returns what it was set to, i.e. the $(".no-id") element

      2.

    • The attr="" content attribute gets set to "some-id"

    • The el.attrElement getter returns the element whose ID is "some-id"

The alternatives to last-one-wins are either:

  • Content attribute always wins; attrElement setter sets the ID if possible, or deletes otherwise

    • This could break shadow tree situations, and leads to situations where if the ID of the element gets mutated, attrElement will stop returning what it was set to.

  • Content attribute always wins; attrElement setter does not manipulate the content attribute

    • This makes the attrElement setter useless until the author remembers to manually clear the content attribute

  • Internal slot always wins

    • This makes the content attribute useless unless you remember to clear the internal slot by setting attrElement = null first.

However, because we don't have a clear precedence order, last-one-wins means we need setting the attribute to affect the internal slot. We can't implement it with a simple getter/setter algorithms for el.attrElement that stand independently; we need to also have side effects when changing the attributes through APIs like setAttribute().

So, we need to say that any element with this reflection setup has attribute change steps that resets the internal slot value. This is the formalization of @alice's current text in #3917, which says "If the content attribute is set directly (such as via Element.setAttribute()), the value for the IDL attribute is reset to null."

This is a bit weird! But I hope with the above I've recorded why it's the best way to implement 2(a) semantics.

Was this page helpful?
0 / 5 - 0 ratings