diff --git a/.changeset/hot-clouds-burn.md b/.changeset/hot-clouds-burn.md new file mode 100644 index 00000000000..05c6dba47c6 --- /dev/null +++ b/.changeset/hot-clouds-burn.md @@ -0,0 +1,7 @@ +--- +'@spectrum-web-components/button': patch +--- + +**Fixed** aria-label updates when `label` property changes. + +- Added pending state checks to prevent `aria-label` conflicts when `PendingStateController` is managing accessibility attributes diff --git a/packages/button/src/ButtonBase.ts b/packages/button/src/ButtonBase.ts index 196394b91c6..d5a4e98a35f 100644 --- a/packages/button/src/ButtonBase.ts +++ b/packages/button/src/ButtonBase.ts @@ -23,6 +23,10 @@ import { import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js'; import { Focusable } from '@spectrum-web-components/shared/src/focusable.js'; import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js'; +import { + type HostWithPendingState, + PendingStateController, +} from '@spectrum-web-components/reactive-controllers/src/PendingState.js'; import buttonStyles from './button-base.css.js'; /** @@ -220,18 +224,34 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [ } } + /** + * Type guard to check if this instance implements HostWithPendingState. + */ + private isHostWithPendingState(): this is this & HostWithPendingState { + return ( + 'pendingStateController' in this && + this.pendingStateController instanceof PendingStateController + ); + } + + private isPendingState(): boolean { + return this.isHostWithPendingState() && this.pending === true; + } + + private updateAriaLabel(): void { + if (this.label) { + this.setAttribute('aria-label', this.label); + } else { + this.removeAttribute('aria-label'); + } + } + protected override firstUpdated(changed: PropertyValues): void { super.firstUpdated(changed); if (!this.hasAttribute('tabindex')) { this.setAttribute('tabindex', '0'); } - if (changed.has('label')) { - if (this.label) { - this.setAttribute('aria-label', this.label); - } else { - this.removeAttribute('aria-label'); - } - } + this.manageAnchor(); this.addEventListener('keydown', this.handleKeydown); this.addEventListener('keypress', this.handleKeypress); @@ -243,6 +263,12 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [ this.manageAnchor(); } + // Do not update aria-label if component is in pending state, + // as PendingStateController may manage it for accessibility. + if (changed.has('label') && !this.isPendingState()) { + this.updateAriaLabel(); + } + if (this.anchorElement) { // Ensure the anchor element is not focusable directly via tab this.anchorElement.tabIndex = -1; @@ -256,14 +282,11 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [ this.anchorElement.addEventListener('focus', this.proxyFocus); } } + protected override update(changes: PropertyValues): void { super.update(changes); - if (changes.has('label')) { - if (this.label) { - this.setAttribute('aria-label', this.label); - } else { - this.removeAttribute('aria-label'); - } + if (changes.has('label') && this.isPendingState()) { + this.updateAriaLabel(); } } } diff --git a/packages/button/test/button.test.ts b/packages/button/test/button.test.ts index da947febb52..8a85512a798 100644 --- a/packages/button/test/button.test.ts +++ b/packages/button/test/button.test.ts @@ -423,6 +423,45 @@ describe('Button', () => { expect(el.getAttribute('aria-label')).to.equal('clickable'); }); + it('updates aria-label when label changes', async () => { + const el = await fixture