From 20e9e3c3206d0bb8fe53db7cc69c2374fb7052fd Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 17 Aug 2022 16:22:03 -0400 Subject: [PATCH] tree updates --- docs/components/tree.md | 16 +++--- docs/resources/changelog.md | 4 +- src/components/tree-item/tree-item.test.ts | 28 ---------- src/components/tree-item/tree-item.ts | 10 ---- src/components/tree/tree.test.ts | 10 ++-- src/components/tree/tree.ts | 61 +++++++++++++--------- 6 files changed, 51 insertions(+), 78 deletions(-) diff --git a/docs/components/tree.md b/docs/components/tree.md index 38bfef1d..2acb14a6 100644 --- a/docs/components/tree.md +++ b/docs/components/tree.md @@ -234,7 +234,7 @@ Use the `lazy` attribute on a tree item to indicate that the content is not yet If you want to disable this behavior after the first load, simply remove the `lazy` attribute and, on the next expand, the existing content will be shown instead. ```html preview - + Available Trees @@ -277,7 +277,7 @@ const App = () => { }; return ( - + Available Trees {childItems.map(item => ( @@ -291,12 +291,12 @@ const App = () => { ### Custom expanded/collapsed icons -Use the `expanded-icon` or `collapsed-icon` slots to change the expanded and collapsed tree element icons respectively. +Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. ```html preview - - - + + + Deciduous @@ -332,8 +332,8 @@ import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Deciduous diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index a05ca6cf..2b2bc37b 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -10,7 +10,9 @@ _During the beta period, these restrictions may be relaxed in the event of a mis ## Next -- Improved single selection in `` so the node expands/collapses when clicked +- Fixed a bug in `` where dynamically changing slotted items wouldn't update the tree properly +- Improved single selection in `` so nodes expand and collapse and receive selection when clicking on the label +- Renamed `expanded-icon` and `collapsed-icon` slots to `expand-icon` and `collapse-icon` in the experimental `` and `` components - Improved RTL support for `` - Refactored components to extend from `ShoelaceElement` to make `dir` and `lang` reactive properties in all components diff --git a/src/components/tree-item/tree-item.test.ts b/src/components/tree-item/tree-item.test.ts index 3f26d514..01649d6d 100644 --- a/src/components/tree-item/tree-item.test.ts +++ b/src/components/tree-item/tree-item.test.ts @@ -49,19 +49,6 @@ describe('', () => { describe('when the user clicks the expand button', () => { describe('and the item is collapsed', () => { - it('should expand the item', async () => { - // Arrange - const expandButton: HTMLElement = parentItem.shadowRoot!.querySelector('.tree-item__expand-button')!; - - // Act - expandButton.click(); - await parentItem.updateComplete; - - // Assert - expect(parentItem).to.have.attribute('expanded'); - expect(parentItem).to.have.attribute('aria-expanded', 'true'); - }); - it('should emit sl-expand and sl-after-expand events', async () => { // Arrange const expandSpy = sinon.spy(); @@ -82,21 +69,6 @@ describe('', () => { }); describe('and the item is expanded', () => { - it('should collapse the item', async () => { - // Arrange - const expandButton: HTMLElement = parentItem.shadowRoot!.querySelector('.tree-item__expand-button')!; - parentItem.expanded = true; - await parentItem.updateComplete; - - // Act - expandButton.click(); - await parentItem.updateComplete; - - // Assert - expect(parentItem).not.to.have.attribute('expanded'); - expect(parentItem).to.have.attribute('aria-expanded', 'false'); - }); - it('should emit sl-collapse and sl-after-collapse events', async () => { // Arrange const collapseSpy = sinon.spy(); diff --git a/src/components/tree-item/tree-item.ts b/src/components/tree-item/tree-item.ts index faf74a8f..472ad109 100644 --- a/src/components/tree-item/tree-item.ts +++ b/src/components/tree-item/tree-item.ts @@ -185,15 +185,6 @@ export default class SlTreeItem extends ShoelaceElement { return !!parent && isTreeItem(parent); } - handleToggleExpand(event: Event) { - event.preventDefault(); - event.stopImmediatePropagation(); - - if (!this.disabled) { - this.expanded = !this.expanded; - } - } - handleChildrenSlotChange() { this.loading = false; this.isLeaf = this.getChildrenItems().length === 0; @@ -238,7 +229,6 @@ export default class SlTreeItem extends ShoelaceElement { 'tree-item__expand-button--visible': showExpandButton })} aria-hidden="true" - @click="${this.handleToggleExpand}" > ${when(this.loading, () => html` `)} ${when( diff --git a/src/components/tree/tree.test.ts b/src/components/tree/tree.test.ts index 6ec58a29..8016fe60 100644 --- a/src/components/tree/tree.test.ts +++ b/src/components/tree/tree.test.ts @@ -58,8 +58,8 @@ describe('', () => { beforeEach(async () => { el = await fixture(html` -
-
+
+
Node 1 Node 2 @@ -76,8 +76,8 @@ describe('', () => { // Assert treeItems.forEach(treeItem => { - expect(treeItem.querySelector('div[slot="expanded-icon"]')).to.be.ok; - expect(treeItem.querySelector('div[slot="collapsed-icon"]')).to.be.ok; + expect(treeItem.querySelector('div[slot="expand-icon"]')).to.be.ok; + expect(treeItem.querySelector('div[slot="collapse-icon"]')).to.be.ok; }); }); }); @@ -437,7 +437,7 @@ describe('', () => { await el.updateComplete; // Assert - expect(node).not.to.have.attribute('selected'); + expect(node).to.have.attribute('selected'); expect(node).to.have.attribute('expanded'); }); }); diff --git a/src/components/tree/tree.ts b/src/components/tree/tree.ts index 50b7533a..745a4cec 100644 --- a/src/components/tree/tree.ts +++ b/src/components/tree/tree.ts @@ -40,11 +40,11 @@ function syncCheckboxes(changedTreeItem: SlTreeItem) { * @since 2.0 * @status experimental * - * @event {{ selection: this.selectedItems }} sl-selection-change - Emitted when an item gets selected or deselected + * @event {{ selection: TreeItem[] }} sl-selection-change - Emitted when an item gets selected or deselected * * @slot - The default slot. - * @slot expanded-icon - The icon to show when the tree item is expanded. - * @slot collapsed-icon - The icon to show when the tree item is collapsed. + * @slot expand-icon - The icon to show when the tree item is expanded. + * @slot collapse-icon - The icon to show when the tree item is collapsed. * * @csspart base - The component's internal wrapper. * @@ -58,9 +58,9 @@ function syncCheckboxes(changedTreeItem: SlTreeItem) { export default class SlTree extends ShoelaceElement { static styles = styles; - @query('slot') defaultSlot: HTMLSlotElement; - @query('slot[name=expanded-icon]') expandedIconSlot: HTMLSlotElement; - @query('slot[name=collapsed-icon]') collapsedIconSlot: HTMLSlotElement; + @query('slot:not([name])') defaultSlot: HTMLSlotElement; + @query('slot[name=expand-icon]') expandedIconSlot: HTMLSlotElement; + @query('slot[name=collapse-icon]') collapsedIconSlot: HTMLSlotElement; /** Specifies the selection behavior of the Tree */ @property() selection: 'single' | 'multiple' | 'leaf' = 'single'; @@ -69,34 +69,35 @@ export default class SlTree extends ShoelaceElement { // A collection of all the items in the tree, in the order they appear. The collection is live, meaning it is // automatically updated when the underlying document is changed. // - private treeItems: HTMLCollectionOf = this.getElementsByTagName('sl-tree-item'); + private treeItems: SlTreeItem[] = []; private lastFocusedItem: SlTreeItem; private readonly localize = new LocalizeController(this); private mutationObserver: MutationObserver; - connectedCallback(): void { + async connectedCallback() { super.connectedCallback(); this.setAttribute('role', 'tree'); this.setAttribute('tabindex', '0'); - this.mutationObserver = new MutationObserver(this.handleTreeChanged); - this.addEventListener('focusin', this.handleFocusIn); this.addEventListener('focusout', this.handleFocusOut); + + await this.updateComplete; + this.mutationObserver = new MutationObserver(this.handleTreeChanged); + this.mutationObserver.observe(this, { childList: true, subtree: true }); } - disconnectedCallback(): void { + disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver.disconnect(); - this.removeEventListener('focusin', this.handleFocusIn); this.removeEventListener('focusout', this.handleFocusOut); } // Generates a clone of the expand icon element to use for each tree item - private getExpandButtonIcon(status: 'expanded' | 'collapsed') { - const slot = status === 'expanded' ? this.expandedIconSlot : this.collapsedIconSlot; + private getExpandButtonIcon(status: 'expand' | 'collapse') { + const slot = status === 'expand' ? this.expandedIconSlot : this.collapsedIconSlot; const icon = slot.assignedElements({ flatten: true })[0] as HTMLElement; // Clone it, remove ids, and slot it @@ -116,9 +117,9 @@ export default class SlTree extends ShoelaceElement { private initTreeItem = (item: SlTreeItem) => { item.selectable = this.selection === 'multiple'; - ['expanded', 'collapsed'] + ['expand', 'collapse'] .filter(status => !!this.querySelector(`[slot="${status}-icon"]`)) - .forEach((status: 'expanded' | 'collapsed') => { + .forEach((status: 'expand' | 'collapse') => { const existingIcon = item.querySelector(`[slot="${status}-icon"]`); if (existingIcon === null) { @@ -133,12 +134,6 @@ export default class SlTree extends ShoelaceElement { }); }; - protected firstUpdated(): void { - [...this.treeItems].forEach(this.initTreeItem); - - this.mutationObserver.observe(this, { childList: true, subtree: true }); - } - handleTreeChanged = (mutations: MutationRecord[]) => { for (const mutation of mutations) { const addedNodes: SlTreeItem[] = [...mutation.addedNodes].filter(isTreeItem) as SlTreeItem[]; @@ -289,12 +284,26 @@ export default class SlTree extends ShoelaceElement { handleClick(event: Event) { const target = event.target as HTMLElement; const treeItem = target.closest('sl-tree-item')!; + const isExpandButton = event + .composedPath() + .some((el: HTMLElement) => el?.classList?.contains('tree-item__expand-button')); - if (!treeItem.disabled) { + if (!treeItem || treeItem.disabled) { + return; + } + + if (this.selection === 'multiple' && isExpandButton) { + treeItem.expanded = !treeItem.expanded; + } else { this.selectItem(treeItem); } } + handleDefaultSlotChange() { + this.treeItems = [...this.querySelectorAll('sl-tree-item')]; + [...this.treeItems].forEach(this.initTreeItem); + } + handleFocusOut = (event: FocusEvent) => { const relatedTarget = event.relatedTarget as HTMLElement; @@ -327,9 +336,9 @@ export default class SlTree extends ShoelaceElement { render() { return html`
- - - + + +
`; }