-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal for Element.currentLang and Element.currentDir #7039
Comments
Moving this to HTML as DOM doesn't define language or direction. We've had discussions about this kind of feature in the past. If someone could dig those up that would be helpful. |
On the other hand, this problem doesn't exist without SD or built-in extends, but enabling something to go through in SD might be the beginning of tons of other requests. |
This works if you know the language(s) being used. If the language is arbitrary, there's really no mechanism to determine it without brute forcing it (silly) or DOM traversal (expensive).
This problem is not exclusive to custom elements with shadow roots. You'd still need DOM traversal to reliably use the current language or direction of any element. A non-custom element use case may be a library or framework that handles localization and would prefer to use the platform-provided
I'd argue that localization is fairly unique and shouldn't be reset. At least, I can't think of a single use case where localization shouldn't persist until explicitly changed in the DOM tree. |
lang and dir are not coupled though, but I agree indeed there's no way to know these directives without some JS seppuku. however, dir is usually language dependent, and if mapping lang to a dir is not too trivial task, and if the browser knows how it should behave accordingly to either lang or user settings, it could be awesome to understand that, yet I believe any hook on the |
It's worth noting that there can be multiple languages in an HTML document, so referencing
It would be useful for libraries, utilities, and child components to be self-aware of the intended language and direction so they can render with the correct locales. |
fair enough, then I guess |
for direction, it seems you can at least do this:
seems to work even in scenarios where elements are nested with different values, including being implicitly inherited from some ancestor. see here https://codepen.io/WickyNilliams/pen/QWqgXOQ though I still think a dedicated property is useful. I would say there should even be some way to observe changes to these values. If an element gets re-parented, or some (unknown) ancestor has its lang/dir values change, aside from polling (ugh) there would be no way to know and react to such a change |
Good tip. I believe this will trigger a reflow, though, so a cached property would be preferred.
I agree. Perhaps an event similar to |
agreed, it is far from ideal, but a decent workaround for now. can an MO cover all cases? what's the perf impact of observing the entire subtree from the document root? what happens if there are nested, intermediary shadow roots and the dir/lang is subject to change inside any of them? you'd have to climb up the tree and attach an observer at every root, as well as document? feels like there might be a ton of edge cases! |
It won't pick up attribute changes in shadow roots, so no. Each component that's interested would need to attach a separate observer to its respective shadow root, which isn't ideal. A composed event that bubbles up would be optimal, but that should probably be a separate proposal. But since we're here, perhaps |
I think observability is a must - that's the trickiest part of this, and it's something browsers already implement in order to support I presented a couple of options in #9918: Option 1Extend Option 2Provide a way to observe changes in CSS selector matching. const result = element.matchSelector(`:lang(${element.currentLang})`);
result.addEventListener('change', () => {
const newLang = element.currentLang;
// …
}); This is based on |
interesting ideas. extending mutation observer seems off to me, since MO is concerned with sub-trees, whereas lang/dir are the opposite (comes from above). is there precedent for an observer which works that way?
|
We could also split the difference between |
It's only concerned with subtrees if you opt into that, otherwise it's just concerned with the element being observed. But you're right that none of the values it observes are computed.
Intersection and resize observers observe computed values that are impacted by things all over the tree.
I agree it would be generally useful, however it might not be the best fit for this use-case. The example I gave only observes one change - you'd need to un-observe and observe the new value each time. Not too tricky though. The potentially trickier issue is timing. Something like |
Hmm, browsers don't seem to respect the element's language when it comes to |
sorry yes, i used an overloaded term with sub-trees. i meant whether entries are derivations of the element/its descendants vs an element/its ancestors. i guess if there is an accompanying computed property on the element, then conceptually such an observer doesn't differ. makes sense re: IO/RO.
hmm yes, that would be quite an awkward API.
might the timing issues cause any temporary inconsistent states? e.g. i'm thinking of a case where i change to hebrew as a lang and rtl as dir - could you end up with hebrew shown in a LTR layout, or the previous content in an RTL layout? either visually, or from the perspective of running code. i'm not familiar enough with browser internals to understand the implications |
The browser would have to calculate styles in order to render, and that would trigger the observer, so I don't think that's a problem. If a tab is "not visible" (therefore not generating frames, therefore not calculating style), running code (eg You'd still get a bit of that with an observer, since it'd be offset by a microtask, but tying it to rendering seems confusing. Observers don't need to be tied to microtasks fwiw. Mutations observers are tied to microtasks, whereas resize/intersection observers are tied to rendering. It feels like this should be timed similar to mutation observers, since it's DOM mutations that cause lang to change. |
Kinda related, something we went around circles in was trying to understand the difference between:
In the end what we want to know is when should we as authors use:
|
As the OP, I want to point out that I think @jakearchibald's proposal for Consider this my vote for that as an alternative to the aforementioned properties. Additionally, the use cases for To recap, from Jake's post above: const result = element.matchSelector(`:lang(${element.computedLang})`);
result.addEventListener('change', () => {
const newLang = element.computedLang;
// …
}); |
A complete solution using a hypothetical matchesSelector let result = element.matchSelector(`:lang(${element.currentLang})`);
function handleChange() {
const newLang = element.currentLang;
result = element.matchSelector(`:lang(${newLang})`);
result.addEventListener("change", handleChange, { once: true })
}
result.addEventListener('change', handleChange, { once: true }); It's quite awkward having to remember to cleanup and attach a new listener on every change imo. Of course this could be cleaned up a little, but it's just to demo it's not as easy as the snippet above |
Another difficult to track ancestor influenced state that would be useful to observe for changes would be Recently ran into an issue where we would like to have that propagate into the shadow root of a custom element which is currently blocked from propagating on its own. So we are trying to investigate ways to observe the state and reflect it in the shadowroot manually. |
@rajsite that feels different to this issue. Can you file a new issue for your request? |
Now that the const isLTR = someElement.matches(":dir(ltr)") Still, being able to observe this would be nice. |
We discussed this today at TPAC in a breakout with @dbaron @jyasskin @fantasai (who also asked @annevk @r12a). Originally, the sentiment for However, after thinking about it some more, some folks were worried that once Ideas discussed were:
During the discussion I was of the opinion we should do 1, but now I’m leaning towards 2, potentially with a very bare-bones object at first. We could later make such objects constructible to address use cases not connected to the DOM (e.g. |
URL exposing APIs follow option 1 |
In addition the I sympathise with the point of users rolling their own parsing but I'd suggest users shouldn't be rolling their own i18n, let alone parsing, and Intl APIs are provided for exactly this. It's just not trivial to get the computed lang to do so. |
To what extent is language even parsed internally versus just a string being forwarded? Very much suspect it's the latter. E.g., https://software.hixie.ch/utilities/js/live-dom-viewer/?%3Cdiv%20lang%3D1test%3Edfsdfs%0A%3Cstyle%3E%0A%3Alang(%5C31test)%20%7B%20background%3Alime%20%7D%0A%3C%2Fstyle%3E (although for some reason this doesn't work properly in Firefox). (Whereas language tags proper are supposed to start with two, three, or five to eight letters as I understand it.) |
@keithamus ooh that’s a good point. I’d argue it should be an @annevk I think @fantasai was referring to this sort of thing: https://dabblet.com/gist/08224b9a69810f6fbffe00dbd1181dca :lang(en) { border: 2px solid blue } <div lang="en-us">I’m blue</div> |
Ah, that makes sense, but I'm relatively sure that parsing happens on top of the computed language. And maybe that explains why browsers have different answers for my test case, although https://www.rfc-editor.org/rfc/rfc4647#section-3.3.2 seems pretty clear that it ought to work. |
Wrt #7039 (comment) my concern was about authors who want to do matching would be seduced by the simplicity of doing string-based language matching themselves, so we should make it really obvious and trivial to the right thing. If we only provide a string accessor, they'll be tempted to use string methods for language matching. Even if we provide a utility class or utility method somewhere else that they could optionally use, they won't know that they should be using it (or using So I don't have any strong opinion on how, it could just be providing a very simple |
Another factor we’ve missed in the earlier discussion is tracking changes, which is a hard requirement for this to be useful. So it looks like there are two designs:
I don’t have a strong preference between the two. Both can be shipped incrementally (e.g. the object could just have one property at first). 1 might be more performant, and given how heavy DOM nodes already are, that is important. It also seems a little more consistent with how the rest of the DOM behaves, and a bit easier to spec. 2 could end up a little cleaner in the end and the object can be repurposed to work outside the DOM too. @annevk what do you think? |
Will any decision here relating to |
Given that |
Talking with @annevk this morning, it looks like we’re both in agreement that the shallower design (Option 1) is better for a variety of reasons (consistency, performance, simplicity). So hopefully we could resolve on that later today.
I imagine so, though that’s less of a pain point, as it’s easier to figure it out today. |
I18N was just now wondering if |
@aphillips I would imagine it always returns either |
@LeaVerou That's what we thought, but wanted to capture the question. The language discussion I have added to the TPAC I18N+WHATWG discussion scheduled for later today (2024-09-24) |
Yup, thanks for adding it, I plan to be there! I'd need to leave the CSS WG meeting to join, based on what @past said, I'd estimate we won't get to it before 5pm so I’m planning to drop by around that time. |
Just to confirm we're looking at |
Probably more like I will point out that the TAG principle that @domenic referenced simply says:
Even if this is not cached for some reason, I'd argue that |
I really like the names @keithamus suggests. Not really a fan of following the really elaborate naming of As for getters vs methods, the question is really whether you consider tree traversal a complex operation. It can definitely be costly and is somewhat non-trivial as |
It seems odd to have a change event for a value that requires a function call to get. Or would the value be provided on the event object? |
The event wasn’t really discussed but it seems reasonable to me that it would include the value (and possibly the old value).
Aside; perhaps it would be useful for the TAG to provide a more objective rubric for this. While implementations shouldn’t guide design IMO, having this closer to the reality of what is already available in an objects data vs what needs to be computed might be a more objective measure than the term “complex”. |
@annevk |
The TAG also encourages against putting data in events that could be a simple getter on the target object. But, if doing it that way is a performance issue / breaks other TAG rules, fair enough. Are we sure it isn't something the UA just caches internally already? |
It's not cached internally (unless you count the content attribute map a cache). The implementation does a flat-tree traversal + content attribute retrieval which are both (for certain values of) "fast" lookups that happen a lot. If I were to implement in Chrome (which I'm happy to do when we resolve the design) I think I would just call out to ComputeInheritedLanguage, hopefully y'all can follow the implementation and notice that it's not doing a cache, and it is walking the flat tree (note I wonder; would there ever be scope to include an options bag to the method call which would preclude using a getter? Does that steer the conversation one way or another? As for the events, perhaps it would be useful to put energy into whatwg/dom#1225 which is a more generic solution for observing style/selector invalidation that could easily map to observing such changes and gives us the facility to use the |
I'd like to propose the introduction of two read-only properties on the
Element
object:Element.currentLang
andElement.currentDir
are read-only properties that reflect the element's current language/direction as determined by their or their closest ancestor'slang
anddir
attributes, respectively.The primary use case is to improve i18n in custom elements, but the benefit will also be seen by frameworks that currently use a separate, non-standard context to determine these values. Exposing the current inherited language and direction will provide better localization capabilities by removing performance hurdles and eliminating the need for additional logic and special contexts.
This information isn't currently available without expensive DOM traversal. Furthermore, selectors such as
Element.closest('[lang]')
will stop if they reach a shadow root, requiring recursive logic to break out of them:As a custom element author, it's not uncommon for users to have dozens of components on a page. It's also not impossible for a page to have multiple languages and directions. For components that require localization, the only way for them to inherit
lang
anddir
is via DOM traversal or other non-standard logic. This, of course, isn't very efficient.Being able to reference
Element.currentLang
andElement.currentDir
will solve this in an elegant way using data the browser is likely already aware of.Additional thoughts:
lang
anddir
to pass through shadow roots. If desired, the custom element author can override it by applyinglang
ordir
to the host element or to any element within the shadow root.:lang
and:dir
(limited support). Unfortunately, there's no clean way to discover this value with JavaScript.The text was updated successfully, but these errors were encountered: