Skip to content

Conversation

mizgaionutalexandru
Copy link
Contributor

@mizgaionutalexandru mizgaionutalexandru commented Aug 12, 2025

Description

This PR modifies the aria-label update logic of the ButtonBase in order to correctly update when the label property changes during its lifetime, gating the buttons that use the PendingStateController to not override their aria-label manipulation.

Unit tests were added to validate these usecases in the future. All existing tests pass.

Implementation based on #5674

Motivation and context

Currently when the label property changes, the aria-label of the button components do not reflect this change.
This issue was introduced in 0.48.0 with the new PendingStateController, hence this PR fixes a regression.

Related issue(s)

Screenshots (if appropriate)

labelSWCdemo.mp4

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Validate aria-label updates when label updates

    1. Go to the button storybook
    2. Inspect the element and get its aria-label
    3. Modify the element's label
    4. Get the element's new aria-label
    5. Observe the updated aria-label
  • Validate aria-label values when element's content changes

    1. Repeat test 1
    2. Change the element's textContent/innerText/innerHTML
    3. Get the element's aria-label
    4. Observe that it stays the same

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

Copy link

changeset-bot bot commented Aug 12, 2025

🦋 Changeset detected

Latest commit: 605828b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 84 packages
Name Type
@spectrum-web-components/button Patch
@spectrum-web-components/action-bar Patch
@spectrum-web-components/action-button Patch
@spectrum-web-components/alert-banner Patch
@spectrum-web-components/alert-dialog Patch
@spectrum-web-components/button-group Patch
@spectrum-web-components/coachmark Patch
@spectrum-web-components/dialog Patch
@spectrum-web-components/infield-button Patch
@spectrum-web-components/picker-button Patch
@spectrum-web-components/picker Patch
@spectrum-web-components/search Patch
@spectrum-web-components/tags Patch
@spectrum-web-components/toast Patch
example-project-rollup Patch
example-project-webpack Patch
@spectrum-web-components/bundle Patch
@spectrum-web-components/vrt-compare Patch
@spectrum-web-components/action-group Patch
@spectrum-web-components/action-menu Patch
@spectrum-web-components/combobox Patch
@spectrum-web-components/contextual-help Patch
@spectrum-web-components/menu Patch
@spectrum-web-components/overlay Patch
@spectrum-web-components/tabs Patch
@spectrum-web-components/number-field Patch
@spectrum-web-components/custom-vars-viewer Patch
@spectrum-web-components/story-decorator Patch
documentation Patch
@spectrum-web-components/breadcrumbs Patch
@spectrum-web-components/popover Patch
@spectrum-web-components/tooltip Patch
@spectrum-web-components/truncated Patch
@spectrum-web-components/top-nav Patch
@spectrum-web-components/slider Patch
@spectrum-web-components/card Patch
@spectrum-web-components/eslint-plugin Patch
@spectrum-web-components/accordion Patch
@spectrum-web-components/asset Patch
@spectrum-web-components/avatar Patch
@spectrum-web-components/badge Patch
@spectrum-web-components/checkbox Patch
@spectrum-web-components/clear-button Patch
@spectrum-web-components/close-button Patch
@spectrum-web-components/color-area Patch
@spectrum-web-components/color-field Patch
@spectrum-web-components/color-handle Patch
@spectrum-web-components/color-loupe Patch
@spectrum-web-components/color-slider Patch
@spectrum-web-components/color-wheel Patch
@spectrum-web-components/divider Patch
@spectrum-web-components/dropzone Patch
@spectrum-web-components/field-group Patch
@spectrum-web-components/field-label Patch
@spectrum-web-components/help-text Patch
@spectrum-web-components/icon Patch
@spectrum-web-components/icons-ui Patch
@spectrum-web-components/icons-workflow Patch
@spectrum-web-components/icons Patch
@spectrum-web-components/iconset Patch
@spectrum-web-components/illustrated-message Patch
@spectrum-web-components/link Patch
@spectrum-web-components/meter Patch
@spectrum-web-components/modal Patch
@spectrum-web-components/progress-bar Patch
@spectrum-web-components/progress-circle Patch
@spectrum-web-components/radio Patch
@spectrum-web-components/sidenav Patch
@spectrum-web-components/split-view Patch
@spectrum-web-components/status-light Patch
@spectrum-web-components/swatch Patch
@spectrum-web-components/switch Patch
@spectrum-web-components/table Patch
@spectrum-web-components/textfield Patch
@spectrum-web-components/thumbnail Patch
@spectrum-web-components/tray Patch
@spectrum-web-components/underlay Patch
@spectrum-web-components/base Patch
@spectrum-web-components/grid Patch
@spectrum-web-components/opacity-checkerboard Patch
@spectrum-web-components/reactive-controllers Patch
@spectrum-web-components/shared Patch
@spectrum-web-components/styles Patch
@spectrum-web-components/theme Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Aug 12, 2025

📚 Branch Preview

🔍 Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-5691

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

Copy link
Contributor

github-actions bot commented Aug 12, 2025

Tachometer results

Chrome

