Skip to content

Commit

Permalink
fix(swatch): allow Swatch Group to manage selection through multiple …
Browse files Browse the repository at this point in the history
…slots
  • Loading branch information
Westbrook committed Mar 15, 2024
1 parent a62f459 commit f333379
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 24 deletions.
83 changes: 61 additions & 22 deletions packages/swatch/src/SwatchGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
SpectrumElement,
TemplateResult,
} from '@spectrum-web-components/base';
import { property } from '@spectrum-web-components/base/src/decorators.js';
import {
property,
queryAssignedElements,
} from '@spectrum-web-components/base/src/decorators.js';
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/src/RovingTabindex.js';
import { MutationController } from '@lit-labs/observers/mutation-controller.js';

Expand Down Expand Up @@ -50,11 +53,28 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
@property({ reflect: true })
public border: SwatchBorder;

@property({ reflect: true })
public density: 'compact' | 'spacious' | undefined;

@property({ reflect: true })
public rounding: SwatchRounding;

@property({ type: Array })
public selected: string[] = [];
public get selected(): string[] {
return this._selected;
}

public set selected(selected: string[]) {
if (selected === this.selected) return;

const oldSelected = this.selected;
this._selected = selected;
this.requestUpdate('selected', oldSelected);
}

// Specifically surface `_selected` internally so that change can be made to this value internally
// without triggering the update lifecycle directly.
private _selected: string[] = [];

@property()
public selects: SwatchSelects;
Expand All @@ -64,8 +84,8 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
@property({ reflect: true })
public shape: SwatchShape;

@property({ reflect: true })
public density: 'compact' | 'spacious' | undefined;
@queryAssignedElements({ flatten: true })
public swatches!: Swatch[];

constructor() {
super();
Expand Down Expand Up @@ -95,7 +115,7 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
? firstSelectedIndex
: firstEnabledIndex;
},
elements: () => [...this.children] as Swatch[],
elements: () => this.swatches,
isFocusableElement: (el: Swatch) => !el.disabled,
});

Expand Down Expand Up @@ -131,24 +151,24 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
this.selectedSet.delete(target.value);
}
}
this.selected = [...this.selectedSet];
this._selected = [...this.selectedSet];
const applyDefault = this.dispatchEvent(
new Event('change', {
cancelable: true,
bubbles: true,
})
);
if (!applyDefault) {
this.selected = oldSelected;
this._selected = oldSelected;
event.preventDefault();
}
}

