Skip to content

Testing: button refactor #5587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/button/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@
"@spectrum-web-components/icons-ui": "1.7.0",
"@spectrum-web-components/progress-circle": "1.7.0",
"@spectrum-web-components/reactive-controllers": "1.7.0",
"@spectrum-web-components/shared": "1.7.0"
"@spectrum-web-components/shared": "1.7.0",
"@spectrum-web-components/tooltip": "1.7.0"
},
"types": "./src/index.d.ts",
"customElements": "custom-elements.json",
Expand Down
32 changes: 26 additions & 6 deletions packages/button/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export const VALID_VARIANTS = [
'negative',
'white',
'black',
];
export const VALID_STATIC_COLORS = ['white', 'black'];
] as const;

export const VALID_STATIC_COLORS = ['white', 'black'] as const;

export type ButtonTreatments = 'fill' | 'outline';

Expand All @@ -56,9 +57,8 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
@property({ type: String, attribute: 'pending-label' })
public pendingLabel = 'Pending';

// Use this property to set the button into a pending state
@property({ type: Boolean, reflect: true, attribute: true })
public pending = false;
@property({ type: Boolean, reflect: true })
public override pending = false;

public pendingStateController: PendingStateController<this>;

Expand All @@ -72,7 +72,7 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
}

public override click(): void {
if (this.pending) {
if (this.pending || this.disabled) {
return;
}
super.click();
Expand Down Expand Up @@ -191,11 +191,31 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
if (!this.hasAttribute('variant')) {
this.setAttribute('variant', this.variant);
}

// Set role="button" by default
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}

if (this.pending) {
this.pendingStateController.hostUpdated();
}
}

protected override updated(changes: PropertyValues<this>): void {
super.updated(changes);

// Update aria-disabled when disabled changes
if (changes.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled.toString());
}

// Update aria-busy when pending changes
if (changes.has('pending')) {
this.setAttribute('aria-busy', this.pending.toString());
}
}

