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/cuddly-drinks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sl-design-system/virtual-list': patch
---

This is a new package that provides a virtual scrolling solution for rendering large lists efficiently. It includes a web component `<sl-virtual-list>` and a `VirtualizerController` for integration with LitElement.

It is based on `@tanstack/virtual-core` and offers customizable item rendering, configurable item sizing, and gap settings.
9 changes: 9 additions & 0 deletions .changeset/nasty-zoos-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@sl-design-system/tree': minor
---

Various improvements:
- Replace `@tanstack/lit-virtual` with `@sl-design-system/virtual-list`
- Implement sorting of tree nodes
- Fix `TreeDataSource` bugs in `toggleDescendants()` and `expandAll()` methods
- Better documentation

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@
"@changesets/cli": "^2.29.7",
"@changesets/get-github-info": "^0.6.0",
"@custom-elements-manifest/analyzer": "^0.10.10",
"@faker-js/faker": "^10.1.0",
"@lit/localize-tools": "^0.8.0",
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-docs": "^9.1.10",
Expand Down
5 changes: 2 additions & 3 deletions packages/components/tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@
"@sl-design-system/icon": "^1.3.1",
"@sl-design-system/shared": "^0.9.1",
"@sl-design-system/skeleton": "^1.0.1",
"@sl-design-system/spinner": "^2.0.1"
"@sl-design-system/spinner": "^2.0.1",
"@sl-design-system/virtual-list": "^0.0.1"
},
"devDependencies": {
"@open-wc/scoped-elements": "^3.0.6",
"@tanstack/lit-virtual": "patch:@tanstack/lit-virtual@npm%3A3.13.12#~/.yarn/patches/@tanstack-lit-virtual-npm-3.13.12-56dd05e0b4.patch",
"lit": "^3.3.1"
},
"peerDependencies": {
"@open-wc/scoped-elements": "^3.0.6",
"@tanstack/lit-virtual": "^3.13.12",
"lit": "^3.1.4"
}
}
200 changes: 198 additions & 2 deletions packages/components/tree/src/flat-tree-data-source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,163 @@ describe('FlatTreeDataSource', () => {
getId: ({ id }) => id,
getLabel: ({ name }) => name,
getLevel: ({ level }) => level,
isExpandable: ({ expandable }) => expandable
isExpandable: ({ expandable }) => expandable,
isExpanded: () => true
}
);
ds.update();
});

it('should not support multiple selection', () => {
expect(ds.multiple).not.to.be.true;
});

it('should have the correct size', () => {
expect(ds.size).to.equal(2);
});

it('should have the correct number of items', () => {
expect(ds.items).to.have.length(5);
});

it('should have the correct labels', () => {
const labels = ds.items.map(n => n.label);

expect(labels).to.deep.equal(['1', '2', '3', '4', '5']);
});

it('should have the correct expandable state', () => {
const expandables = ds.items.map(n => n.expandable);

expect(expandables).to.deep.equal([true, true, false, false, false]);
});

it('should have the correct expanded state', () => {
const expanded = ds.items.map(n => n.expanded);

expect(expanded).to.deep.equal([true, true, false, false, false]);
});

it('should have the correct levels', () => {
const levels = ds.items.map(n => n.level);

expect(levels).to.deep.equal([0, 1, 2, 1, 0]);
});
});

