Certain props are not converted from camelCase to kebab-case when rendering
Version
3.2.31
Reproduction link
github.com https://archive.softwareheritage.org/browse/origin/directory/?origin_url=https://github.com/vincerubinetti/vue-aria-bug×tamp=2022-02-24T05:36:51.635332%2B00:00
Steps to reproduce
Certain popular aria attributes like label
and expanded
get correctly converted from camelCase to kebab-case, but others like controls
and haspopup
do not.
What is expected?
For behavior to be consistent. Either for vue to convert all props back to kebab-case, or at least for it to convert all HTML supported props and not just some of them at random.
What is actually happening?
Only some props are being converted. The behavior is inconsistent and unintuitive.
Related issue: https://github.com/vuejs/vue/issues/11913
This is also especially problematic when trying to pass a lot of props at once, like this:
Component.vue:
<slot
aria-label="test string"
aria-multiselectable="test string"
aria-expanded="test string"
aria-controls="test string"
aria-haspopup="test string"
aria-activedescendant="test string"
>
</slot>
Using it:
<Component v-slot="props"><div v-bind="props"></div></Component>
Because Vue converts all of those kebab-cases to camelCase when passing it down, but then it only transforms some of them back to kebab-case when spreading them onto the slot element.
As a result, I have to manually run a toKebabCase
function on all the v-bind
props every time I use the component, which is cumbersome, tedious, and unexpected.
In fact,The ariaHasPopup property of the Element interface reflects the value of the aria-haspopup attribute.So you need use aria-has-popup
.
Not quite. I'd still rate this a bug.
Why are some attributes not hyphenated?
Because we currently don't actively hyphenate attribute values in the patch phase. Usually that's not an issue as people normally use hyphenated attribute names in templates, but it can be an issue when i.e. passing camelCased props in an object to vbind.
Then why are some attributes hyphenated, and others not?
Because Vue prefers to set them via their element property counterparts if they exist. el.ariaLabel
does exist, so Vue uses this to set it, which is then reflected as the attribute aria-label
by the browser.
But ariaHaspopup
does not exist - the property is named el.ariaHasPopup
(which is inconsistent as the attribute is indeed called aria-haspopup
). so this would work again right now:
<div :ariaHasPopup="true" />
so what's the solution?
The simple solution would be to enforce hyphenation when we add attributes (unless we are in SVGs which can have camelCase attributes).
Thank you for the information. It sounds like part of the issue is just the inconsistency of the spec itself. But yeah, if Vue is going to try to do this casing stuff "automagically", I would expect it to at least handle all the standard attributes.
So you need use aria-has-popup.
This of course won't work because linters will throw a fit:
And I'm not going to disable that rule or add a bunch of manual attribute exceptions or something. Also, adding an extra kebab to attributes that aren't there in the spec is just asking for trouble down the line. Someone's gonna look at that code, think it's a mistake, rename it to the correct HTML attribute, then cause bad silent errors that latently and inexplicably wreak havoc.
But I was going to say, it sounds like I can just specify attribute names in camelCase as a stopgap. At least that would bring attention to future people viewing my code that this is a special case and you should leave it alone.
But then I tried it and it doesn't seem to work for all of my properties:
<slot
:id="`select-${id}`"
role="combobox"
:ariaLabel="name"
ariaMultiSelectable="true"
:ariaExpanded="expanded"
:ariaControls="`list-${id}`"
ariaHasPopup="listbox"
:ariaActiveDescendant="`option-${id}-${highlighted}`"
></slot>
<button id="select-16" role="combobox" aria-label="Filter by Score" aria-multiselectable="true" aria-expanded="false" ariacontrols="list-16" aria-haspopup="listbox" ariaactivedescendant="option-16-0"></button>
(Note that I tried all different combinations of kebab-case, capitalizations, event making the props dynamic vs static. None worked to fix activedescendant
or controls
).
So it seems like I have to stick with my dirty kebabify
function solution for now.
it seems activedescendant
and controls
don't have matching element properties, so here my second point from above doesn't take effect.
A question: why don't you provide them in kebap-case in the first place (until we have fixed this)? Are they set up in JS where it's cumbersome to define hyphenated properties on an object?
This might be a simpler work around for the slot binding
it should also still work after the proposed pr is merged
<slot
aria--label="test string"
aria--multiselectable="test string"
aria--expanded="test string"
aria--controls="test string"
aria--haspopup="test string"
aria--activedescendant="test string"
>
</slot>
probably not linter friendly
The camel-casing of slot props was introduced for behavior consistency with Vue 2, and unfortunately cannot be changed due to its breaking nature.
A possible workaround is to avoid camel-casing props that start with aria-*
or data-*
. These are the main possible edge cases for the issue we have here, because absolute majority of native HTML attributes do not contain hyphens. with the exception of accept-charset
and http-equiv
. The downside is that this still leaves theoretical edge cases, and does not cover SVG attributes (which can be both kebab-case and camelCase).
I sent a draft PR to solve this. There's still a few things to talk/think through though (hence a draft).