Skip to content

Commit

Permalink
refactor(chips): update to abstract the selection logic using controller
Browse files Browse the repository at this point in the history
  • Loading branch information
mimshins committed Jan 13, 2025
1 parent 421c4f3 commit 70556f7
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 151 deletions.
4 changes: 0 additions & 4 deletions packages/web-components/src/chip-group/chip-group.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ export default css`
display: none !important;
}
:host([full-width]) {
width: 100%;
}
:host {
display: inline-block;
}
Expand Down
95 changes: 9 additions & 86 deletions packages/web-components/src/chip-group/chip-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { DeselectEvent, SelectEvent } from "../chip";
import { Chip } from "../chip/chip";
import { SynchronizeRequestEvent } from "../chip/events";
import { getRenderRootSlot, logger } from "../utils";
import { Slots } from "./constants";
import { SelectChangeEvent } from "./events";
Expand Down Expand Up @@ -43,16 +42,6 @@ export class ChipGroup extends LitElement {
@property({ type: String })
public label = "";

/**
* Indicates if the chip should be full width.
*/
@property({ type: Boolean, attribute: "full-width" })
public fullWidth = false;

private _selectedValues: string[] = [];

private _initiallySynced = false;

private get _chips() {
const chipsSlot = getRenderRootSlot(this.renderRoot, Slots.DEFAULT);

Expand All @@ -70,7 +59,6 @@ export class ChipGroup extends LitElement {

this._handleChipSelection = this._handleChipSelection.bind(this);
this._handleChipDeselection = this._handleChipDeselection.bind(this);
this._synchronize = this._synchronize.bind(this);
}

public override connectedCallback() {
Expand All @@ -85,8 +73,6 @@ export class ChipGroup extends LitElement {
DeselectEvent.type,
this._handleChipDeselection as EventListener,
);

this.addEventListener(SynchronizeRequestEvent.type, this._synchronize);
}

public override disconnectedCallback(): void {
Expand All @@ -101,8 +87,6 @@ export class ChipGroup extends LitElement {
DeselectEvent.type,
this._handleChipDeselection as EventListener,
);

this.removeEventListener(SynchronizeRequestEvent.type, this._synchronize);
}

protected override willUpdate(changed: PropertyValues<this>) {
Expand All @@ -111,89 +95,28 @@ export class ChipGroup extends LitElement {
if (changed.has("cols")) {
this.style.setProperty("--chips-cols", String(this.cols));
}

if (this._initiallySynced) return;

const init = () => {
this._synchronize();

this._initiallySynced = true;
};

if (!this.hasUpdated) void this.updateComplete.then(init);
else init();
}

private _synchronize() {
const chips = this._chips;
const selectedChips = chips.filter(item => item.selected);

if (selectedChips.length === 0 && this._selectedValues.length !== 0) {
this._selectedValues = [];

return;
}

if (selectedChips.length >= 1) {
if (this.selectMode === "single") {
if (
this._selectedValues.length === 1 &&
selectedChips.length === 1 &&
this._selectedValues[0] === selectedChips[0]?.value
) {
return;
}

const selectedValue = this._selectedValues[0];

selectedChips.forEach(item => {
if (item.value === selectedValue) return;

item.selected = false;
});

this._selectedValues = selectedValue ? [selectedValue] : [];
} else {
this._selectedValues = selectedChips.map(chip => chip.value);
}
}
}

private _handleChipDeselection(event: DeselectEvent) {
const chip = event.target as Chip;
const value = chip.value;

const selectedValues = this._selectedValues.filter(v => v !== value);

if (this.selectionRequired && selectedValues.length === 0) {
chip.selected = true;

this._selectedValues = [value];

return;
}

this._selectedValues = selectedValues;
private _handleChipDeselection() {
const selectedValues = this._chips
.filter(chip => chip.selected)
.map(chip => chip.value);

this.dispatchEvent(new SelectChangeEvent({ values: selectedValues }));
}

private _handleChipSelection(event: SelectEvent) {
const chip = event.target as Chip;
const value = chip.value;

if (this.selectMode === "multiple") {
this._selectedValues = this._selectedValues.concat(value);
} else this._selectedValues = [value];
private _handleChipSelection() {
const selectedValues = this._chips
.filter(chip => chip.selected)
.map(chip => chip.value);

this.dispatchEvent(new SelectChangeEvent({ values: this._selectedValues }));
this.dispatchEvent(new SelectChangeEvent({ values: selectedValues }));
}

protected override render() {
const rootClasses = classMap({
root: true,
[this.orientation]: true,
"full-width": this.fullWidth,
});

if (!this.label) {
Expand Down
1 change: 0 additions & 1 deletion packages/web-components/src/chip-group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export * from "./events";
*
* @prop {'single' | 'multiple'} [select-mode='single'] - The select mode of the chip group.
* @prop {'horizontal' | 'vertical'} [orientation='horizontal'] - The orientation of the chip group.
* @prop {boolean} [full-width=false] - Indicates if the chip should be full width.
* @prop {string} [label=""] -
* Defines a string value that can be used to set a label
* for assistive technologies.
Expand Down
141 changes: 141 additions & 0 deletions packages/web-components/src/chip/Controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { type ReactiveController, type ReactiveControllerHost } from "lit";
import { KeyboardKeys } from "../internals";
import { waitAMicrotask } from "../utils";
import type { Chip } from "./chip";
import { DeselectEvent, SelectEvent } from "./events";

type Host = ReactiveControllerHost & Chip;

/*
* To use, elements should add the controller and call
* `controller.handleCheckedChange()` in a getter/setter. This must
* be synchronous to match native behavior.
*/
class ChipSelectionController implements ReactiveController {
private readonly _host: Host;

constructor(host: Host) {
this._host = host;

this.handleClick = this.handleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleSelectedChange = this.handleSelectedChange.bind(this);
}

private get _rootNode() {
const rootNode = this._host.getRootNode() as
| ShadowRoot
| Document
| HTMLElement
| null;

return rootNode;
}

private get _selectionProperties() {
const chipGroup = this._host.closest("tapsi-chip-group");

if (!chipGroup) return null;

return {
selectMode: chipGroup.selectMode,
selectionRequired: chipGroup.selectionRequired,
};
}

private get _elements(): [Host, ...Host[]] {
if (!this._rootNode || !this._host.isConnected) return [this._host];

return Array.from(
this._rootNode.querySelectorAll<Host>("tapsi-chip"),
) as unknown as [Host, ...Host[]];
}

private get _siblings() {
return this._elements.filter(element => element !== this._host);
}

public hostConnected() {
if (this._host.selected) this._applySelectionProperties();
}

/**
* Should be called whenever the host's `selected` property changes
* synchronously.
*/
public handleSelectedChange() {
this._applySelectionProperties();
}

private _applySelectionProperties() {
const properties = this._selectionProperties;

if (!properties) return;

const siblings = this._siblings;
const selectedSiblings = siblings.filter(chip => chip.selected);
const hostSelected = this._host.selected;

const { selectMode } = properties;
const isSingleSelect = selectMode === "single";

if (isSingleSelect && hostSelected && selectedSiblings.length !== 0) {
selectedSiblings.forEach(chip => {
chip.selected = false;
});
}
}

public async handleClick(event: MouseEvent) {
if (this._host.disabled) return;

// allow event to propagate to user code after a microtask.
await waitAMicrotask();

if (event.defaultPrevented) return;

const properties = this._selectionProperties;

if (!properties) return;

const selectedSiblings = this._siblings.filter(chip => chip.selected);

const { selectionRequired } = properties;

let targetEvent: SelectEvent | DeselectEvent;

const hostSelected = this._host.selected;
const newHostSelected = !hostSelected;

if (
selectionRequired &&
!newHostSelected &&
selectedSiblings.length === 0
) {
return;
}

if (hostSelected) targetEvent = new DeselectEvent();
else targetEvent = new SelectEvent();

this._host.selected = !hostSelected;

this._host.dispatchEvent(targetEvent);
}

public async handleKeyDown(event: KeyboardEvent) {
if (this._host.disabled) return;

// allow event to propagate to user code after a microtask.
await waitAMicrotask();

if (event.defaultPrevented) return;

if (event.key !== KeyboardKeys.ENTER) return;
if (!event.currentTarget) return;

(event.currentTarget as HTMLElement).click();
}
}

export default ChipSelectionController;
Loading

0 comments on commit 70556f7

Please sign in to comment.