button permalink

test-basic

Version Bytes Avg Time vs remote vs branch
npm latest 501 kB 50.51ms - 51.75ms - faster ✔
5% - 9%
2.84ms - 5.25ms
branch 475 kB 54.14ms - 56.22ms slower ❌
5% - 10%
2.84ms - 5.25ms
-
Firefox

button permalink

test-basic

Version Bytes Avg Time vs remote vs branch
npm latest 501 kB 110.03ms - 114.77ms - faster ✔
2% - 8%
2.45ms - 9.91ms
branch 475 kB 115.69ms - 121.47ms slower ❌
2% - 9%
2.45ms - 9.91ms
-


private updateAriaLabel(): void {
if (this.label) {
this.setAttribute('aria-label', this.label);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user sets aria-label directly after the component has set it via the label property, the component might override the user's intent here. I would be checking if aria-label is set by the user or not before we trigger component logic.

private _userSetAriaLabel = false;

connectedCallback() {
    super.connectedCallback();
    // Detect if aria-label was set by the user before component logic runs
    this._userSetAriaLabel = this.hasAttribute('aria-label');
}

attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
    if (name === 'aria-label' && oldValue !== newValue) {
        this._userSetAriaLabel = true;
    }
    super.attributeChangedCallback(name, oldValue, newValue);
}

private updateAriaLabel(): void {
    if (this._userSetAriaLabel) return; // Don't override user intent
    if (this.label) {
        this.setAttribute('aria-label', this.label);
    } else {
        this.removeAttribute('aria-label');
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we want to allow both label and aria-label on the component at the same time? Isn't label used just for aria-label purposes, as per docs and code?
If the consumer doesn't set a label but sets an aria-label, the component will have the desired aria-label.

Copy link
Contributor

@Rajdeepc Rajdeepc Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically we generally don’t want both to apply at the same time. The implementation should respect a consumer’s explicit aria-label and only use label as a fallback for accessibility. This will avoid race conditions and user confusion.
I think we should document that aria-label always takes priority over label.

  • If both are set, use aria-label.
  • If only label is set, set aria-label to label.
  • If neither is set, no aria-label is present.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this might be a good improvement, even though there is no need for a consumer to provide both label and aria-label. Currently, on main, if label and aria-label are set, the label will override the aria-label. That being said, I feel like adding more is out of scope and we're looking forward to see this fix merged. Can we keep this in mind and act on it in another PR if it truly is something we want?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think keeping that change as out of scope makes sense. I'd like to see us create a follow-up ticket to cover the suggestion though. @Rajdeepc would you be able to take care of that and include your code suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree with both of you! Let's create a follow up ticket and unblock this PR for now.


// Don't override PendingStateController's aria-label changes
if (changed.has('label') && !this.isPendingState()) {
this.updateAriaLabel();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both update and updated might call updateAriaLabel for the same change. Can you call updateAriaLabel in only one lifecycle or just make sure it runs once per change.
Do you have any specific reason to call this in update method?

Copy link
Contributor Author

@mizgaionutalexandru mizgaionutalexandru Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This surfaces multiple conflicts since the Button component has label and the PendingStateController that modify the aria-label. Maybe a long term major change would be to get rid of the label prop altogether 🤔 or if we keep it we need a library level way to determine if it's only for aria-label purposes or not, in order to update the PendingStateController accordingly.
For now I gated the updates according to the pending state to happen either in update or updated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool thats something we would like to do in future. Do you mind validating this in screen readers?

Copy link
Contributor Author

@mizgaionutalexandru mizgaionutalexandru Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I added a screen recording, validating the manual test cases with a screen reader. Let me know if I should add anything else.

Copy link
Member

@jnurthen jnurthen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should not add support for consumers using aria-label directly on SWC.
aria-label is an aria global so it forces the component itself to be exposed as an accessible node. If in the future we wanted to change the implementation to instead use a button within the shadow DOM we wouldn't be able to do so.

@mizgaionutalexandru
Copy link
Contributor Author

IMO we should not add support for consumers using aria-label directly on SWC. aria-label is an aria global so it forces the component itself to be exposed as an accessible node. If in the future we wanted to change the implementation to instead use a button within the shadow DOM we wouldn't be able to do so.

This PR does not "add support for aria-label". It only fixes an inconsistency bug in that area.
Am I missing something here? @jnurthen

}

private updateAriaLabel(): void {
if (this.label) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we explicitly check for undefined instead because empty quotes would be a valid label (label="")? cc @nikkimk is that allowed on buttons or am I just thinking of images that use the empty label string sometimes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I haven't thought of that since this check was also used previously. 🤔
My take is that we don't want to allow consumers to use an empty string as an aria-label, they can just skip using it altogether. I know something like this is used with the alt attribute of images though.


private updateAriaLabel(): void {
if (this.label) {
this.setAttribute('aria-label', this.label);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think keeping that change as out of scope makes sense. I'd like to see us create a follow-up ticket to cover the suggestion though. @Rajdeepc would you be able to take care of that and include your code suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: button-base aria label not updating on change
4 participants