fix(tree): add initial sync (#1080)

This commit is contained in:
Alessandro
2022-12-17 17:26:42 +01:00
committed by GitHub
parent 59cd70ae6f
commit d8b7040a9e
2 changed files with 130 additions and 13 deletions

View File

@@ -655,4 +655,98 @@ describe('<sl-tree>', () => {
});
});
});
describe('Checkboxes synchronization', () => {
describe('when the tree gets initialized', () => {
describe('and a parent node is selected', () => {
it('should select all the nested children', async () => {
// Arrange
const tree = await fixture<SlTree>(html`
<sl-tree selection="multiple">
<sl-tree-item selected>
Parent Node
<sl-tree-item selected>Child Node 1</sl-tree-item>
<sl-tree-item>
Child Node 2
<sl-tree-item>Child Node 2 - 1</sl-tree-item>
<sl-tree-item>Child Node 2 - 2</sl-tree-item>
</sl-tree-item>
</sl-tree-item>
</sl-tree>
`);
const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));
// Act
await tree.updateComplete;
// Assert
treeItems.forEach(treeItem => {
expect(treeItem).to.have.attribute('selected');
});
});
});
describe('and a parent node is not selected', () => {
describe('and all the children are selected', () => {
it('should select the parent node', async () => {
// Arrange
const tree = await fixture<SlTree>(html`
<sl-tree selection="multiple">
<sl-tree-item>
Parent Node
<sl-tree-item selected>Child Node 1</sl-tree-item>
<sl-tree-item selected>
Child Node 2
<sl-tree-item>Child Node 2 - 1</sl-tree-item>
<sl-tree-item>Child Node 2 - 2</sl-tree-item>
</sl-tree-item>
</sl-tree-item>
</sl-tree>
`);
const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));
// Act
await tree.updateComplete;
// Assert
treeItems.forEach(treeItem => {
expect(treeItem).to.have.attribute('selected');
});
expect(treeItems[0].indeterminate).to.be.false;
});
});
describe('and some of the children are selected', () => {
it('should set the parent node to indeterminate state', async () => {
// Arrange
const tree = await fixture<SlTree>(html`
<sl-tree selection="multiple">
<sl-tree-item>
Parent Node
<sl-tree-item selected>Child Node 1</sl-tree-item>
<sl-tree-item>
Child Node 2
<sl-tree-item>Child Node 2 - 1</sl-tree-item>
<sl-tree-item>Child Node 2 - 2</sl-tree-item>
</sl-tree-item>
</sl-tree-item>
</sl-tree>
`);
const treeItems = Array.from<SlTreeItem>(tree.querySelectorAll('sl-tree-item'));
// Act
await tree.updateComplete;
// Assert
expect(treeItems[0]).not.to.have.attribute('selected');
expect(treeItems[0].indeterminate).to.be.true;
expect(treeItems[1]).to.have.attribute('selected');
expect(treeItems[2]).not.to.have.attribute('selected');
expect(treeItems[3]).not.to.have.attribute('selected');
expect(treeItems[4]).not.to.have.attribute('selected');
});
});
});
});
});
});

View File

@@ -8,31 +8,44 @@ import SlTreeItem from '../tree-item/tree-item';
import styles from './tree.styles';
import type { CSSResultGroup } from 'lit';
function syncCheckboxes(changedTreeItem: SlTreeItem) {
function syncCheckboxes(changedTreeItem: SlTreeItem, initialSync = false) {
function syncParentItem(treeItem: SlTreeItem) {
const children = treeItem.getChildrenItems({ includeDisabled: false });
if (children.length) {
const allChecked = children.every(item => item.selected);
const allUnchecked = children.every(item => !item.selected && !item.indeterminate);
treeItem.selected = allChecked;
treeItem.indeterminate = !allChecked && !allUnchecked;
}
}
function syncAncestors(treeItem: SlTreeItem) {
const parentItem: SlTreeItem | null = treeItem.parentElement as SlTreeItem;
if (SlTreeItem.isTreeItem(parentItem)) {
const children = parentItem.getChildrenItems({ includeDisabled: false });
const allChecked = !!children.length && children.every(item => item.selected);
const allUnchecked = children.every(item => !item.selected && !item.indeterminate);
parentItem.selected = allChecked;
parentItem.indeterminate = !allChecked && !allUnchecked;
syncParentItem(parentItem);
syncAncestors(parentItem);
}
}
function syncDescendants(treeItem: SlTreeItem) {
for (const childItem of treeItem.getChildrenItems()) {
childItem.selected = !childItem.disabled && treeItem.selected;
childItem.selected = initialSync
? treeItem.selected || childItem.selected
: !childItem.disabled && treeItem.selected;
syncDescendants(childItem);
}
if (initialSync) {
syncParentItem(treeItem);
}
}
syncAncestors(changedTreeItem);
syncDescendants(changedTreeItem);
syncAncestors(changedTreeItem);
}
/**
@@ -88,6 +101,7 @@ export default class SlTree extends ShoelaceElement {
this.addEventListener('sl-lazy-change', this.handleSlotChange);
await this.updateComplete;
this.mutationObserver = new MutationObserver(this.handleTreeChanged);
this.mutationObserver.observe(this, { childList: true, subtree: true });
}
@@ -155,13 +169,22 @@ export default class SlTree extends ShoelaceElement {
};
@watch('selection')
handleSelectionChange() {
async handleSelectionChange() {
const isSelectionMultiple = this.selection === 'multiple';
const items = this.getAllTreeItems();
this.setAttribute('aria-multiselectable', this.selection === 'multiple' ? 'true' : 'false');
this.setAttribute('aria-multiselectable', isSelectionMultiple ? 'true' : 'false');
for (const item of items) {
item.selectable = this.selection === 'multiple';
item.selectable = isSelectionMultiple;
}
if (isSelectionMultiple) {
await this.updateComplete;
[...this.querySelectorAll(':scope > sl-tree-item')].forEach((treeItem: SlTreeItem) =>
syncCheckboxes(treeItem, true)
);
}
}