WebKit / standards-positions

WebKit's positions on emerging web specifications
https://webkit.org/standards-positions/
242 stars 18 forks source link

Customized built-in elements #97

Closed annevk closed 11 months ago

annevk commented 1 year ago

Request for position on an emerging web specification

Information about the spec

Bugs tracking this feature

Anything else we need to know

WebKit supports autonomous custom elements, but hasn't implemented this aspect of custom elements.

Colleagues and I wanted to share our thinking around that in this issue and also open it up for feedback from the community.

annevk commented 1 year ago

Custom elements give web developers the power to create their own HTML elements. The idea behind customized built-in elements is to give web developers a similar power, but starting from an existing HTML element. Essentially subclassing it. The main use cases driving customized built-in elements are:

Customized built-in elements attempts to achieve this through an is attribute, e.g., <input is=my-input>. (The reason it’s not <my-input extends=input> is because it was deemed too hard to change implementations to key internal logic on the element type rather than the local name and namespace pair.)

Colleagues and I are interested in addressing these use cases, but thus far we haven’t been convinced customized built-in elements is the way to do it:

The main compelling use case to us at this point is the HTML parser use case and we’d love to jointly dive into that deeper to see if we can come up with a better solution that comes with fewer drawbacks. https://github.com/whatwg/html/issues/8114 is probably a good place for that particular discussion.

WebReflection commented 1 year ago

I think there are tons of arguments for desiring cross-browser builtin extends out of the box within the linked bug, so I won't repeat myself or other developers findings and otherwise-impossible improvements builtin extends bring compared to Shadow DOM but this point in particular makes me very curious:

Customized built-in elements also cannot realistically have good accessibility support built-in

in my experience it has the best accessibility support because developers don't need to do anything to add standard builtin accessibility to elements that already support that, so I'd like to hear, or better understand, in which case builtin extends makes accessibility worse.

One missing point though is the ability to extend elements in the HEAD of a site, being that for runtime import maps, meta, even title, or literally any other element that doesn't need Shadow DOM but it's crucial to describe a document and its expected behavior.

romainmenke commented 1 year ago

One aspect not yet listed is that customized built-in elements do not load/initialize gracefully.

The only way we see them working well is when JavaScript is a blocking resource and when you are a 100% sure it will always load successfully.

For async/defered JavaScript the elements will always exist partly as un-upgraded elements. The elements will first behave as native elements before being customized. This will lead to unexpected document states and hard to solve bugs.

This is the main reason we never used this API for client work. We could have picked a polyfill for Safari but we never saw this feature as viable.


This is different from regular custom elements. Those are not expected to have any behaviour when the JavaScript is slow or fails to load.

WebReflection commented 1 year ago

@romainmenke as one that used builtin extend forever (and wrote all polyfills to date), I am curious around these takes too:

The only way we see them working well is when JavaScript is a blocking resource and when you are a 100% sure it will always load successfully.

The whole point of builtin extends is that these don't need JS by design and the element will do what that element does by standard specifications. This current behavior is pretty off from your conclusion that builtin extends must be successfully loaded and/or as blocking resource. I completely miss your use case that fails here, being that an image, a button, an input, a table, or any other element that would work out of the box as-is, which is the only reason to use builtin extends.

For async/defered JavaScript the elements will always exist partly as un-upgraded elements.

This is true for "dumb elements" too (Custom Elements without builtin extend and without JS driving them) except these literally do nothing and represent nothing until the class that takes them over kicks in. How are these superior for tables, inputs, head elements, even divs, quotes, pre elements, and so on?

The elements will first behave as native elements before being customized.

This is exactly the feature builtin extends bring to the table, so it looks like you are confused around what builtin extends are or what's their application? You can have a body tag, even an html one, that's builtin extend: how anything you say breaks there?

This will lead to race conditions and hard to solve bugs.

This is in contradiction with the statement you made after:

This is the main reason we never used this API.

If you never used this API, how can you write opinions around such API? It'd be great to have more contributors from people that actually understood and used builtin extends in production, first Polymer users or A-Frame users or somebody from Google as they also used these in various products and these days are falling back to ugly home-made solutions like it is for the YouTube case.

Screenshot from 2022-11-28 17-15-45

romainmenke commented 1 year ago

If you never used this API, how can you write opinions around such API?

Edited my comment. Obviously this isn't an unmotivated comment. Haven't used it in production vs. haven't used it at all.


My point was that either you want a standard element or you want a custom one. You never deliberately want a standard element for the first 100ms of a page view and a custom one for the remainder. There is no way this can ever be viewed as a desirable trait.

The first 100ms can easily become the first 5s for users on slower devices with a slow connection.

I have no intention of derailing this thread with a lengthy discussion about this detail. If you do want to discuss this further then I am happy to do so elsewhere.

WebReflection commented 1 year ago

@romainmenke to simplify my answer: please do check any website that use builtin extends with JS disabled (to mimic no JS load at all) now check with the same constraint any site based on custom elements without builtins and see the difference.

Also: race conditions don't exist in JS by definition because JS is single tread since day 0 but if there's anything that cause problems with builtin extends it will cause problems with custom elements too, except, like I've said, these represent nothing until the definition kicks in ... surely can't represent body, html, style, pre, code, button, link, tables, link, script, title, and the list goes on and on (pretty much any void element in the specs + every other).

WebReflection commented 1 year ago

@romainmenke

You never deliberately want a standard element for the first 100ms of a page view and a custom one for the remainder.

I definitively do and these are the benefits:

The first 100ms can easily become the first 5s for users on slower devices with a slow connection.

Exactly why I prefer builtin extends over non-builtin extends if my purpose is to show a video tag, an img, a table, a body, etc etc

WebReflection commented 1 year ago

I have no intention of derailing this thread with a lengthy discussion about this detail

Me neither, but this discussion is claiming non-real-world facts so it's good to counter-argument these as long as anyone is willing to answer my questions ... to date, I keep asking questions but I rarely receive answers.