protected override renderButton(): TemplateResult {
return html`
${this.buttonContent}
Expand Down
54 changes: 50 additions & 4 deletions packages/button/src/ButtonBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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 '@spectrum-web-components/tooltip/sp-tooltip.js';
import buttonStyles from './button-base.css.js';

/**
Expand All @@ -48,12 +49,33 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
@property({ type: String })
public type: 'button' | 'submit' | 'reset' = 'button';

/**
* The reason why the button is disabled. When set, this will override
* the native disabled attribute and use aria-disabled instead, keeping
* the button focusable for better accessibility.
*/
@property({ type: String, attribute: 'disabled-reason' })
public disabledReason?: string;

/**
* Whether the button is disabled. This can be either through
* the native disabled attribute or through disabledReason.
*/
public get isDisabled(): boolean {
return this.disabled || !!this.disabledReason;
}

@property({ type: Boolean, reflect: true })
public override disabled = false;

/**
* HTML anchor element that component clicks by proxy
*/
@query('.anchor')
private anchorElement!: HTMLAnchorElement;

private tooltipId = `tooltip-${Math.random().toString(36).substring(2)}`;

public override get focusElement(): HTMLElement {
return this;
}
Expand All @@ -76,6 +98,9 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
return content;
}

@property({ type: Boolean, reflect: true })
public pending = false;

constructor() {
super();
this.proxyFocus = this.proxyFocus.bind(this);
Expand All @@ -86,7 +111,7 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
}

private handleClickCapture(event: Event): void | boolean {
if (this.disabled) {
if (this.isDisabled) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
Expand Down Expand Up @@ -154,9 +179,24 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
}

protected override render(): TemplateResult {
return this.href && this.href.length > 0
? this.renderAnchor()
: this.renderButton();
return html`
${this.disabledReason
? html`
<sp-tooltip
self-managed
id=${this.tooltipId}
placement="top"
variant="info"
offset="6"
Comment on lines +188 to +190
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can be configured.

>
${this.disabledReason}
</sp-tooltip>
`
: ''}
${this.href && this.href.length > 0
? this.renderAnchor()
: this.renderButton()}
`;
}

protected handleKeydown(event: KeyboardEvent): void {
Expand Down Expand Up @@ -260,6 +300,12 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
// Set up focus delegation
this.anchorElement.addEventListener('focus', this.proxyFocus);
}

if (changed.has('pending')) {
// Update ARIA attributes for loading state
this.setAttribute('aria-busy', this.pending ? 'true' : 'false');
}

}
protected override update(changes: PropertyValues): void {
super.update(changes);
Expand Down
112 changes: 112 additions & 0 deletions packages/button/stories/button-disabled-tooltip.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { html, TemplateResult } from '@spectrum-web-components/base';
import { ifDefined } from '@spectrum-web-components/base/src/directives.js';
import '@spectrum-web-components/button/sp-button.js';
import '@spectrum-web-components/tooltip/sp-tooltip.js';

interface StoryArgs {
disabledReason?: string;
variant?: 'accent' | 'primary' | 'secondary' | 'negative';
treatment?: 'fill' | 'outline';
}

export default {
title: 'Button/Disabled with Tooltip',
component: 'sp-button',
argTypes: {
disabledReason: { control: 'text' },
variant: {
control: {
type: 'select',
options: ['accent', 'primary', 'secondary', 'negative'],
},
},
treatment: {
control: {
type: 'select',
options: ['fill', 'outline'],
},
},
},
args: {
disabledReason:
'This button is disabled because required fields are missing',
variant: 'primary',
treatment: 'fill',
},
parameters: {
actions: {
handles: ['click'],
},
},
};

export const Default = (args: StoryArgs): TemplateResult => {
return html`
<div style="display: flex; flex-direction: column; gap: 20px;">
<div style="display: flex; gap: 20px; align-items: center;">
<!-- Regular disabled button -->
<sp-button
disabled
variant=${ifDefined(args.variant)}
treatment=${ifDefined(args.treatment)}
>
Regular Disabled
</sp-button>

<!-- Disabled button with tooltip -->
<sp-button
disabled-reason=${ifDefined(args.disabledReason)}
variant=${ifDefined(args.variant)}
treatment=${ifDefined(args.treatment)}
>
Disabled with Tooltip
</sp-button>
</div>
<p>
<em>
Try tabbing to the buttons. The second button will show a
tooltip when focused.
</em>
</p>
</div>
`;
};

export const FocusExample = (): TemplateResult => {
return html`
<div style="display: flex; flex-direction: column; gap: 20px;">
<div style="display: flex; gap: 20px; align-items: center;">
<sp-button>Regular Button</sp-button>

<sp-button
disabled-reason="This button requires additional permissions"
>
Focusable Disabled
</sp-button>

<sp-button disabled>Regular Disabled</sp-button>
</div>
<p>
<em>Tab order demonstration:</em>
<br />
1. Regular Button (focusable)
<br />
2. Disabled with Tooltip (focusable, shows tooltip)
<br />
3. Regular Disabled (not focusable)
</p>
</div>
`;
};
4 changes: 2 additions & 2 deletions packages/button/test/button.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ describe('Button', () => {

await elementUpdated(el);

expect(el.hasAttribute('aria-disabled'), 'initially not').to.be
.false;
expect(el.getAttribute('aria-disabled')).to.equal('false');
expect(el.disabled).to.be.false;

el.disabled = true;
await elementUpdated(el);
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4892,6 +4892,7 @@ __metadata:
"@spectrum-web-components/progress-circle": "npm:1.7.0"
"@spectrum-web-components/reactive-controllers": "npm:1.7.0"
"@spectrum-web-components/shared": "npm:1.7.0"
"@spectrum-web-components/tooltip": "npm:1.7.0"
languageName: unknown
linkType: soft

Expand Down
Loading