Skip to content

Commit

Permalink
feat(chips): add multi-action chip navigation
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 538573474
  • Loading branch information
asyncLiz authored and copybara-github committed Jun 7, 2023
1 parent 919a9d3 commit 2444734
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 18 deletions.
2 changes: 1 addition & 1 deletion chips/lib/assist-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AssistChip extends Chip {
};
}

protected override renderPrimaryAction() {
protected override renderAction() {
const {ariaLabel} = this as ARIAMixinStrict;
if (this.href) {
return html`
Expand Down
13 changes: 8 additions & 5 deletions chips/lib/chip-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import {Chip} from './chip.js';
export class ChipSet extends LitElement {
get chips() {
return this.childElements.filter(
(child => child instanceof Chip) as (child: HTMLElement) =>
child is Chip);
(child): child is MaybeMultiActionChip => child instanceof Chip);
}

@queryAssignedElements({flatten: true})
Expand Down Expand Up @@ -57,7 +56,7 @@ export class ChipSet extends LitElement {

if (isHome || isEnd) {
const index = isHome ? 0 : chips.length - 1;
chips[index].focus();
chips[index].focus({trailing: isEnd});
this.updateTabIndices();
return;
}
Expand All @@ -70,7 +69,7 @@ export class ChipSet extends LitElement {
// If there is not already a chip focused, select the first or last chip
// based on the direction we're traveling.
const nextChip = forwards ? chips[0] : chips[chips.length - 1];
nextChip.focus();
nextChip.focus({trailing: !forwards});
this.updateTabIndices();
return;
}
Expand Down Expand Up @@ -101,7 +100,7 @@ export class ChipSet extends LitElement {
continue;
}

nextChip.focus();
nextChip.focus({trailing: !forwards});
this.updateTabIndices();
break;
}
Expand All @@ -124,3 +123,7 @@ export class ChipSet extends LitElement {
}
}
}

interface MaybeMultiActionChip extends Chip {
focus(options?: FocusOptions&{trailing?: boolean}): void;
}
9 changes: 3 additions & 6 deletions chips/lib/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import '../../focus/focus-ring.js';
import '../../ripple/ripple.js';

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

Expand Down Expand Up @@ -49,8 +49,7 @@ export abstract class Chip extends LitElement {
<md-focus-ring for=${this.primaryId}></md-focus-ring>
<md-ripple for=${this.primaryId}
?disabled=${this.rippleDisabled}></md-ripple>
${this.renderPrimaryAction()}
${this.renderTrailingAction?.() || nothing}
${this.renderAction()}
</div>
`;
}
Expand All @@ -71,9 +70,7 @@ export abstract class Chip extends LitElement {
`;
}

protected abstract renderPrimaryAction(): TemplateResult;

protected renderTrailingAction?(): TemplateResult|typeof nothing;
protected abstract renderAction(): TemplateResult;

protected renderOutline() {
return html`<span class="outline"></span>`;
Expand Down
10 changes: 7 additions & 3 deletions chips/lib/filter-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
import '../../elevation/elevation.js';

import {html, nothing, PropertyValues, svg} from 'lit';
import {property} from 'lit/decorators.js';
import {property, query} from 'lit/decorators.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';

import {Chip} from './chip.js';
import {MultiActionChip} from './multi-action-chip.js';
import {renderRemoveButton} from './trailing-actions.js';

/**
* A filter chip component.
*/
export class FilterChip extends Chip {
export class FilterChip extends MultiActionChip {
@property({type: Boolean}) elevated = false;
@property({type: Boolean}) removable = false;
@property({type: Boolean}) selected = false;
Expand All @@ -26,6 +26,10 @@ export class FilterChip extends Chip {
return 'option';
}

@query('.primary.action') protected readonly primaryAction!: HTMLElement|null;
@query('.trailing.action')
protected readonly trailingAction!: HTMLElement|null;

constructor() {
super();
this.addEventListener('click', () => {
Expand Down
19 changes: 16 additions & 3 deletions chips/lib/input-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
*/

import {html, nothing} from 'lit';
import {property} from 'lit/decorators.js';
import {property, query} from 'lit/decorators.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';

import {Chip} from './chip.js';
import {MultiActionChip} from './multi-action-chip.js';
import {renderRemoveButton} from './trailing-actions.js';

/**
* An input chip component.
*/
export class InputChip extends Chip {
export class InputChip extends MultiActionChip {
@property({type: Boolean}) avatar = false;
@property() href = '';
@property() target: '_blank'|'_parent'|'_self'|'_top'|'' = '';
Expand All @@ -39,6 +39,19 @@ export class InputChip extends Chip {
return !this.href && this.disabled;
}

protected get primaryAction() {
// Don't use @query() since a remove-only input chip still has a span that
// has "primary action" classes.
if (this.removeOnly) {
return null;
}

return this.renderRoot.querySelector<HTMLElement>('.primary.action');
}

@query('.trailing.action')
protected readonly trailingAction!: HTMLElement|null;

protected override getContainerClasses() {
return {
...super.getContainerClasses(),
Expand Down
102 changes: 102 additions & 0 deletions chips/lib/multi-action-chip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {html, isServer, nothing, TemplateResult} from 'lit';

import {Chip} from './chip.js';

/**
* A chip component with multiple actions.
*/
export abstract class MultiActionChip extends Chip {
protected abstract readonly primaryAction: HTMLElement|null;
protected abstract readonly trailingAction: HTMLElement|null;

constructor() {
super();
if (!isServer) {
this.addEventListener('focusin', this.updateTabIndices.bind(this));
this.addEventListener('focusout', this.updateTabIndices.bind(this));
this.addEventListener('keydown', this.handleKeyDown.bind(this));
}
}

override focus(options?: FocusOptions&{trailing?: boolean}) {
if (options?.trailing && this.trailingAction) {
this.trailingAction.focus(options);
return;
}

super.focus(options);
}

protected override firstUpdated() {
this.updateTabIndices();
}

protected override renderAction() {
return html`
${this.renderPrimaryAction()}
${this.renderTrailingAction()}
`;
}

protected abstract renderPrimaryAction(): TemplateResult;

protected abstract renderTrailingAction(): TemplateResult|typeof nothing;

private handleKeyDown(event: KeyboardEvent) {
const isLeft = event.key === 'ArrowLeft';
const isRight = event.key === 'ArrowRight';
// Ignore non-navigation keys.
if (!isLeft && !isRight) {
return;
}

if (!this.primaryAction || !this.trailingAction) {
// Does not have multiple actions.
return;
}

// Check if moving forwards or backwards
const isRtl = getComputedStyle(this).direction === 'rtl';
const forwards = isRtl ? isLeft : isRight;
const isPrimaryFocused = this.primaryAction?.matches(':focus-within');
const isTrailingFocused = this.trailingAction?.matches(':focus-within');

if ((forwards && isTrailingFocused) || (!forwards && isPrimaryFocused)) {
// Moving outside of the chip, it will be handled by the chip set.
return;
}

// Prevent default interactions, such as scrolling.
event.preventDefault();
// Don't let the chip set handle this navigation event.
event.stopPropagation();
const actionToFocus = forwards ? this.trailingAction : this.primaryAction;
actionToFocus.focus();
this.updateTabIndices();
}

private updateTabIndices() {
const {primaryAction, trailingAction} = this;
if (!primaryAction || !trailingAction) {
// Does not have multiple actions.
primaryAction?.removeAttribute('tabindex');
trailingAction?.removeAttribute('tabindex');
return;
}

if (trailingAction.matches(':focus-within')) {
trailingAction.removeAttribute('tabindex');
primaryAction.tabIndex = -1;
return;
}

primaryAction.removeAttribute('tabindex');
trailingAction.tabIndex = -1;
}
}

0 comments on commit 2444734

Please sign in to comment.