Reasons builtin extends, when builtin extends make sense, are superior to me:

I also don't want to derail this issue though, so I might just step-aside until I have at least some answer around the questions I've made, thank you!

romainmenke commented 1 year ago

race conditions don't exist in JS by definition because JS is single tread since day 0

yup yup, edited my comment so as not to trigger unwanted associations. I did not mean a multi-threading race condition where multiple threads are accessing the same memory.

A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.

So by a broader definition race conditions most definitely exist within websites.


I have no intention to debate this further here. Especially not at this intensity.

I know that a lot of people want this feature and that people do not like it that Safari does not support it.

I came here to share one more aspect which makes this feature not ideal. Those concerns are not the result of misunderstanding the feature. It should be ok for me to express these things.

Maybe we are unique in finding that this is an issue and in that case I am sure that my concern won't have any impact on the outcome.

WebReflection commented 1 year ago

@romainmenke

It should be ok for me to express these things

absolutely, but it should be OK for me to provide real-world solutions that don't suffer anything you mentioned so far.

I have tons of libraries based on builtin extends that are just working and none of these suffer any point you mentioned, but I don't want to flood this issue with libraries, although I am curious to read your code around your claims. My twitter DMs are open, if interested in sharing more.

P.S. race conditions as you mentioned there, are solved by customElements.whenDefined which method exists for that exactly purpose: avoid kicking in stuff before it's needed.

MikeVaz commented 1 year ago

I remember reading the thread and I couldn't find any reasonable argument in favor of Not implementing it in WebKit. In reality it caused a lot of pain for teams who try to use the platform features over a library and add some negative points to the WebKit and Apple brands.

In my opinion it is a simple inheritance. Blocking inheritance only in some cases seems odd. I can extend HTMLElement but why I wouldn't be able extending HTMLSpanElement. And the use case like to get a display inline could be just one of examples re-using some of the properties of the parent class.

Now, we can argue what is the best way to define it declaratively <span is="my-span"></span> or something else. But we already have a standard defining that so why not just follow it.

I read your bullet points like 5 times and can't really agree with any of those. They seems questionable. Which makes me think there is a bigger hidden reason why team not implementing it. I just don't get why.... 🤷‍♂️

MikeVaz commented 1 year ago

Could you elaborate on The syntax used prioritizes implementers over web developers which is wrong per the Priority of Constituencies. ?

WebReflection commented 1 year ago

If I can pile up a question:

Customized built-in elements cannot have access to shadow trees

I still need to see out there a built in extend that needs or use Shadow DOM at all but this point reminds me Shadow DOM can be already added to any element on the page so it’s hard to understand what’s the point in favor of not having builtin extends there.

Thanks in advance for any clarification

bennypowers commented 1 year ago

@WebReflection certain built in extends can have shadow roots, but not controls

https://dom.spec.whatwg.org/#dom-element-attachshadow

I'm curious to know why that is, as well.

WebReflection commented 1 year ago

@bennypowers thank you! I still think builtin extends don’t need or ask for shadow DOM at all, actually the opposite, at least in my experience, because their purpose is to enhance, not to change their nature, so I accept the limitation without being a blocker but it seems more like an attachShadow issue, not a builtin one.

edit on the other hand, this SD limitation seems a very valid reason to have builtin extends, as there’s no other way in the platform if not by requiring userland boilerplate to extend those elements when SD is not needed.

MikeVaz commented 1 year ago

One of the unusual use cases in favor of customizing built-ins is progressive enhancement. It took some time for Safari and other browsers to start support <dialog> element. But we could have progressively enhanced it. <dialog is="my-progressive-dialog"> to add a conditional code extending HTMLDialogElement if available or using a custom one. Limitation around customizing built-ins blocked us from doing that. So unblocking such scenarios would be helpful.

steveblue commented 1 year ago

I haven't experienced any of these shortcomings mentioned above when implementing customized built-ins. I disagree with the assertion the primary use-case for customized built-ins are form controls and their inclusion in WebKit would thus be superfluous given form-associated elements already cover this use case. There are plenty of instances where customized built-ins work brilliantly, including extending table and list elements or HTMLButtonElement, where the user would want to retain existing behaviors (that are not exclusive to accessibility), but extend their functionality.

While I agree the shortcomings of customized built-ins should be addressed by at the very least a warning that Shadow DOM isn't possible for most instances, I don't think this aspect to them should block their inclusion in a browser. Every custom element doesn't have to utilize Shadow DOM.

Chrome, Firefox, and Edge all support customized built-ins. Even if a successor were chosen, it would be wildly inappropriate for those browsers to deprecate customized built-ins, so if WebKit doesn't support the specification web engineers are left with fragmentation and have to load a polyfill, which undermines the use value of custom elements (despite the best efforts of @WebReflection, not knocking your work, it's quite good). For those who have adopted customized built-ins, WebKit is asking a lot for those users to adopt another spec and abandon customized built-ins, when a majority of browsers already implemented the specification. This violates one aspect of custom elements I find the most alluring: longevity. A custom element that works today should work in browsers for years to come. is should just work.

bahrus commented 1 year ago

Most (all?) the use cases for customized built-ins would, I think, be better served by standardized element decorators / behaviors / custom attributes / directives - what virtually every framework other than react have found it beneficial to support. This would allow multiple, loosely coupled enhancements by different vendors to be applied to the same element, in a way that could avoid conflicts with future enhancements to the built-in elements. I find it puzzling that webkit hasn't proposed this as their preferred alternative. I also think such decorators could serve a dual purpose during template instantiation.

But the ability for limited implementation single inheritance / built in method overriding also seems useful, especially for the template element, or the form element (e.g. defining custom checkValidity() overrides). I'm not even sure customized built-ins support that, but if they do, that does seem useful to me. Webkit's stated objections to heavy use of this antipattern (?) are valid, it seems to me, so in my ideal world, support for both would be provided, with a heavy nod towards the former (element behaviors) as far as the preferred approach.

