fix(button): sync aria attributes between host and native button - #31264
Zac-Smucker-Bryan wants to merge 14 commits into
Conversation
|
@Zac-Smucker-Bryan is attempting to deploy a commit to the Ionic Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Thanks for this PR!
I talked to the team about this PR and, while it does look solid and solve the issue's title problem, there's a bit more to it than this.
There's actually 2 remaining issues in the original issue that won't be resolved by this PR:
- Many other aria attributes not being copied
- Other components that can act like buttons not getting this fix (this was a comment on the issue, but should still be addressed)
The first one I've created an internal follow-up ticket to address because it's much more involved than this solution and something we'll need to review on how to do well. We can't just add 50 @Watchs and call it good.
The second one I'd prefer if we address in this PR
codeCraft-Ritik
left a comment
There was a problem hiding this comment.
Great fix! Synchronizing dynamic aria-description updates with the native button improves accessibility while keeping the component behavior consistent. The accompanying test provides good confidence in the change.
ShaneK
left a comment
There was a problem hiding this comment.
Thanks for reworking this! The helper approach is the right direction and a lot better than adding a @watch per attribute. Left a handful of comments inline.
The two I'd want sorted before this goes in: the watcher gets torn down on disconnectedCallback but never recreated, so aria sync breaks the first time the button moves in the DOM, and covering the other button-like components (ion-item/ion-card at least) from my earlier review. The rest are nits.
|
PR revised based on feedback and expanded scope |
ShaneK
left a comment
There was a problem hiding this comment.
Thank you so much for taking a shot at this massive undertaking! Let me know if you'd like me to help you out with this, I know it's a pretty daunting task
@ShaneK Yes, this PR is pushing the limits of what I know. But I have learned a lot. I would love your help, especially with the I have done an initial review of your comments and think I can address everything else with more helpful tests and such, once we get the right helper structure in place. My only other question would be: in your comment #31264 (comment), you say "Moving the inherit into connectedCallback fixes the watcher lifetime, but I think it trades that for a worse problem." If the ultimate helper means things can go back to the way they were in terms of calls, all the better, I just didn't want to create other issues without thinking it through. Let me know the best way to collaborate to get it done. |
|
As I am working through things, I think I can have a revised PR/commits/tests etc. next week @ShaneK, and think I even have a helper that is simpler based on your specific feedback. |
Adds @watch('aria-description') to button.tsx before onAriaChanged
Set aria description and both buttons should match. Update aria description on host button, and both buttons should still match.
Previously, ARIA attributes inherited from the host were only captured once at componentWillLoad. Attributes set or changed after initial load (e.g. by ion-input-password-toggle updating aria-label/aria-pressed as visibility toggles) were not reflected onto the native button, causing screen readers to announce stale values unless a watch decorator was used for each attribute. Adds watchAttributes/watchForAriaAttributeChanges to helpers.ts, which use a MutationObserver to keep inherited ARIA attributes in sync for the lifetime of the component. Replaces the previous per-attribute @watch decorators with this more general mechanism. Update Button.tsx to reflect this and use these new helpers. Add tests to test syncing all attributes. Fixes ionic-team#30626
…pt to helper Change disconnect to destroy to match other ionic conventions Update onChange to accept null values Add support for removeAttribute, including if null values triggered
…ttributes to connectedCallback Import helper to ion-item and ion-card move inheritedAriaAttributes to connectedCallback in these components to preserve helper call order
Update tests for ion-button with annotations Add tests for removeAttribute and attribute sync to ion-button, ion-card, and ion-item
…on, item, and card
Consistency based on element being tested
6e3e997 to
f142f36
Compare
@utils/helpers was imported twice and passed npm run lint but failed build. Now, it is imported only once.
ShaneK
left a comment
There was a problem hiding this comment.
This is really close now, and the helper itself is in good shape after the last round.
The one I'd want sorted is the ignore list on ion-card and ion-item. They inherit only aria-label but watch all ~49 ARIA attributes, so role and aria-hidden reach the native element after load when neither does at load. On ion-item that means the inner <button> can end up carrying role="listitem". Swapping those two calls to watchAttributes(this.el, ['aria-label'], ...) covers it.
The other is the detach/reattach and aria-disabled tests. I reverted the four source files to main, kept the specs, and both still pass, so neither covers the thing it's named for.
Everything else is nits. If you'd rather hand this off at this point just say the word and I can finish it up. Thank you for sticking with this one, it's a much better fix than where it started.
Issue number: resolves #30626
What is the current behavior?
The native
buttoninsideion-buttonis not being updated with the aria-attributes if it changes onion-buttonafter initial render, includingaria-description. Additionally, the same dynamic is happening with other button like elements, includingion-cardandion-item.What is the new behavior?
The native
buttoninsideion-buttonupdates with the aria attributes if it changes onion-button. Changes include:ariaAttributesaria-disabledsince it is managed inrender()separately. Render forced with prop change.private ariaWatcher?: AttributeWatcher;to class Buttonprivate didLoad = false;for conditional use inconnectedCallbackand use in addedcomponentDidLoadfor starting the watcher after initial component loaditem.tsx,ariaWatcherandwatchForAriaAttributeChanges, replicating functionality of the watch block, only applying toaria-labelcurrentlyprivate didLoad = false;for conditional use inconnectedCallbackand use in existingcomponentDidLoadfor starting the watcher after initial component loaditem.e2e.test, add tests to ensure:card.tsx,ariaWatcherandwatchForAriaAttributeChanges, replicating functionality of the watch block, only applying toaria-labelcurrentlyprivate didLoad = false;for conditional use inconnectedCallbackand use in addedcomponentDidLoadfor starting the watcher after initial component loadcard.e2e.test, add tests to ensure:Does this introduce a breaking change?