From ba8e5c9a1b7857a179c4d56a2ef41de4f8db53c2 Mon Sep 17 00:00:00 2001 From: Casey Eickhoff Date: Mon, 15 Sep 2025 16:12:52 -0600 Subject: [PATCH 01/12] fix(pending-state): correct reflection of aria in pending controller --- .../reactive-controllers/src/PendingState.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/reactive-controllers/src/PendingState.ts b/tools/reactive-controllers/src/PendingState.ts index a76e8987af3..a7787034f3d 100644 --- a/tools/reactive-controllers/src/PendingState.ts +++ b/tools/reactive-controllers/src/PendingState.ts @@ -73,17 +73,27 @@ export class PendingStateController const { pending, disabled, pendingLabel } = this.host; const currentAriaLabel = this.host.getAttribute('aria-label'); - if (pending && !disabled && currentAriaLabel !== pendingLabel) { - // Cache the current `aria-label` to be restored when no longer `pending` + // If the current `aria-label` is different from the pending label, cache it + // or if the cached `aria-label` is different from the current `aria-label`, cache it + if ( + (!this.cachedAriaLabel && + currentAriaLabel && + currentAriaLabel !== pendingLabel) || + (this.cachedAriaLabel !== currentAriaLabel && + currentAriaLabel && + currentAriaLabel !== pendingLabel) + ) { this.cachedAriaLabel = currentAriaLabel; + } + + if (pending && !disabled) { // Since it is pending, we set the aria-label to `pendingLabel` or "Pending" this.host.setAttribute('aria-label', pendingLabel || 'Pending'); - } else if (!pending || disabled) { + } else { // Restore the cached `aria-label` if it exists if (this.cachedAriaLabel) { this.host.setAttribute('aria-label', this.cachedAriaLabel); - } else if (!pending) { - // If no cached `aria-label` and not `pending`, remove the `aria-label` + } else { this.host.removeAttribute('aria-label'); } } From f380b21478088dcbe477eeade59c65f9b0946581 Mon Sep 17 00:00:00 2001 From: Casey Eickhoff Date: Fri, 19 Sep 2025 13:11:05 -0600 Subject: [PATCH 02/12] chore: cache aria helper, label to updated lifecycle --- packages/button/src/ButtonBase.ts | 16 ++++---- packages/button/stories/index.ts | 13 +++++++ packages/button/stories/template.ts | 3 +- packages/button/test/button.test.ts | 39 +++++++++++++++++++ .../reactive-controllers/src/PendingState.ts | 26 +++++++++---- 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/packages/button/src/ButtonBase.ts b/packages/button/src/ButtonBase.ts index 196394b91c6..e604ce18044 100644 --- a/packages/button/src/ButtonBase.ts +++ b/packages/button/src/ButtonBase.ts @@ -225,13 +225,7 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [ 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 +237,14 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [ this.manageAnchor(); } + if (changed.has('label')) { + if (this.label) { + this.setAttribute('aria-label', this.label); + } else { + this.removeAttribute('aria-label'); + } + } + if (this.anchorElement) { // Ensure the anchor element is not focusable directly via tab this.anchorElement.tabIndex = -1; diff --git a/packages/button/stories/index.ts b/packages/button/stories/index.ts index 5d5a4e8b2cb..90493f57134 100644 --- a/packages/button/stories/index.ts +++ b/packages/button/stories/index.ts @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +/* eslint-disable import/no-extraneous-dependencies */ import { html, TemplateResult } from '@spectrum-web-components/base'; import { ifDefined } from '@spectrum-web-components/base/src/directives.js'; @@ -86,6 +87,17 @@ export const argTypes = { type: 'boolean', }, }, + label: { + name: 'label', + type: { name: 'string', required: false }, + description: 'The label to apply to the aria-label of the button.', + table: { + type: { summary: 'string' }, + }, + control: { + type: 'text', + }, + }, }; export const makeOverBackground = @@ -122,6 +134,7 @@ export function renderButton(properties: Properties): TemplateResult { treatment=${ifDefined(properties.treatment)} variant=${ifDefined(properties.variant)} static-color=${ifDefined(properties.staticColor)} + label=${ifDefined(properties.label)} > ${properties.content || 'Click Me'} diff --git a/packages/button/stories/template.ts b/packages/button/stories/template.ts index 0ea6cf6352d..6831ef3795c 100644 --- a/packages/button/stories/template.ts +++ b/packages/button/stories/template.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ - +/* eslint-disable import/no-extraneous-dependencies */ import { html, TemplateResult } from '@spectrum-web-components/base'; import { ifDefined } from '@spectrum-web-components/base/src/directives.js'; import { @@ -30,6 +30,7 @@ export interface Properties { target?: '_blank' | '_parent' | '_self' | '_top'; noWrap?: boolean; iconOnly?: boolean; + label?: string; } export const Template = ({ 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