thepassle commented 1 year ago

I'd love to see customized built-ins happen, but I did always understood Safari's reservations about adding them, because it could potentially become harder to add new features to native HTMLElements in a non-breaking way. Was there ever a solution found to address these concerns?

WebReflection commented 1 year ago

@thepassle imagine these days “standard” solutions propose template literal tags based libraries where @click or ?attr are injected as template element content and gosh knows what any other template literal tag based library can propose as alternative, as opposite of separating view and behavior/listeners in core DOM, like builtin extends or CE can do already …

thepassle commented 1 year ago

Sorry, I fail to see how that answers my question

WebReflection commented 1 year ago

@thepassle my bad … all I wanted to say is that if there’s any concern around future extensibility of any element, template literal tags based solutions that exist today are a great example of how that concern is not solved at all by not providing builtin extends, as any library in that field use their own ergonomics to provide this or that feature, including ref attribute, and so on.

Westbrook commented 1 year ago

I’m certainly one of the many that have not used customised built-ins in any form of scale due to the lack of WebKit support, so there’s not much that I can bring in favor of shipping the spec’d behavior here, other than that it is spec’d. In that way, shipping it or actively working to get it out of the spec seems the correct course for WebKit here. Neither of which has been done.

For my money, however, it would be great to see actual in production (or at least a fully thought out form) usage of this specification to date. There are many argument as to what you might do with this spec, but even as a rather seasoned web component author and consumer I’m not sure I’ve ever needed to do something with this part of the spec. At least not one that didn’t involve disproving someone’s implication that “web components” weren’t “shipped” wifey enough to leverage.

If anyone on this thread could point me to a lib or repo with which I could expand on my understanding in this area, I’d be much obliged!

WebReflection commented 1 year ago

@Westbrook it’s a bit controversial as for 9 years WebKit has been loud about not shipping builtins extend, so that developers either never cared much (React about CE in general) or had to migrate to avoid needing a polyfill forever (Polymer, AFrame, Google AMP). So basically you’re asking who’s currently betting on a doomed standard that requires mandatory polyfill in case WebKit browsers are part of the equation. I have provided tons of libraries that either workaround this situation or solve it with importMaps but the latter is too late to the game to show concrete examples, yet I’ve already talked about YouTube going out with their home made builtin extend for a style tag, and if YouTube is not a good example already about what a site might need to do with builtin extends, I don’t know what else would convince you developers are finding, or will find, other ways to extend obtrusively builtin elements, as they’ve done to date already.

I agree this limbo is unacceptable and quite a parody of how meaningless standards could be … so kill it (hopefully providing alternatives before) or embrace it because it’s out there and nobody is innovating around this standard due its current state.

edit I did explore and innovated around builtin extend potential with many libraries but I don’t want to spam this thread with self-referenced links

bahrus commented 1 year ago

I'm feeling a bit like I undersold the benefits of customized built-in previously, so I'd like to try again.

At the risk of repeating well-discussed arguments that have already been tried over the past 9 years...

I think the strongest case for supporting this would be to allow for overriding of existing public methods, sticking to the same signature. Public methods seem to be quite rare today, possibly partly caused because lack of 100% adoption of this proposal being incorporated by all the browsers has thwarted this use case.

The strongest use case I can think of is what I mentioned earlier -- the form's checkValidity() method. Other use cases are when the developer needs something to happen before a method is called by the base component, or after. Examples would be calls to focus, click, play, pause, etc.

True, the external code calling the method could do this, but what if the caller of the method is going to be logically separate from the provider that needs to tweak what it does?

I can't think of many scenarios currently with the DOM the way it is, but going forward, I can foresee the benefits growing.

As we speak, there are groups that are developing experimental web components that are meant to become built-in. They may encounter scenarios where they want to say "this is a method I will provide a default implementation for, but it makes total sense for the consumer to override it and provide more features by consumers". Or "this is a method that a whole class of more powerful components could extend".

Why prevent this the moment it becomes built-in?

