mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-02 17:53:30 +00:00
[ACS-11325] Add keyboard navigation and a11y support for adf-tree component (#11973)
* [ACS-11325] Add keyboard navigation and a11y support for adf-tree component * [ACS-11325] clean up comments * [ACS-11325] cr fixes * [ACS-11325] cr fixes * [ACS-11325] cr fix * [ACS-11325] sonar issue * [ACS-11325] fix lint
This commit is contained in:
@@ -627,6 +627,10 @@
|
||||
"LOAD-MORE-BUTTON": "Load more {{ name }}",
|
||||
"ACTIONS": {
|
||||
"TOOLTIP": "Open actions menu"
|
||||
},
|
||||
"ARIA": {
|
||||
"SELECTED": "{{ name }} selected",
|
||||
"DESELECTED": "{{ name }} deselected"
|
||||
}
|
||||
},
|
||||
"LIBRARY": {
|
||||
|
||||
@@ -42,18 +42,23 @@
|
||||
<mat-tree-node
|
||||
class="adf-tree-row"
|
||||
[attr.data-automation-id]="'node_' + node.id"
|
||||
[attr.aria-label]="node.nodeName"
|
||||
[attr.aria-selected]="treeNodesSelection.isSelected(node)"
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodePadding
|
||||
[adf-context-menu]="contextMenuOptions"
|
||||
[adf-context-menu-enabled]="!!contextMenuOptions"
|
||||
(contextmenu)="contextMenuSource = node"
|
||||
(click)="expandCollapseNode(node)"
|
||||
(keydown.enter)="expandCollapseNode(node); $event.stopPropagation()"
|
||||
(keydown.space)="onNodeSelected(node); $event.preventDefault(); $event.stopPropagation()"
|
||||
>
|
||||
<div class="adf-tree-expand-collapse-container">
|
||||
<button
|
||||
*ngIf="node.hasChildren"
|
||||
class="adf-tree-expand-collapse-button"
|
||||
(click)="expandCollapseNode(node)"
|
||||
mat-icon-button
|
||||
matTreeNodeToggle
|
||||
>
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
@@ -70,24 +75,26 @@
|
||||
[id]="node.id"
|
||||
[checked]="descendantsAllSelected(node)"
|
||||
[indeterminate]="descendantsPartiallySelected(node)"
|
||||
(change)="onNodeSelected(node)"
|
||||
(change)="onNodeSelected(node);"
|
||||
(click)="$event.stopPropagation()"
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
(keydown.space)="$event.stopPropagation()"
|
||||
data-automation-id="has-children-node-checkbox" />
|
||||
<ng-template #noChildrenNodeCheckbox>
|
||||
<mat-checkbox
|
||||
[id]="node.id"
|
||||
[checked]="treeNodesSelection.isSelected(node)"
|
||||
(change)="onNodeSelected(node)"
|
||||
(click)="$event.stopPropagation()"
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
(keydown.space)="$event.stopPropagation()"
|
||||
data-automation-id="no-children-node-checkbox" />
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<div class="adf-tree-cell">
|
||||
<span
|
||||
class="adf-tree-cell-value"
|
||||
[class.adf-tree-clickable-cell-value]="node.hasChildren"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
(keyup.enter)="expandCollapseNode(node)"
|
||||
(click)="expandCollapseNode(node)">
|
||||
[class.adf-tree-clickable-cell-value]="node.hasChildren">
|
||||
{{ node.nodeName }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -95,7 +102,10 @@
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
[attr.aria-label]="'ADF-TREE.ACTIONS.TOOLTIP' | translate"
|
||||
[attr.id]="'action_menu_right_' + node.id">
|
||||
[attr.id]="'action_menu_right_' + node.id"
|
||||
(click)="$event.stopPropagation()"
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
(keydown.space)="$event.stopPropagation()">
|
||||
<mat-icon adf-icon="more_vert" />
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
|
||||
@@ -43,7 +43,6 @@ $tree-header-font-size: 12px !default;
|
||||
&:focus {
|
||||
background-color: var(--mat-sys-surface-container);
|
||||
outline-offset: -1px;
|
||||
outline: 1px solid var(--mat-sys-secondary);
|
||||
}
|
||||
|
||||
.adf-tree-expand-collapse-button,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TreeComponent } from './tree.component';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ContextMenuDirective, UnitTestingUtils, UserPreferencesService } from '@alfresco/adf-core';
|
||||
import { TreeNode, TreeNodeType } from '../models/tree-node.interface';
|
||||
import { TreeResponse } from '../models/tree-response.interface';
|
||||
import { singleNode, treeNodesChildrenMockExpanded, treeNodesMock, treeNodesMockExpanded, treeNodesNoChildrenMock } from '../mock/tree-node.mock';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TreeService } from '../services/tree.service';
|
||||
@@ -29,6 +30,7 @@ import { DebugElement } from '@angular/core';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('TreeComponent', () => {
|
||||
let fixture: ComponentFixture<TreeComponent<TreeNode>>;
|
||||
@@ -40,7 +42,8 @@ describe('TreeComponent', () => {
|
||||
const composeNodeSelector = (nodeId: string) => `[data-automation-id="node_${nodeId}"]`;
|
||||
const getNode = (nodeId: string) => testingUtils.getByCSS(composeNodeSelector(nodeId));
|
||||
|
||||
const clickDisplayNameElement = (nodeId: string) => testingUtils.clickByCSS(`${composeNodeSelector(nodeId)} .adf-tree-cell-value`);
|
||||
const clickDisplayNameElement = (nodeId: string) =>
|
||||
testingUtils.getByCSS(`${composeNodeSelector(nodeId)} .adf-tree-cell-value`).nativeElement.click();
|
||||
|
||||
const getDisplayNameValue = (nodeId: string) => testingUtils.getInnerTextByCSS(`${composeNodeSelector(nodeId)} .adf-tree-cell-value`);
|
||||
|
||||
@@ -181,6 +184,7 @@ describe('TreeComponent', () => {
|
||||
component.refreshTree();
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
const collapseSpy = spyOn(component.treeService, 'collapseNode');
|
||||
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(true);
|
||||
clickExpandCollapseBtn(component.treeService.treeNodes[0].id);
|
||||
@@ -201,15 +205,18 @@ describe('TreeComponent', () => {
|
||||
component.refreshTree();
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
fixture.detectChanges();
|
||||
const collapseSpy = spyOn(component.treeService, 'expandNode');
|
||||
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(false);
|
||||
component.treeService.treeNodes = Array.from(treeNodesMock);
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
fixture.detectChanges();
|
||||
const expandSpy = spyOn(component.treeService, 'expandNode');
|
||||
clickExpandCollapseBtn(component.treeService.treeNodes[0].id);
|
||||
expect(collapseSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
|
||||
expect(expandSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
|
||||
});
|
||||
|
||||
it('should call collapseNode on TreeService when collapsing node by clicking at node label and node has children', () => {
|
||||
component.refreshTree();
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
spyOn(component.treeService, 'collapseNode');
|
||||
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(true);
|
||||
clickDisplayNameElement(component.treeService.treeNodes[0].id);
|
||||
@@ -219,8 +226,10 @@ describe('TreeComponent', () => {
|
||||
it('should call expandNode on TreeService when expanding node by clicking at node label and node has children', () => {
|
||||
component.refreshTree();
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeNodes = Array.from(treeNodesMock);
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
fixture.detectChanges();
|
||||
spyOn(component.treeService, 'expandNode');
|
||||
spyOn(component.treeService.treeControl, 'isExpanded').and.returnValue(false);
|
||||
clickDisplayNameElement(component.treeService.treeNodes[0].id);
|
||||
expect(component.treeService.expandNode).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
|
||||
});
|
||||
@@ -356,6 +365,86 @@ describe('TreeComponent', () => {
|
||||
component.loadMoreSubnodes(component.treeService.treeNodes.find((node: TreeNode) => node.nodeType === TreeNodeType.LoadMoreNode));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('announcement', () => {
|
||||
let translateSpy: jasmine.Spy<TranslateService['instant']>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
translateSpy = spyOn(TestBed.inject(TranslateService), 'instant');
|
||||
});
|
||||
|
||||
it('should use selected key when node is selected', () => {
|
||||
component.onNodeSelected(component.treeService.treeNodes[0]);
|
||||
expect(translateSpy).toHaveBeenCalledWith('ADF-TREE.ARIA.SELECTED', { name: 'testName1' });
|
||||
});
|
||||
|
||||
it('should use deselected key when node is deselected', () => {
|
||||
component.treeNodesSelection.select(component.treeService.treeNodes[0]);
|
||||
component.onNodeSelected(component.treeService.treeNodes[0]);
|
||||
expect(translateSpy).toHaveBeenCalledWith('ADF-TREE.ARIA.DESELECTED', { name: 'testName1' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expansionModel.changed subscription', () => {
|
||||
it('should load and expand node when expansionModel fires added event', () => {
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeNodes = Array.from(treeNodesMock);
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
const expandSpy = spyOn(component.treeService, 'expandNode');
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
expect(expandSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0], treeNodesMockExpanded);
|
||||
});
|
||||
|
||||
it('should collapse node when expansionModel fires removed event', () => {
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
const collapseSpy = spyOn(component.treeService, 'collapseNode');
|
||||
component.treeService.treeControl.collapse(component.treeService.treeNodes[0]);
|
||||
expect(collapseSpy).toHaveBeenCalledWith(component.treeService.treeNodes[0]);
|
||||
});
|
||||
|
||||
it('should not expand node when expansionModel fires added event and node is already loading', () => {
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeNodes[0].isLoading = true;
|
||||
const expandSpy = spyOn(component.treeService, 'expandNode');
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
expect(expandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not collapse node when expansionModel fires removed event and node is loading', () => {
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
component.treeService.treeNodes[0].isLoading = true;
|
||||
const collapseSpy = spyOn(component.treeService, 'collapseNode');
|
||||
component.treeService.treeControl.collapse(component.treeService.treeNodes[0]);
|
||||
expect(collapseSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not load children when node is expanded but children are already loaded', () => {
|
||||
fixture.detectChanges();
|
||||
const expandSpy = spyOn(component.treeService, 'expandNode');
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
expect(expandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset isLoading and collapse node when getSubNodes fails during expansion', () => {
|
||||
fixture.detectChanges();
|
||||
component.treeService.treeNodes = Array.from(treeNodesMock);
|
||||
component.treeService.treeNodes[0].isLoading = false;
|
||||
|
||||
const subject = new Subject<TreeResponse<TreeNode>>();
|
||||
spyOn(component.treeService, 'getSubNodes').and.returnValue(subject.asObservable());
|
||||
|
||||
component.treeService.treeControl.expand(component.treeService.treeNodes[0]);
|
||||
expect(component.treeService.treeNodes[0].isLoading).toBeTrue();
|
||||
|
||||
subject.error(new Error('error'));
|
||||
|
||||
expect(component.treeService.treeNodes[0].isLoading).toBeFalse();
|
||||
expect(component.treeService.treeControl.isExpanded(component.treeService.treeNodes[0])).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context menu', () => {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
ViewEncapsulation,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
import { BehaviorSubject, merge, Observable, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, merge, Observable, Subject, EMPTY } from 'rxjs';
|
||||
import { TreeNode, TreeNodeType } from '../models/tree-node.interface';
|
||||
import { TreeService } from '../services/tree.service';
|
||||
import { ContextMenuDirective, IconModule, PaginationModel, UserPreferencesService } from '@alfresco/adf-core';
|
||||
@@ -37,13 +38,15 @@ import { SelectionChange, SelectionModel } from '@angular/cdk/collections';
|
||||
import { TreeResponse } from '../models/tree-response.interface';
|
||||
import { MatCheckbox, MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { TreeContextMenuResult } from '../models/tree-context-menu-result.interface';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { takeUntil, catchError } from 'rxjs/operators';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { MatTreeModule, MatTreeNode } from '@angular/material/tree';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-tree',
|
||||
@@ -66,6 +69,9 @@ import { MatMenuModule } from '@angular/material/menu';
|
||||
export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
treeService = inject<TreeService<T>>(TreeService);
|
||||
private readonly userPreferenceService = inject(UserPreferencesService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly liveAnnouncer = inject(LiveAnnouncer);
|
||||
|
||||
/** TemplateRef to provide empty template when no nodes are loaded */
|
||||
@Input()
|
||||
@@ -107,6 +113,9 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
@ViewChildren(MatCheckbox)
|
||||
public nodeCheckboxes: QueryList<MatCheckbox>;
|
||||
|
||||
@ViewChildren(MatTreeNode)
|
||||
private readonly matTreeNodes: QueryList<MatTreeNode<T>>;
|
||||
|
||||
private readonly loadingRootSource = new BehaviorSubject<boolean>(false);
|
||||
private _contextMenuSource: T;
|
||||
private _contextMenuOptions: any[];
|
||||
@@ -152,9 +161,13 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.loadingRoot$ = this.loadingRootSource.asObservable();
|
||||
this.refreshTree(0, this.userPreferenceService.paginationSize);
|
||||
this.treeNodesSelection.changed.subscribe((selectionChange: SelectionChange<T>) => {
|
||||
this.treeNodesSelection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((selectionChange: SelectionChange<T>) => {
|
||||
this.onTreeSelectionChange(selectionChange);
|
||||
});
|
||||
this.treeService.treeControl.expansionModel.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((change: SelectionChange<T>) => {
|
||||
change.added.forEach((node: T) => this.handleNodeExpanded(node));
|
||||
change.removed.forEach((node: T) => this.handleNodeCollapsed(node));
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -207,6 +220,7 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
this.treeNodesSelection.deselect(...response.entries);
|
||||
this.paginationChanged.emit(response.pagination);
|
||||
this.loadingRootSource.next(false);
|
||||
setTimeout(() => this.matTreeNodes?.first?.makeFocusable());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,21 +231,7 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
*/
|
||||
public expandCollapseNode(node: T): void {
|
||||
if (node.hasChildren && !node.isLoading) {
|
||||
if (this.treeService.treeControl.isExpanded(node)) {
|
||||
this.treeService.collapseNode(node);
|
||||
} else {
|
||||
node.isLoading = true;
|
||||
this.treeService.getSubNodes(node.id, 0, this.userPreferenceService.paginationSize).subscribe((response: TreeResponse<T>) => {
|
||||
this.treeService.expandNode(node, response.entries);
|
||||
node.isLoading = false;
|
||||
if (this.treeNodesSelection.isSelected(node)) {
|
||||
//timeout used to update nodeCheckboxes query list after new nodes are added so they can be selected
|
||||
setTimeout(() => {
|
||||
this.treeNodesSelection.select(...response.entries);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.treeService.treeControl.toggle(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +265,9 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
* @param node selected node
|
||||
*/
|
||||
public onNodeSelected(node: T): void {
|
||||
if (!this.selectableNodes) {
|
||||
return;
|
||||
}
|
||||
this.treeNodesSelection.toggle(node);
|
||||
const descendants: T[] = this.treeService.treeControl.getDescendants(node).filter(this.isRegularNode);
|
||||
if (descendants.length > 0) {
|
||||
@@ -273,6 +276,13 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
: this.treeNodesSelection.deselect(...descendants);
|
||||
}
|
||||
this.checkParentsSelection(node);
|
||||
|
||||
this.liveAnnouncer.announce(
|
||||
this.translateService.instant(this.treeNodesSelection.isSelected(node) ? 'ADF-TREE.ARIA.SELECTED' : 'ADF-TREE.ARIA.DESELECTED', {
|
||||
name: node.nodeName
|
||||
}),
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,6 +311,42 @@ export class TreeComponent<T extends TreeNode> implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private handleNodeExpanded(node: T): void {
|
||||
if (!node.hasChildren || node.isLoading || this.treeService.getChildren(node).length > 0) {
|
||||
return;
|
||||
}
|
||||
node.isLoading = true;
|
||||
this.treeService
|
||||
.getSubNodes(node.id, 0, this.userPreferenceService.paginationSize)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
node.isLoading = false;
|
||||
this.treeService.treeControl.collapse(node);
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe((response: TreeResponse<T>) => {
|
||||
if (!this.treeService.treeControl.isExpanded(node)) {
|
||||
node.isLoading = false;
|
||||
return;
|
||||
}
|
||||
this.treeService.expandNode(node, response.entries);
|
||||
if (this.treeNodesSelection.isSelected(node)) {
|
||||
// timeout used to update nodeCheckboxes query list after new nodes are added so they can be selected
|
||||
setTimeout(() => {
|
||||
this.treeNodesSelection.select(...response.entries);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleNodeCollapsed(node: T): void {
|
||||
if (!node.isLoading) {
|
||||
this.treeService.collapseNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
private checkParentsSelection(node: T): void {
|
||||
let parent: T = this.treeService.getParentNode(node.parentId);
|
||||
while (parent) {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('TreeService', () => {
|
||||
it('should collapse node containing children', () => {
|
||||
const treeNodesMockExpandedCopy = Array.from(treeNodesMockExpanded);
|
||||
service.treeNodes = treeNodesMockExpandedCopy;
|
||||
service.treeControl.expand(treeNodesMockExpandedCopy[0]);
|
||||
const nodesSourceSpy = spyOn(service.treeNodesSource, 'next');
|
||||
const treeControlCollapseSpy = spyOn(service.treeControl, 'collapse');
|
||||
service.collapseNode(treeNodesMockExpandedCopy[0]);
|
||||
@@ -66,6 +67,15 @@ describe('TreeService', () => {
|
||||
expect(service.treeNodes.length).toEqual(treeNodesMock.length);
|
||||
});
|
||||
|
||||
it('should not call treeControl.collapse when node is not in expansionModel (re-entrant safety)', () => {
|
||||
const treeNodesMockExpandedCopy = Array.from(treeNodesMockExpanded);
|
||||
service.treeNodes = treeNodesMockExpandedCopy;
|
||||
const treeControlCollapseSpy = spyOn(service.treeControl, 'collapse');
|
||||
service.collapseNode(treeNodesMockExpandedCopy[0]);
|
||||
expect(treeControlCollapseSpy).not.toHaveBeenCalled();
|
||||
expect(service.treeNodes.length).toEqual(treeNodesMock.length);
|
||||
});
|
||||
|
||||
it('should collapse node with more levels', () => {
|
||||
service.treeNodes = Array.from(treeNodesChildrenMockExpanded);
|
||||
const nodesSourceSpy = spyOn(service.treeNodesSource, 'next');
|
||||
|
||||
@@ -70,7 +70,9 @@ export abstract class TreeService<T extends TreeNode> extends DataSource<T> {
|
||||
*/
|
||||
public collapseNode(nodeToCollapse: T): void {
|
||||
if (nodeToCollapse?.hasChildren) {
|
||||
this.treeControl.collapse(nodeToCollapse);
|
||||
if (this.treeControl.isExpanded(nodeToCollapse)) {
|
||||
this.treeControl.collapse(nodeToCollapse);
|
||||
}
|
||||
const children: T[] = this.treeNodes.filter((node: T) => nodeToCollapse.id === node.parentId);
|
||||
children.forEach((child: T) => {
|
||||
this.collapseInnerNode(child);
|
||||
|
||||
Reference in New Issue
Block a user