Skip to content

Commit

Permalink
feat(chips): add new soft-disabled attribute for focusable disabled…
Browse files Browse the repository at this point in the history
… chips

The `always-focusable` attribute is also now deprecated in favor of this new attribute. It'll be removed in some upcoming major version change.

PiperOrigin-RevId: 652472686
  • Loading branch information
zelliott authored and copybara-github committed Jul 15, 2024
1 parent aea7781 commit 750b886
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 26 deletions.
19 changes: 8 additions & 11 deletions chips/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ const assist: MaterialStoryInit<StoryKnobs> = {
>${GOOGLE_LOGO}</md-assist-chip
>
<md-assist-chip
label=${label || 'Disabled assist chip (focusable)'}
disabled
label=${label || 'Soft-disabled assist chip (focusable)'}
soft-disabled
always-focusable
?elevated=${elevated}></md-assist-chip>
</md-chip-set>
Expand Down Expand Up @@ -100,9 +100,8 @@ const filters: MaterialStoryInit<StoryKnobs> = {
?elevated=${elevated}
removable></md-filter-chip>
<md-filter-chip
label=${label || 'Disabled filter chip (focusable)'}
disabled
always-focusable
label=${label || 'Soft-disabled filter chip (focusable)'}
soft-disabled
?elevated=${elevated}
removable></md-filter-chip>
</md-chip-set>
Expand Down Expand Up @@ -144,9 +143,8 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
?disabled=${disabled}
remove-only></md-input-chip>
<md-input-chip
label=${label || 'Disabled input chip (focusable)'}
disabled
always-focusable></md-input-chip>
label=${label || 'Soft-disabled input chip (focusable)'}
soft-disabled></md-input-chip>
</md-chip-set>
`;
},
Expand Down Expand Up @@ -177,9 +175,8 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
>${GOOGLE_LOGO}</md-suggestion-chip
>
<md-suggestion-chip
label=${label || 'Disabled suggestion chip (focusable)'}
disabled
always-focusable
label=${label || 'Soft-disabled suggestion chip (focusable)'}
soft-disabled
?elevated=${elevated}></md-suggestion-chip>
</md-chip-set>
`;
Expand Down
4 changes: 2 additions & 2 deletions chips/internal/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
);
}

:host([disabled]) {
:host(:is([disabled], [soft-disabled])) {
pointer-events: none;
}

Expand Down Expand Up @@ -242,7 +242,7 @@
}

a,
button:not(:disabled) {
button:not(:disabled, [aria-disabled='true']) {
cursor: inherit;
}
}
5 changes: 3 additions & 2 deletions chips/internal/assist-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ export class AssistChip extends Chip {

protected override get rippleDisabled() {
// Link chips cannot be disabled
return !this.href && this.disabled;
return !this.href && (this.disabled || this.softDisabled);
}

protected override getContainerClasses() {
return {
...super.getContainerClasses(),
// Link chips cannot be disabled
disabled: !this.href && this.disabled,
disabled: !this.href && (this.disabled || this.softDisabled),
elevated: this.elevated,
link: !!this.href,
};
Expand All @@ -60,6 +60,7 @@ export class AssistChip extends Chip {
class="primary action"
id="button"
aria-label=${ariaLabel || nothing}
aria-disabled=${this.softDisabled || nothing}
?disabled=${this.disabled && !this.alwaysFocusable}
type="button"
>${content}</button
Expand Down
56 changes: 56 additions & 0 deletions chips/internal/assist-chip_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,61 @@ describe('Assist chip', () => {
.withContext('should not have any disabled styling or behavior')
.toBeNull();
});

it('should not allow link chips to be soft-disabled', async () => {
// Arrange
// Act
const chip = await setupTest();
chip.href = 'link';
chip.softDisabled = true;
await chip.updateComplete;

// Assert
expect(chip.renderRoot.querySelector('.disabled,:disabled'))
.withContext('should not have any disabled styling or behavior')
.toBeNull();
});
});

it('should use aria-disabled when soft-disabled', async () => {
// Arrange
// Act
const chip = await setupTest();
chip.softDisabled = true;
await chip.updateComplete;

// Assert
expect(chip.renderRoot.querySelector('button[aria-disabled="true"]'))
.withContext('should have aria-disabled="true"')
.not.toBeNull();
});

it('should be focusable when soft-disabled', async () => {
// Arrange
const chip = await setupTest();
chip.softDisabled = true;
await chip.updateComplete;

// Act
chip.focus();

// Assert
expect(document.activeElement)
.withContext('soft-disabled chip should be focused')
.toBe(chip);
});

it('should not be clickable when soft-disabled', async () => {
// Arrange
const clickListener = jasmine.createSpy('clickListener');
const chip = await setupTest();
chip.softDisabled = true;
chip.addEventListener('click', clickListener);

// Act
chip.click();

// Assert
expect(clickListener).not.toHaveBeenCalled();
});
});
15 changes: 15 additions & 0 deletions chips/internal/chip-set_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,21 @@ describe('Chip set', () => {
});
});

