Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/gentle-peaches-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sl-design-system/grid': patch
---

Improve reliability of selection mode

When you add an `<sl-grid-selection-column>` to a grid, the selection column automatically enables multiple selection mode on the data source. However, if you then changed the `items` property, the new data source would not have selection enabled. This fixes both cases by having a `selects` property on grid that is kept in sync with the data source. The selection column also sets the `selects` property on the grid.
118 changes: 118 additions & 0 deletions packages/components/grid/src/grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,124 @@ describe('sl-grid', () => {
expect(toggleSpy).to.have.been.calledOnce;
expect(toggleSpy.firstCall.args[0]).to.have.property('data', el.items?.at(0));
});

it('should set the selects property on the new data source when items are updated', async () => {
const newItems = [
{ firstName: 'Alice', lastName: 'Johnson' },
{ firstName: 'Bob', lastName: 'Wilson' }
];

el.items = newItems;
await el.updateComplete;

expect(el.dataSource?.selects).to.equal('multiple');
});
});

describe('single select', () => {
beforeEach(async () => {
el = await fixture(html`
<sl-grid
.items=${[
{ firstName: 'John', lastName: 'Doe' },
{ firstName: 'Jane', lastName: 'Smith' },
{ firstName: 'Alice', lastName: 'Johnson' }
]}
selects="single"
row-action="select"
>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
</sl-grid>
`);

await waitForGridToRenderData(el);
});

it('should have the selects property set to "single"', () => {
expect(el.selects).to.equal('single');
expect(el.dataSource?.selects).to.equal('single');
});

it('should toggle the "selected" part of the row when clicking in the row', async () => {
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();
await new Promise(resolve => setTimeout(resolve));

const row = el.renderRoot.querySelector<HTMLTableRowElement>('tbody tr:first-of-type');
expect(row?.part.contains('selected')).to.be.true;
});

it('should allow only one row to be selected at a time', async () => {
// Select first row
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();
await new Promise(resolve => setTimeout(resolve));

let selectedRows = el.renderRoot.querySelectorAll<HTMLTableRowElement>('tbody tr[part~="selected"]');
expect(selectedRows).to.have.lengthOf(1);

// Select second row - should deselect first row
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:nth-of-type(2) td:last-of-type')?.click();
await new Promise(resolve => setTimeout(resolve));

selectedRows = el.renderRoot.querySelectorAll<HTMLTableRowElement>('tbody tr[part~="selected"]');
expect(selectedRows).to.have.lengthOf(1);

// Verify first row is no longer selected
const firstRow = el.renderRoot.querySelector<HTMLTableRowElement>('tbody tr:first-of-type');
expect(firstRow?.part.contains('selected')).to.be.false;

// Verify second row is selected
const secondRow = el.renderRoot.querySelector<HTMLTableRowElement>('tbody tr:nth-of-type(2)');
expect(secondRow?.part.contains('selected')).to.be.true;
});

it('should deselect a row when clicking it again', async () => {
// Select a row
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();
await new Promise(resolve => setTimeout(resolve));

let row = el.renderRoot.querySelector<HTMLTableRowElement>('tbody tr:first-of-type');
expect(row?.part.contains('selected')).to.be.true;

// Click again to deselect
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();
await new Promise(resolve => setTimeout(resolve));

row = el.renderRoot.querySelector<HTMLTableRowElement>('tbody tr:first-of-type');
expect(row?.part.contains('selected')).to.be.false;
});

it('should emit an sl-grid-selection-change event when the selection changes', () => {
const onSelectionChange = spy();

el.addEventListener('sl-grid-selection-change', onSelectionChange);

el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();
el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:nth-of-type(2) td:last-of-type')?.click();

expect(onSelectionChange).to.have.been.calledTwice;
});

it('should call toggle() on the data source when a row is selected', () => {
const toggleSpy = spy(el.dataSource!, 'toggle');

el.renderRoot.querySelector<HTMLTableCellElement>('tbody tr:first-of-type td:last-of-type')?.click();

expect(toggleSpy).to.have.been.calledOnce;
expect(toggleSpy.firstCall.args[0]).to.have.property('data', el.items?.at(0));
});

it('should set the selects property on the new data source when items are updated', async () => {
const newItems = [
{ firstName: 'Alice', lastName: 'Johnson' },
{ firstName: 'Bob', lastName: 'Wilson' }
];

el.items = newItems;
await el.updateComplete;

expect(el.dataSource?.selects).to.equal('single');
});
});

describe('row action activate', () => {
Expand Down
14 changes: 13 additions & 1 deletion packages/components/grid/src/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
/** @internal Emits when the selection in the grid changes. */
@event({ name: 'sl-grid-selection-change' }) selectionChangeEvent!: EventEmitter<SlSelectionChangeEvent<T>>;

/**
* The selection mode for the grid. If you are using a `ListDataSource`, you should
* set the selection mode on the data source instead of on the grid. If you are using the
* `items` property, then you need to set the selection mode on the grid itself.
* @default undefined
*/
@property() selects?: 'single' | 'multiple';

/** @internal Emits when the state in the grid has changed. */
@event({ name: 'sl-grid-state-change' }) stateChangeEvent!: EventEmitter<SlStateChangeEvent<T>>;

Expand Down Expand Up @@ -385,13 +393,17 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
}

if (changes.has('items')) {
this.dataSource = this.items ? new ArrayListDataSource(this.items) : undefined;
this.dataSource = this.items ? new ArrayListDataSource(this.items, { selects: this.selects }) : undefined;

if (this.dataSource) {
this.#updateDataSource();
}
}

if (changes.has('selects') && this.dataSource?.selects !== this.selects) {
this.dataSource!.selects = this.selects;
}

if (changes.has('scopedElements')) {
this.#addScopedElements(this.scopedElements);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/components/grid/src/selection-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class GridSelectionColumn<T = any> extends GridColumn<T> {
override willUpdate(changes: PropertyValues<this>): void {
super.willUpdate(changes);

if (changes.has('grid') && this.grid) {
this.grid.selects = 'multiple';
}

if (changes.has('grid') && this.grid?.dataSource) {
this.grid.dataSource.selects = 'multiple';
}
Expand Down