private manageChange = (): void => {
private manageChange = async (): Promise<void> => {
const presentSet = new Set();
this.selectedSet = new Set(this.selected);
const swatches = [...this.children] as Swatch[];
swatches.forEach((swatch) => {
await Promise.all(this.swatches.map((swatch) => swatch.updateComplete));
this.swatches.forEach((swatch) => {
presentSet.add(swatch.value);
if (swatch.selected) {
this.selectedSet.add(swatch.value);
Expand All @@ -159,7 +179,8 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
this.selectedSet.delete(value);
}
});
this.selected = [...this.selectedSet];
this._selected = [...this.selectedSet];
this.rovingTabindexController.clearElementCache();
};

private getPassthroughSwatchActions(
Expand Down Expand Up @@ -273,7 +294,7 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
];

// Create Swatch actions that build state to be applied later.
const nextSelected = new Set(this.selected);
let nextSelected = new Set(this.selected);
const currentValues = new Set();
if (changes.has('selected')) {
swatchActions.push((swatch) => {
Expand All @@ -289,19 +310,37 @@ export class SwatchGroup extends SizedMixin(SpectrumElement, {
});
}

// Do Swatch actions to each Swach in the collection.
this.rovingTabindexController.elements.forEach((swatch) => {
swatchActions.forEach((action) => {
action(swatch);
const doActions = (): void => {
nextSelected = new Set(this.selected);

// Do Swatch actions to each Swatch in the collection.
this.swatches.forEach((swatch) => {
swatchActions.forEach((action) => {
action(swatch);
});
});
});

// Apply state built in actions back to the Swatch Group
if (changes.has('selected')) {
this.selected = [...nextSelected].filter((selectedValue) =>
currentValues.has(selectedValue)
// Apply state built in actions back to the Swatch Group
if (changes.has('selected')) {
this._selected = [...nextSelected.values()].filter(
(selectedValue) => currentValues.has(selectedValue)
);
}
};

if (this.hasUpdated) {
// Do actions immediately when the element has already updated.
doActions();
} else {
// On first update wait for a `slotchange` event, which is not currently managed
// by the element lifecycle before allowing Swatch actions to be commited.
this.shadowRoot.addEventListener(
'slotchange',
() => {
requestAnimationFrame(doActions);
},
{ once: true }
);
this.rovingTabindexController.clearElementCache();
}
}
}
54 changes: 52 additions & 2 deletions packages/swatch/test/swatch-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ describe('Swatch Group', () => {
el.selected = [selectedChild.value];
await elementUpdated(el);
await nextFrame();
await nextFrame();

expect(selectedChild.selected).to.be.true;

Expand Down Expand Up @@ -320,6 +321,8 @@ describe('Swatch Group - DOM selected', () => {
);

await elementUpdated(el);
await nextFrame();
await nextFrame();

expect(consoleWarnStub.called).to.be.true;
const spyCall = consoleWarnStub.getCall(0);
Expand Down Expand Up @@ -354,7 +357,7 @@ describe('Swatch Group - DOM selected', () => {

expect(el.selected).to.deep.equal(['color-1', 'color-3']);
});
it('merges `selected` and selection from DOM', async () => {
it('merges `selected` and selection from DOM', async function () {
const el = await fixture<SwatchGroup>(html`
<sp-swatch-group selects="multiple" .selected=${['color-1']}>
<sp-swatch value="color-0" color="red"></sp-swatch>
Expand All @@ -365,10 +368,12 @@ describe('Swatch Group - DOM selected', () => {
`);

await elementUpdated(el);
await nextFrame();
await nextFrame();

expect(el.selected).to.deep.equal(['color-1', 'color-3']);
});
it('lazily accepts selection from DOM', async () => {
it('lazily accepts selection from DOM', async function () {
const el = await fixture<SwatchGroup>(html`
<sp-swatch-group selects="multiple">
<sp-swatch value="color-0" color="red"></sp-swatch>
Expand All @@ -379,12 +384,16 @@ describe('Swatch Group - DOM selected', () => {
`);

await elementUpdated(el);
await nextFrame();
await nextFrame();
const color1 = el.querySelector('[value="color-1"]') as Swatch;

expect(el.selected).to.deep.equal(['color-3']);

color1.selected = true;
await elementUpdated(el);
await nextFrame();
await nextFrame();

expect(el.selected).to.deep.equal(['color-3', 'color-1']);
});
Expand All @@ -403,3 +412,44 @@ describe('Swatch Group - DOM selected', () => {
expect(el.selected).to.deep.equal(['color-2']);
});
});

describe('Swatch Group - slotted', () => {
it('manages [selects="single"] selection through multiple slots', async () => {
const test = await fixture<HTMLDivElement>(
html`
<div>
<sp-swatch value="First">First</sp-swatch>
<sp-swatch value="Second">Second</sp-swatch>
<sp-swatch value="Third" selected>Third</sp-swatch>
</div>
`
);

const firstItem = test.querySelector('sp-swatch') as Swatch;
const thirdItem = test.querySelector('sp-swatch[selected]') as Swatch;

const shadowRoot = test.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<sp-swatch-group label="Selects Single Group" selects="single">
<slot></slot>
</sp-swatch-group>
`;

const el = shadowRoot.querySelector('sp-swatch-group') as SwatchGroup;
await elementUpdated(el);
// Await test slot change time.
await nextFrame();
await nextFrame();

expect(el.selected, '"Third" selected').to.deep.equal(['Third']);
expect(firstItem.selected).to.be.false;
expect(thirdItem.selected).to.be.true;

firstItem.click();
await elementUpdated(el);

expect(el.selected, '"First" selected').to.deep.equal(['First']);
expect(firstItem.selected).to.be.true;
expect(thirdItem.selected).to.be.false;
});
});

0 comments on commit f333379

Please sign in to comment.