it('should NOT skip over soft-disabled chips', async () => {
const first = new TestAssistChip();
const second = new TestAssistChip();
second.softDisabled = true;
const third = new TestAssistChip();
const chipSet = await setupTest([first, second, third]);
await testNavigation({
chipSet,
ltrKey: 'ArrowRight',
rtlKey: 'ArrowLeft',
current: first,
next: second,
});
});

it('should focus trailing actions when navigating backwards', async () => {
const first = new TestInputChip();
const second = new TestInputChip();
Expand Down
37 changes: 34 additions & 3 deletions chips/internal/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import '../../focus/md-focus-ring.js';
import '../../ripple/ripple.js';

import {html, LitElement, PropertyValues, TemplateResult} from 'lit';
import {html, isServer, LitElement, PropertyValues, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';

Expand Down Expand Up @@ -35,12 +35,25 @@ export abstract class Chip extends chipBaseClass {
*/
@property({type: Boolean, reflect: true}) disabled = false;

/**
* Whether or not the chip is "soft-disabled" (disabled but still
* focusable).
*
* Use this when a chip needs increased visibility when disabled. See
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls
* for more guidance on when this is needed.
*/
@property({type: Boolean, attribute: 'soft-disabled', reflect: true})
softDisabled = false;

/**
* When true, allow disabled chips to be focused with arrow keys.
*
* Add this when a chip needs increased visibility when disabled. See
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls
* for more guidance on when this is needed.
*
* @deprecated Use `softDisabled` instead of `alwaysFocusable` + `disabled`.
*/
@property({type: Boolean, attribute: 'always-focusable'})
alwaysFocusable = false;
Expand Down Expand Up @@ -70,7 +83,14 @@ export abstract class Chip extends chipBaseClass {
* Some chip actions such as links cannot be disabled.
*/
protected get rippleDisabled() {
return this.disabled;
return this.disabled || this.softDisabled;
}

constructor() {
super();
if (!isServer) {
this.addEventListener('click', this.handleClick.bind(this));
}
}

override focus(options?: FocusOptions) {
Expand All @@ -97,7 +117,7 @@ export abstract class Chip extends chipBaseClass {

protected getContainerClasses(): ClassInfo {
return {
'disabled': this.disabled,
'disabled': this.disabled || this.softDisabled,
'has-icon': this.hasIcon,
};
}
Expand Down Expand Up @@ -139,4 +159,15 @@ export abstract class Chip extends chipBaseClass {
const slot = event.target as HTMLSlotElement;
this.hasIcon = slot.assignedElements({flatten: true}).length > 0;
}

private handleClick(event: Event) {
// If the chip is soft-disabled or disabled + always-focusable, we need to
// explicitly prevent the click from propagating to other event listeners
// as well as prevent the default action.
if (this.softDisabled || (this.disabled && this.alwaysFocusable)) {
event.stopImmediatePropagation();
event.preventDefault();
return;
}
}
}
9 changes: 5 additions & 4 deletions chips/internal/filter-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ export class FilterChip extends MultiActionChip {
id="button"
aria-label=${ariaLabel || nothing}
aria-pressed=${this.selected}
aria-disabled=${this.softDisabled || nothing}
?disabled=${this.disabled && !this.alwaysFocusable}
@click=${this.handleClick}
@click=${this.handleClickOnChild}
>${content}</button
>
`;
Expand All @@ -88,7 +89,7 @@ export class FilterChip extends MultiActionChip {
return renderRemoveButton({
focusListener,
ariaLabel: this.ariaLabelRemove,
disabled: this.disabled,
disabled: this.disabled || this.softDisabled,
});
}

Expand All @@ -103,8 +104,8 @@ export class FilterChip extends MultiActionChip {
return super.renderOutline();
}

private handleClick(event: MouseEvent) {
if (this.disabled) {
private handleClickOnChild(event: MouseEvent) {
if (this.disabled || this.softDisabled) {
return;
}

Expand Down
54 changes: 54 additions & 0 deletions chips/internal/filter-chip_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ describe('Filter chip', () => {
expect(chip.selected).withContext('chip.selected').toBeFalse();
});

it('should not select on click when soft-disabled', async () => {
// Arrange
const {chip, harness} = await setupTest();
chip.softDisabled = true;

// Act
await harness.clickWithMouse();

// Assert
expect(chip.selected).withContext('chip.selected').toBeFalse();
});

it('can prevent default', async () => {
const {chip, harness} = await setupTest();
const handler = jasmine.createSpy();
Expand Down Expand Up @@ -100,4 +112,46 @@ describe('Filter chip', () => {
.toBeTrue();
});
});

it('should be focusable when soft-disabled', async () => {
// Arrange
const {chip} = await setupTest();
chip.softDisabled = true;
await chip.updateComplete;

// Act
chip.focus();

// Assert
expect(document.activeElement)
.withContext('soft-disabled chip should be focused')
.toBe(chip);
});

it('should not be clickable when soft-disabled', async () => {
// Arrange
const clickListener = jasmine.createSpy('clickListener');
const {chip, harness} = await setupTest();
chip.softDisabled = true;
chip.addEventListener('click', clickListener);

// Act
await harness.clickWithMouse();

// Assert
expect(clickListener).not.toHaveBeenCalled();
});

it('should use aria-disabled when soft-disabled', async () => {
// Arrange
// Act
const {chip} = await setupTest();
chip.softDisabled = true;
await chip.updateComplete;

// Assert
expect(chip.renderRoot.querySelector('button[aria-disabled="true"]'))
.withContext('should have aria-disabled="true"')
.not.toBeNull();
});
});
7 changes: 4 additions & 3 deletions chips/internal/input-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class InputChip extends MultiActionChip {

protected override get rippleDisabled() {
// Link chips cannot be disabled
return !this.href && this.disabled;
return !this.href && (this.disabled || this.softDisabled);
}

protected get primaryAction() {
Expand All @@ -59,7 +59,7 @@ export class InputChip extends MultiActionChip {
...super.getContainerClasses(),
avatar: this.avatar,
// Link chips cannot be disabled
disabled: !this.href && this.disabled,
disabled: !this.href && (this.disabled || this.softDisabled),
link: !!this.href,
selected: this.selected,
'has-trailing': true,
Expand Down Expand Up @@ -94,6 +94,7 @@ export class InputChip extends MultiActionChip {
class="primary action"
id="button"
aria-label=${ariaLabel || nothing}
aria-disabled=${this.softDisabled || nothing}
?disabled=${this.disabled && !this.alwaysFocusable}
type="button"
>${content}</button
Expand All @@ -105,7 +106,7 @@ export class InputChip extends MultiActionChip {
return renderRemoveButton({
focusListener,
ariaLabel: this.ariaLabelRemove,
disabled: !this.href && this.disabled,
disabled: !this.href && (this.disabled || this.softDisabled),
tabbable: this.removeOnly,
});
}
Expand Down
Loading

0 comments on commit 750b886

Please sign in to comment.