diff --git a/.changeset/fix-action-button-label-slotchange.md b/.changeset/fix-action-button-label-slotchange.md new file mode 100644 index 00000000000..88060681fdd --- /dev/null +++ b/.changeset/fix-action-button-label-slotchange.md @@ -0,0 +1,7 @@ +--- +'@adobe/spectrum-wc': patch +--- + +**fix(action-button):** `` now tracks label content added or removed after the initial render. + +The default slot wasn't bound to `SlotTextController`'s `handleSlotChange`, so `hasLabel` only reflected content present at connect. Dynamically clearing or setting the label in a consumer app (e.g. a framework re-render) left the `swc-ActionButton--iconOnly` class and its spacing stale until an unrelated property change forced a re-render. diff --git a/2nd-gen/packages/swc/components/action-button/ActionButton.ts b/2nd-gen/packages/swc/components/action-button/ActionButton.ts index 35cb0c5f2b3..cf8c18820e8 100644 --- a/2nd-gen/packages/swc/components/action-button/ActionButton.ts +++ b/2nd-gen/packages/swc/components/action-button/ActionButton.ts @@ -184,7 +184,7 @@ export class ActionButton extends PendingMixin(ButtonBase) { > - + ${this.renderPendingState()} diff --git a/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts b/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts index 937a6ce4ed4..928cf2aca96 100644 --- a/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts +++ b/2nd-gen/packages/swc/components/action-button/test/action-button.test.ts @@ -614,6 +614,69 @@ export const PendingBehaviorTest: Story = { }, }; +export const DynamicLabelBehaviorTest: Story = { + render: () => html` + + + Edit + + `, + play: async ({ canvasElement, step }) => { + const button = await getComponent( + canvasElement, + 'swc-action-button' + ); + const internalButton = button.renderRoot.querySelector('button'); + const labelNode = [...button.childNodes].find( + (node) => node.textContent?.trim() === 'Edit' + ) as ChildNode; + + await step('starts icon+label, not icon-only', async () => { + expect( + internalButton?.classList.contains('swc-ActionButton--iconOnly'), + 'icon-only class is absent while a label is slotted' + ).toBe(false); + }); + + await step( + 'removing the label after first render adds icon-only', + async () => { + labelNode.remove(); + // slotchange dispatches asynchronously after the assignment. + await Promise.resolve(); + await button.updateComplete; + + expect( + internalButton?.classList.contains('swc-ActionButton--iconOnly'), + 'icon-only class is added once the label is removed' + ).toBe(true); + } + ); + + await step('re-adding a label removes icon-only', async () => { + button.append('Edit'); + await Promise.resolve(); + await button.updateComplete; + + expect( + internalButton?.classList.contains('swc-ActionButton--iconOnly'), + 'icon-only class is removed once a label is slotted again' + ).toBe(false); + }); + }, +}; +DynamicLabelBehaviorTest.storyName = 'Dynamic label behavior'; + export const IconOnlyAriaTest: Story = { render: () => html`