describe('expanded state', () => {
beforeEach(() => {
ds = new FlatTreeDataSource(
[
{ id: 1, name: '1', level: 0, expandable: true, expanded: true },
{ id: 2, name: '2', level: 1, expandable: true, expanded: false },
{ id: 3, name: '3', level: 2, expandable: false },
{ id: 4, name: '4', level: 1, expandable: false },
{ id: 5, name: '5', level: 0, expandable: false }
],
{
getId: ({ id }) => id,
getLabel: ({ name }) => name,
getLevel: ({ level }) => level,
isExpandable: ({ expandable }) => expandable,
isExpanded: ({ expanded }) => expanded ?? false
}
);
ds.update();
});

it('should expand a node when calling expand()', () => {
const node = ds.items[1];

expect(node.expanded).to.be.false;
expect(ds.items).to.have.length(4);

ds.expand(node);

expect(node.expanded).to.be.true;
expect(ds.items).to.have.length(5);
});

it('should collapse a node when calling collapse()', () => {
const node = ds.items[0];

expect(node.expanded).to.be.true;
expect(ds.items).to.have.length(4);

ds.collapse(node);

expect(node.expanded).to.be.false;
expect(ds.items).to.have.length(2);
});

it('should toggle the expanded state of a node when calling toggle()', () => {
const node = ds.items[1];

expect(node.expanded).to.be.false;
expect(ds.items).to.have.length(4);

ds.toggle(node);

expect(node.expanded).to.be.true;
expect(ds.items).to.have.length(5);

ds.toggle(node);

expect(node.expanded).to.be.false;
expect(ds.items).to.have.length(4);
});

it('should expand all descendants when calling expandDescendants()', () => {
const node = ds.items[0];

expect(ds.items).to.have.length(4);

ds.expandDescendants(node);

expect(ds.items.filter(i => i.expandable).every(i => i.expanded)).to.be.true;
expect(ds.items).to.have.length(5);
});

it('should collapse all descendants when calling collapseDescendants()', () => {
const node = ds.items[0];

expect(ds.items).to.have.length(4);

ds.collapseDescendants(node);

expect(ds.items.every(i => !i.expanded)).to.be.true;
expect(ds.items).to.have.length(2);
});

it('should toggle all descendants when calling toggleDescendants()', () => {
const node = ds.items[0];

expect(ds.items).to.have.length(4);

ds.toggleDescendants(node);

expect(ds.items.map(i => i.expanded)).to.deep.equal([false, false]);
expect(ds.items).to.have.length(2);
});

it('should expand all nodes when calling expandAll()', () => {
ds.expandAll();

expect(ds.items.filter(i => i.expandable).every(i => i.expanded)).to.be.true;
expect(ds.items).to.have.length(5);
});

it('should collapse all nodes when calling collapseAll()', () => {
ds.collapseAll();

expect(ds.items.filter(i => i.expandable).every(i => !i.expanded)).to.be.true;
expect(ds.items).to.have.length(2);
});
});

describe('level guides', () => {
beforeEach(() => {
/**
* Tree structure (do not show the root; use ASCII art for the indent guides):
* A (level 0, not last)
* ├─ A.1 (level 1, not last)
* └─ A.2 (level 1, last child)
Expand Down Expand Up @@ -84,4 +227,57 @@ describe('FlatTreeDataSource', () => {
]);
});
});

describe('sorting', () => {
beforeEach(() => {
ds = new FlatTreeDataSource(
[
{ id: 3, name: 'C', level: 0, expandable: true },
{ id: '3.1', name: 'Z', level: 1, expandable: false },
{ id: '3.2', name: 'Y', level: 1, expandable: false },
{ id: 1, name: 'A', level: 0, expandable: false },
{ id: 2, name: 'B', level: 0, expandable: false }
],
{
getId: ({ id }) => id,
getLabel: ({ name }) => name,
getLevel: ({ level }) => level,
isExpandable: ({ expandable }) => expandable,
isExpanded: () => true
}
);
ds.update();
});

it('should not have a sort by default', () => {
expect(ds.sort).to.be.undefined;
});

it('should have a sort after calling setSort', () => {
ds.setSort('name', 'asc');

expect(ds.sort).to.deep.equal({ by: 'name', direction: 'asc' });
});

it('should remove the sort after calling removeSort', () => {
ds.setSort('name', 'asc');
ds.removeSort();

expect(ds.sort).to.be.undefined;
});

it('should sort the nodes when sorting is applied', () => {
expect(ds.items.map(n => n.label)).to.deep.equal(['C', 'Z', 'Y', 'A', 'B']);

ds.setSort('name', 'asc');
ds.update();

expect(ds.items.map(n => n.label)).to.deep.equal(['A', 'B', 'C', 'Y', 'Z']);

ds.setSort('name', 'desc');
ds.update();

expect(ds.items.map(n => n.label)).to.deep.equal(['C', 'Z', 'Y', 'B', 'A']);
});
});
});
9 changes: 6 additions & 3 deletions packages/components/tree/src/flat-tree-data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from './tree-data-source.js';

export interface FlatTreeDataSourceMapping<T> extends TreeDataSourceMapping<T> {
/** Returns the level in the tree of the given item. */
getLevel(item: T): number;
}

Expand Down Expand Up @@ -135,14 +136,16 @@ export class FlatTreeDataSource<T = any> extends TreeDataSource<T> {
isSelected
} = this.#mapping;

const expandable = isExpandable(item);

const treeNode: TreeDataSourceNode<T> = {
id: getId(item),
childrenCount: getChildrenCount?.(item),
dataNode: item,
description: getAriaDescription?.(item),
expandable: isExpandable(item),
expanded: isExpanded?.(item) ?? false,
expandedIcon: getIcon?.(item, true),
expandable,
expanded: (expandable && isExpanded?.(item)) ?? false,
expandedIcon: expandable ? getIcon?.(item, true) : undefined,
icon: getIcon?.(item, false),
label: getLabel(item),
lastNodeInLevel,
Expand Down
Loading