Ideally, JavaScript would provide an "overridable" keyword to make this explicit. Even more ideally, we would be able to prevent developers from attaching new methods on their extension, which would then break should the platform introduce that method with the same name in the future. But nothing this proposal / standard does prevents that from happening today, it only makes it easier (the fact that there's a robust polyfill is testimony to that). So I think the standard does more good than harm, especially if accompanied by lots of skulls and crossbones in the documentation, and especially if instructions / hooks are provided for the developer to carry out the enhancements in a future proof way. The developer has no motive that I can see for designing a fragile component/extension, so I imagine they would comply.

clshortfuse commented 1 year ago

I started on working on a Web Components based rewrite of a UI framework. It's accessibility-focused which I unfortunately find very lacking in most UI frameworks today. So I was somewhat forced to do it, and decided to do with Web Components. I can share my experience since starting July.

I've mostly written all my components to be 1:1 with native components to work around lack of Safari support for customized built-in element. The idea is all custom element work exactly the same as the native ones, down to events (change, input), so just as you would use a library to tweak your HTMLInputElement, you can do the same with CustomInputElement.

There just happens to be A LOT of code that I wish wasn't necessary. But at the same time, I've enhanced a few things beyond native. Also, because the native components are wrapped, it allows a bit more rebalancing of the differences between browsers. For example, input[type=range] is not draggable with touch on Safari. Firefox doesn't report :active for some controls and also uses focus rings. One example where wrapping is needed is, listbox items should be focusable when disabled So <x-listboxbutton disabled> doesn't actually set [disabled] on the native button, but instead would just set aria-disabled=true. But that could be opt in, rather the default for all controls. The amount of code necessary to replicate FormAssociated properly, with form reset, page back/forward navigation, and validity while staying accessible is more than I anticipated.

The fact we can't extend HTMLButtonElement directly means we have to manually workaround some weird quirks related to form submission. One aspect is [type=submit] (issue here). We have to clone the wrapped <input> back into light DOM so that it can be submitted. And still, it's a "fake" button in lightDOM, so let's just hope the code author doesn't look too closely at SubmitEvent.submitter, since it doesn't point to the real <x-button>. It also creates some weirdness with HTMLDialogElement because the intended syntax is supposed to have <form method=dialog>. There's a lot of weird wrangling related to if the custom dialog element (wrapped) should include a <form> in shadow DOM, or maybe the user wants to supply their own form and custom buttons, which are to be slotted in <x-dialog><form slot=form></form></x-dialog>. (edit: typo in syntax)

Then there's also the issue of implicit submission which is when you're focused on, say, <x-checkbox> and want to press Enter to submit like native. There's some extra code needed there which then comes back to replicated submit buttons if your form has <x-button type=submit>.

The last I can think of also lightDOM / shadowDOM issues with ARIA nodes. AOM Phase 2, which allows directly assigning element nodes (eg: ariaActiveDescendantElement = node) isn't available yet. The fact that we're wrapping everything means the real elements are in shadow DOM. The solution is to try address every possible venue where authors would need to use aria tags and silently handle them.

That said, I've worked around mostly all of it. But when I wrote my comment back in July, I didn't know the mountain of work I was exposing myself to. Am I happy with what I have accomplished so far? Yes. But I have a lot of years of experience on my belt, and I wouldn't ask anybody else to commit themselves this amount of work. Writing accessible Web Components is extremely taxing. Heck, roving tab indexes with ShadowDOM is actually broken on Chrome and Google has yet to fix it. I don't know how people are building their accessible Web Components without hitting this issue.

I'm about to start on what I think are the hardest of controls: comboboxes/HTMLSelectElement. I've intentionally left them last because of the light / shadow DOM complexity. I've implemented HTMLSelectElement for a ListBox setup, and found out it's impossible to do it 100% to spec because we can't do customSelectElement[0] to get the child CustomOptionElement at index 0. [Symbol.iterator]() generator is fine for for...of, but not index numbers. So, already, it's out of spec. Maybe there is a way to reconstruct HTMLOptionsCollection with Custom Elements and stay in spec, but it's definitely not clear for developers, and I wasn't going to stress myself any longer to make it work.

And while we can just tell ourselves to wait until FormAssociated and AOM continue to mature, the point is, it's still a barrier of entry for writing accessible Web Components in first place, when being able to extend the native ones would be a lot easier on devs.

WebReflection commented 1 year ago

@clshortfuse thanks for sharing ... and all I can think about your story, is that if anything changes or gets improved on the builtin side, you also don't get that out of the box, and you need to write more code, while if anything change on any other accessibility area, you need to refactor ... reading your comment feels like the platform is not serving you well, 'cause re-implementing everything and doing it right is a burden no developer should deal with to just ship components that work (and can be fixed in gotchas via tiny ad-hoc polyfills). It feels like WC are over time becoming less interesting, and requiring thousand lines of code to give users what an input with some event would've done already feels, imho, so off from what CE initially promised.

steveblue commented 1 year ago

@annevk is there any change in Apple's position regarding customized built-ins?

Please implement customized built-ins per the specification and bring browser cross-functionality to the entire set of specifications that comprise Web Components. WebKit is so close after supporting ElementInternals and Declarative Shadow DOM. I see no reason for omitting customized built-ins from WebKit. The omission causes fragmentation of the platform web developers rely on. If you need more clarity on the use value of customized built-ins I can provide a copy of my book which demonstrates at least 3-4 examples.

clshortfuse commented 1 year ago

Hi again, everyone. I'm here to update some findings since my last post and clarify a bit on my perspective based on writing an all-native, no transcompilation, Web Components based framework in a post IE landscape. For those interested, I think it can be looked at as case study to what an attempt at writing a UI framework that fully embraces all web standards entails, and all the obstacles included.


<label>

This isn't new since my write-up, but I had honestly left this out, but will be important later. As it stands, a form-associated (input) element cannot be wrapped with <label> as you would a real element. For example, I can write in lightDOM:

<label>
 <span>I accept</span>
 <input type=checkbox>
</label>

The screen readers parse it just fine. But a solely form-associated Custom Element will not properly be read by screen readers if you try to wrap it with label. I'm going to just copy from the Material Components actual example of intended usage:

<label>
  Material 3
  <md-checkbox checked>
    #shadow-root (open)
      <div class="container"><!-- snip --></div>
      <input type=checkbox>
  </md-checkbox>
</label>

If you've worked on accessible Web Components long enough, you'd know this isn't going to work. Because <md-checkbox> is just an HTMLElement and not an HTMLInputElement, screen readers will not the boundary between the light and shadow DOM and your checkbox will actually be labelless. The intention here is not to criticize, but they will eventually, as I did, have to realize that it will require a lot more complexity to get it work right and be accessible. This is what my components look like:

<x-checkbox>
  #shadow-root (open)
    <label class="shape">
      <input type=checkbox>
      <slot id=slot>
        -> #text
      </slot>
      <!-- snip -->
    </label>
  "Check?"
<x-checkbox>

Now, I'm okay with a <slot> inside my checkbox, since I would like to spare page authors (aka future me) the need to build extra DOM elements. There's no need to set aria-label unless you don't add text and I would like to hide the ARIA complexities as much as possible. But allowing support for a <label> wrapped Customized built-in element would make things much easier. Then Google's setup would work, and I wouldn't have to wrap every single native input element in a shadow dom with a <label> or ask the users to include an [label] attribute or <span slot=label>. Also, it has to be a set on the <x-checkbox> element because you can't just do <x-checkbox aria-labelledby="checkbox-label"> because you can't pass down that aria-labelledby id to the inner <input> element because that would break light/shadow DOM boundaries. But if we could support Customized built-in element, then this would work as well:

<x-checkbox name="checkbox"></x-checkbox> <!-- Only works if customized HTMLInputElement -->
<label for="checkbox">Check?</label>

It really shows that form-associated is neat, but honestly, not enough. That said, should it be the fault of the screen readers why the above doesn't work without customized built-ins? Is it a failure of the form-associated spec? Is it a browser issue when (not) informing the screen reader? I don't know, but I would think Customized built-in elements would more easily solve this issue.

Edit: Deeper analysis shows it's because it's a form-associated control using a native control internally. Screen-readers are reading the <input> which, in Google's implementation, cannot have HTMLInputElement.labels cross DOM boundaries. Therefore the label is blank. The "intended" form-associated solution is wire up everything that a checkbox does without using any native controls at all. I discuss the complexity of wiring everything later in this post and how that contradicts ARIA guidelines.


<fieldset>

Since my write-up, I hadn't really focused too much on formDisabledCallback. I was happy all my custom elements worked well in forms. I then realized I didn't pay attention to <fieldset>. This required a lot of retooling since you can't just observe [disabled]. It's best written as :host(:disabled), and now that Webkit has finished publishing support for ElementInternals and Form-Associated Custom Elements it seems that we can reasonably reduce a large chunk of code related to, instead of tracking disabled state and binding the internal state to shadow DOM elements, like so:

#shadow-root
  <label disabled={disabledState}>
    <input type=checkbox disabled={disabledState}>
    <div id="state-layer" focused={focusedState} disabled={disabledState}></div>
label[disabled] { /* rules */ }
input[disabled] { /* rules */ }
#state-layer[disabled] { /* rules */ }

We can use the host's state which is automatically set via formDisabledCallback (no JS required) :

:host(:disabled) label { /* rules */ }
:host(:disabled) input { /* rules */ }
:host(:disabled) #state-layer { /* rules */ }

Now, how does this relate to Customized Built-in? Well, now that all my components properly support fieldset and will hopefully have a reduced complexity, I would like to use that support elsewhere. Let's take the ever-vague "Card" paradigm. Cards can be actionable. Cards can have form controls (buttons, checkboxes). Cards can disabled. If a card is disabled, then a card's child controls should also be disabled. It would make sense for there for there to be a <fieldset> somewhere. It's the most reasonable construction instead of individually disabling inner control elements. We already have an element that can dictate the disabled state of children, so let's use it. It works well with form-associated, so again, let's use it.

So where can we put it? How do we want page authors to script <x-card>. How does <x-card disabled> work? Let's say we want to put a fieldset in the shadow dom, so authors can include their own controls. This following will not work:

<x-card disabled> <!--extends HTMLElement -->
  #shadow-root (open)
    <fieldset id=fieldset disabled={disabledState}><slot></slot></fieldset> <!-- fieldset will not break light/shadow boundary -->
  <x-button>Custom Author Button</x-button>
  <button>Native Author Button</button>
  <button disabled>Explicitly disabled button</button>
</x-card>

Both <button> and <x-button> will not be affected. Even a form associated with type of fieldset will not work. Is it due to browser bugs? Maybe. Is it a flaw in the spec, where as Form Associated components don't influence children? Maybe. But there is another solution: extend HTMLFieldSetElement. This should work fine.

<x-card disabled> <!--extends HTMLFieldSetElement -->
  #shadow-root (open)
    <slot></slot>
  <x-button>Custom Button</x-button>
  <button>Native Button</button>
  <button disabled>Explicitly disabled button</button>
</x-card>

On the note of Web Components, I do like them, so I would push back against a characterization that they aren't good. As much as I rail against the light/shadow DOM complexity, I do appreciate the abstraction. I like having CSS Modules where my styles are just in CSS files and my build scripts don't have to invasively parse JS files to minify styles. AdoptedStyleSheets is better for performance and just makes things a bit more inspectable when debugging (not in DOM). The fact my unique elements are identifiable by #id, classified by .class, and have attributable states with [attributes] makes a whole lot of sense, instead of the previous performance hack of slapping a class on everything.

Unfortunately, HTML still has to be inlined since we don't have a consensus on HTML Modules and it appears to align more to return a Javascript object (module) instead of a DocumentFragment. Doesn't play well with CSP, so I'm not sure where that's going. It's not a show-stopper; it just doesn't minify and involves having markup inside your script files (not compartmentalized).

In terms of JS, because we have to wire everything in light dom (HTMLElement) back to shadow dom (HTMLInputElement), that's a lot of code. That means some abstraction of that wiring is inevitable. In other words, I feel like any decent Web Component structure will have to eventually outsource to a state tracking/binding framework (eg: React/lit/vue), or build one. I've opted to build my own. observedAttributes isn't enough when you also want to support reflective properties. For my use cases, I guess it's fine, because I will probably reuse this beyond just HTML attributes and UI state, and use it for rendering data as well (MVVM/MVP). But proper, accessible Web Components aren't as easily setup without a state tracker.

One of the main rules of accessibility for web is not to reinvent the wheel and use native controls as much as possible. ARIA's first advice is "No ARIA is better than Bad ARIA". Custom Elements really seem to be structured in a way to allow the creation of extremely complex controls where no mixture of native (built-in) elements are used. But I rarely ever see this, personally. It's always "build a better checkbox" or template repeatable DOM trees. Most people just want to tweak, not build from scratch. That means having a starting point of a Customized Built-in Element reduces the need for complex abstractions of attribute/state tracking.


Will Customized built-in element solve all our problems? Probably not. But the reality is, it's extremely complicated to cover all use cases. Light and Shadow DOM boundary crossing is a real struggle. Form Associated is a tool and we can use it as much as we can. I'm trying here. But at the same time we need more tools to build better, accessible components and component libraries, and Customized built-in elements can be another useful piece in the toolkit


Credits to @rniwa at Apple for continuing work on these aspects. FormAssociated is really useful, and will be an extremely useful tool for code simplicity. Also, thanks to @josepharhar on the Chromium team for resolving the focus issue on Chrome.

I hope we can continue resolving these issues. But I also hope to put all this code in to play this year. Going back to the long, complex light-DOM, class-filled, SCSS-reliant structure of older components feel so clunky now by comparison.

WebReflection commented 1 year ago

@clshortfuse I'd be surprised if Liskov argument would ever overweight the amount of accessibility shenanigans ShadowDOM brings to the plate ... thanks for putting it down in such a nice way, with concrete examples that would never compete with "the good'ol HTML standard" 🙏

WebReflection commented 1 year ago

FWIWI ... all my arguments against "what ShadowDOM doesn't solve" have been around native accessibility and backward compatibility so that I couldn't stand more around these arguments! Not having builtin extends means more bloat, more moving parts, more APIs in place, not always 100% cross browser compat, and so on and so fort, to have a bloody label that is smarter, improves UX, or just work with whatever content it has ... and that's the gist of builtin-extend power that only one browser is struggling to understand. I hope this latest comment would seal the deal: builtin extends and forget about polyfills to have what every browser offers and what every standard to date has built into, ARIA included.

dgrammatiko commented 1 year ago

It's a pity that Interop 2023 didn't vote for the Custom Builtins https://github.com/web-platform-tests/interop/issues/234. Probably sets the expectations here as well...

WebReflection commented 1 year ago

@dgrammatiko in a world where developers wants just to move forward and WebKit has been loud about never supporting builtin extends, these pools are just biased as much as the ecosystem is around builtin extends these days, but Svelte didn’t circumvent this with shadow DOM, and neither did React, they all keep augmenting features around the “good old” standard, often causing more accessibility problems than they’re trying to solve, increasing the substitution principle risk WebKit has been loud about, in terms of backward web compat.

rniwa commented 1 year ago

@clshortfuse : Thanks for a nice detailed writeup. We do have a proposal / idea of cross-shadow ARIA API: https://github.com/leobalter/cross-root-aria-delegation/blob/main/explainer.md That should solve some of the issues you've outlined there. Making form-associated custom element work better with label and fieldset makes sense to me. Disabledness being applied to its descendant elements also makes sense. We should come up with some API to enable that.

clshortfuse commented 1 year ago

I took another look at the <label><x-checkbox> issue and it seems the problem is there's a disconnect between what parsed by forms (light DOM <x-checkbox>) and what is actually focused (not delegated) and inspected by screen readers (shadow DOM <input>). In tree format, with explicit roles for clarity:

<label>
  <span>My Label Text</span>
  <x-checkbox role="none"> <!-- form-associated element -->
    #shadow-root
    <input role="checkbox" type=checkbox> <!-- screen reader element -->
  </x-checkbox>
</label>

The .labels property on the <input> is empty because there is no <label> within its Shadow DOM boundary. I solved this because I put a <label> in the shadow DOM. But to use this paradigm, you will not have to track the labels on the form-associated element (extremely complicated without a callback), and wire it (apply it downwards) to the shadow DOM input element via parsing the textContent and injecting it into aria-label (because AOM isn't available yet).

The "correct" tree for form associated is this:

<label>
  <span>My Label Text</span>
  <x-checkbox role="checkbox"> <!-- form-associated element + screen-reader element -->
    #shadow-root
     <!-- handle everything -->
  </x-checkbox>
</label>

I checked other frameworks in case I might be doing something wrong. I know Google's setup doesn't work right. I checked Adobe (Spectrum), they're doing both <label> and <input> inside the shadow DOM. Ionic is doing the same, but they have a role="checkbox" inside a `role="checkbox"" which is really weird to me (and probably violates ARIA guidelines). Seems close which gives me an idea.

We don't want to have to write all the interactions (keyboard, mouse, touch). Also, native controls get to "cheat" bounding rects where custom elements cannot. (For example, Chrome will track your horizontal cursor position in type=range while dragging, even if you escape the element boundaries. Custom elements cannot track mousemove after mouseout.) But with ARIAMixins from ElementInternals, I believe I can track all signals and values from a Shadow DOM element and apply that to the host (and avoiding light DOM mutation):

<label>
  <span>My Label Text</span>
  <x-checkbox role="checkbox"> <!-- form-associated element + screen-reader element -->
    #shadow-root
      <input type=checkbox aria-hidden=true onchange={
        const { host } = this.getRootNode();
        host._elementInternals.ariaChecked = this.intermediate ? 'mixed' : String(this.checked);
      }> 
  </x-checkbox>
</label>

To be clear, this is more light/dom rewiring which I would have liked to avoid. I was also able to get everything to play nice with screen readers without rarely any ARIA tags or properties with the exception of things like aria-checked=mixed. I understand ARIA is there, but if there is a native element, it feels wasteful to have to manually rewire each and every ARIA property. Maybe something like a one-liner: host._elementInternals.ariaDelegateElement = this.shadowRoot.querySelector('input') which can tell element internals to delegate accessibility properties to a specific element. Not sure how feasible that would be.


@rniwa Thanks! It does seem that API proposal would help reduce code complexity. For now, I'll wire it up via ARIAMixins and see how far I get. I'll probably take a look at a workaround for <fieldset> after that.

dannymcgee commented 1 year ago

One use case I had recently for customized builtins that I haven't seen anyone mention yet is the <a> tag.

It's incredibly common for non-CE frameworks to use some sort of router-link component to render a new view client-side instead of needing to ping a server and load an entire document from scratch. This is ideally accomplished without sacrificing native behavior like updating the location and history, allowing users to preview the href in the status bar on hover, right-clicking the link to copy the URL or open it in a new tab, etc.

Most of that native behavior is impossible to replicate with JavaScript, which means the only recourse is to wrap the <a> tag with an additional CE DOM node.

I don't know how familiar y'all are with CSS (sorry for the snark 🙂), but that burdens developers with some seriously nontrivial layout challenges. Especially if you're building this type of component as a library author who expects consumers to be able to customize the look and feel of this "enhanced hyperlink". It's one thing for block-level elements where you can just set the child to absolute position and fill the parent's bounding box, but hyperlinks often (and by default) prefer to be inline, which makes things immensely trickier.

The only "good" solution I can think of off-hand is to force the CE wrapper to display: contents and... I don't know, pass every CSS property in the spec down to the child element with a CSS custom property? Yeah, nevermind, I can't even think of an air-quotes-good solution to that.

There's likely a reasonable solution to that particular problem that I'm just not seeing at the moment, but my broader point is: this isn't React, where a component is just an ethereal API wrapper around the "real" DOM node. Wrapping a native element with a custom element (or any element for that matter) is consequential in and of itself. It profoundly changes the behavior of CSS in ways that can convert a trivial layout task into an hours-long exercise in frustration.

I really don't think using wrapper elements purely for functionality enhancement is a good idea. I'm not saying the is="" feature is necessarily a better one, but it does address some tricky use cases that I don't see a lot of great alternatives for in the current spec.

FWIW, my go-to tool for avoiding the functionality-wrapper antipattern in frameworks that are not React is directives, in the vein of Angular or Vue. They're a really fantastic compositional tool, they encourage modularity and single-responsibility thinking, and they don't muck with the DOM tree in ways that give me migraines. 🙂

WebReflection commented 1 year ago

@dannymcgee a, img, tr, td, th, tbody, tfoot, option and select, li, dd, dt, button too ... only div and few others are good once wrapped, the semantics of section, nav, article, and so many other components with a well defined meaning and behavior have the exact same issue you are describing, and some of them cannot even be wrapped (td among others) ... the solution is is="" or jQuery (not even kidding) to augment elements like it's been done forever to date. is="" would at least enable automatically the augmentation, <a ref={augment} /> in React too, and goodbye shareable components among libraries. I've brought up this argument for years but it's been always ignored.

ghost commented 1 year ago

It seems like this is still one of the most wanted features for safari (from a developer perspective): https://front-end.social/@jensimmons/110100912182602683.

Would be great to see some progress here. I think @WebReflection has already brought up all the arguments. The main use case i heard from many people lately is to use this for progressive enhancement. So would be great to see the safari team at least resume the discussion on this topic. Thanks!

colepeters commented 1 year ago

I'd like to chime in with another use case. I've been trying to build a couple custom elements that would make authoring dynamically generated responsive images easy for users of Enhance. You can see the current state of things in this issue. We're trying to expose one parent CE on which a user declares a source image, which would compose with one or more child CEs on which variations of that source image (size, quality, cropping, etc) would be declared (and then passed to an API for the generation of those images as specified). For example:

<my-picture src='a-large-source-image.jpg' alt='A large source image'>
  <my-source media='(min-width: 90em)' width='600'></my-source>
  <my-source media='(min-width: 48em)' width='400'></my-source>
  <my-source width='200'></my-source>
</my-picture>

To summarize the specific problem we're facing: the picture element requires its source elements to be direct descendants, which means that the following markup (resulting from the above example) is invalid:

<my-picture>
  <picture>
    <my-source>
      <source … />
    </my-source>
    <my-source>
      <source … />
    </my-source>
    <img … />
  </picture>
</my-picture>

This is a prime example for allowing customized built in elements, where a strict element hierarchy is required. In this example, being able to extend the source element with a custom element would solve this issue. Without customized built in elements, custom element solutions for markup patterns like this become either ergonomically confusing for component consumers, or simply impossible.

I do understand some of the concerns raised by the WebKit team, but I also feel like we're losing out on a lot by not having customized built ins available on all browsers.

(Also, note that this issue definitely occurs for other hierarchically dependent elements like ul/ol > li, dl > dt & dd, and anything involving tables, although admittedly there's more forgiveness here in terms of what the client will render as child content. With picture > source, it's much more strict.)

I also think it's worth echoing something said in the opening comments on this issue:

Using child elements is the established way in HTML to provide fallback contents. E.g., this is how canvas and picture approach it.

This is super on point, but what about cases where a lack of customized built ins prevent us from authoring child elements properly to begin with? 😅

WebReflection commented 1 year ago

I've recently stumbled upon a not-so-discussed issue that's kinda forcing pyscript to abuse the type of a <script> tag to solve issues intrinsic with the project goal: we need a safe space to represent code and none of the UI constraints (or any HTMLElement extend) represents such place ... the solution is <script> tag because it can be anywhere, most interestingly in the head of a document, like a comment could, plus it's semantic and it doesn't translate entities that break otherwise the innovation such project is trying to bring to the world.

The discussion is happening here and, as you maybe read, the ideal solution would be to have a <script is="py-script" type="text/x-python"> element we can use for the project purpose, but, as explained in there, we don't want to put the polyfill I wrote on developers shoulders and, if that polyfill won't ever go away, it doesn't look like the best way ahead we have neither ... as it's also more verbose compared to a <script type="py-script"> alternative, which already won't block, it won't parse entities, it won't fail if CSS to hide those elements didn't load from CDN or wasn't present, it won't ever cause FOUC, and so on and so forth.

Long story short: bugs are coming due entities not expected or parsed in the wild, because the project right now uses custom elements on the page, that inevitably cause problems to both browsers, users, and ourselves (we have a fancy dance to maintain to parse innerHTML into a document to then retrieve its textContent).

So, if nothing, this piles up to the possibilities that the inability to extend builtin is blocking in the wild ... but at least it's not just about layout or content or hierarchy, it's a real-world use case that's blocked (or better, inevitably related as discussion) somehow by this current situation.

clshortfuse commented 1 year ago

I decided to sit down and track all the issues with a11y, using native HTMLInputElement with Web Components. I've filed that over at https://github.com/WICG/webcomponents/issues/996 with a CodePen here

Specific to Webkit, I've noticed the following:

I've been working around these issues for the past few months and I've mostly just ignored them. I've ignored them in the sense I reason "that's just how things are" and never filed the bugs. But I'm starting to realize that was a mistake. The sooner I file them, the sooner they get fixed, and the cleaner my code can be.

There's actually no reason to even use <label> inside a Shadow DOM if we can use aria-labelledby and point it to a <slot> element. I was just wrapping text content with <label>, but that a very light DOM style way of working since, in light DOM, it allows screen-reader to read the content without having to generate an id to tag aria-labelledby. In truth, a Web Component should be able to have this structure:

 <x-button>
    #shadow-root
      <input type=button aria-labelledby=slot>
      <slot id=slot aria-hidden=true></slot>
  </x-button>

That's it. You shouldn't need to even get into the proposed "cross-root ARIA". I'm not saying it doesn't have it's uses, but they should be pretty rare or for extremely complex controls. But the above structure doesn't work in Webkit. Screen-reader will read the label as blank. If we can fix that, we can use slimmer trees as well avoid having to manually wire all the facets of ARIA (value, checked, min, max, step, etc) over JS.


On the larger note of Customized Built-ins. I feel like most issues stem from a multi-element structure: <fieldset>, <form>, <table>, etc. FormAssociated was a good step in the right direction, but it's really just one component. We shouldn't need create FieldsetAssociated, TableAssociated, PictureAssociated etc for every possible element that participates in a tree. I'm just brainstorming here, but I feel like it would make more sense for there to be a shared API of sorts.

I think it can make sense for a component to tap into more aspects of ElementInternals, that are not just <form>. I've personally held off on working on "TableAssociated" web components because the lack of an API.


But at the same time, I recently would like to overload aspects of <form>. For example, there is no way to intercept a form submission attempt to manually validate controls. The "correct" way is to use the [novalidate] attr, and then manually perform your validation. It seems pretty hacky, whereas an actual overload would make more sense.

rniwa commented 1 year ago

Would it be possible to test Safari version 16.4 (18615.x)? We added the support for ElementInternals with ARIA.

clshortfuse commented 1 year ago

@rniwa My Macbook Pro borked, so I've been using Playwright's WebKit build for Linux. I've tested ElementInternals on it without a polyfill and it seems to be working.

Playwright claims it's 16.4, though it shows an internal number of 1811 🤷 . I don't think it's related to ElementInternals but I do have an 2015 iMac I will test later. Still, the issue is HTMLInputElement by themselves don't report a label with HTMLSlotElements:

image

unless you override display:contents with something like display:inline-block:

image

I'm using ElementInternals just for setFormValue and would like to use native elements as much as possible for ARIA (less code).

Side note: Nice work on ElementInternals. From my testing formStateRestoreCallback is only properly working on Webkit. Improper on Edge and never fired on Chromium or Firefox. 🤗

Edit: Playwright user agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15

clshortfuse commented 1 year ago

Tested on real Mac, Safari 16.4. Same issue.

Filed: https://bugs.webkit.org/show_bug.cgi?id=254934

dannymcgee commented 1 year ago

@clshortfuse

 <x-button>
    #shadow-root
      <input type=button aria-labelledby=slot>
      <slot id=slot aria-hidden=true></slot>
  </x-button>

I'm actually pretty confused by this example — doesn't aria-hidden="true" completely remove that node from the accessibility tree? What's the reason for adding that to the slot?

clshortfuse commented 1 year ago

I understand it looks weird. You would reason that you're removing it from ARIA. I noted this in the codepen:

Using [aria-hidden=true] on <slot> will avoid screen-readers from interpretting slot as text to be read. It should only serve as a label for the input. Mostly screen-readers do this implicitly, but it is best to code this explicitly.

Basically, screen readers will see that element was used by as a label and skip over it. It makes sense if you look at the tree. Here's how it differs on Chrome without hiding it:

image

And then applying aria-hidden=true to the slot:

image

It's a matter of being explicit and not needing the screen-reader to guess.

trusktr commented 1 year ago

(It seems this thread has gone a little off-topic.)

Customized built-ins were not implemented in WebKit for good reasons. It is a strange way to implement extension of classes (this conversation covered elsewhere already).

I don't see an alternative proposed position. That would be useful!

Alternatives like element-behaviors and custom-attributes (naming of those APIs bike sheddable) solve the same issues better than customized built-ins, and don't muddy the shape of the Custom Elements definition API surface.

Those alternatives won't take much effort to implement either, because the Custom Elements implementation already has the machinery! A native implementation will be much simpler, as a (simple) polyfill requires use of MutationOberver which makes the polyfill much more complicated for both devs and users.

Why do we insist on keeping bad API shapes?

WebReflection commented 1 year ago

Those alternatives won't take much effort to implement either,

So why none of those alternatives has been moved forward?

because the Custom Elements implementation already has the machinery

does that mean builtin can't benefit because builtin don't have Custom Elements' machinery?

Why do we insist on keeping bad API shapes?

because we're stuck with zero alternatives shipped and no way to extend builtin elements used in every major framework out there?

bahrus commented 1 year ago

I am. Feedback appreciated.

Yes, I am hoping someone with more clout takes it up, but I wanted to get my two cents in first, before we go off track (in my opinion).

colepeters commented 1 year ago

@trusktr Agreed that things have gone a little off topic here; I've found things hard to follow myself.

The alternatives you mentioned look really intriguing, though I'm not sure if they solve the issue I mentioned — that is, maintaining compliant element hierarchy with custom elements (the example I gave using picture > source demonstrates this clearly I think). I'm going to be taking a look at these regardless.

However, I think it's fair to say that there's no proposed alternative here because Webkit's stance is an exception to the spec which is already implemented in every other modern browser. We can do what we need to do everywhere else, right now, but not in Webkit. I haven't yet seen a proposal from Webkit that addresses issues like strict element hierarchy (and there lots of cases where this is critical). If Webkit cannot themselves provide a working alternative that all other browsers can agree on, it seems to me the right thing to do is for them to honour the spec by implementing it, rather than leaving us to figure it out with third parties, polyfills, etc.