From f3dc885a98416842a0d75af285ebc2a37afc07d9 Mon Sep 17 00:00:00 2001 From: Valentin Mladenov Date: Mon, 30 Sep 2024 18:41:04 +0300 Subject: [PATCH] fix(tree-view): add aria multiselectable attribute when needed (#1563) ## PR Checklist Please check if your PR fulfills the following requirements: - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) - [ ] If applicable, have a visual design approval ## PR Type What kind of change does this PR introduce? - [x] Bugfix - [ ] Feature - [ ] Code style update (formatting, local variables) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes - [ ] CI related changes - [ ] Documentation content changes - [ ] Other... Please describe: ## What is the current behavior? `clr-tree` throws ExpressionChangedAfterItHasBeenCheckedError when setting `aria-multiselectable`. Issue Number: #1562, CDE-2301 ## What is the new behavior? Aria multiselectable attribute is added when needed. ## Does this PR introduce a breaking change? - [ ] Yes - [x] No ## Other information (cherry picked from commit 71b005644dd9babc7d4ab2918bd3dd6f784af2a6) --- projects/angular/clarity.api.md | 2 +- .../angular/src/data/tree-view/tree.spec.ts | 2 +- projects/angular/src/data/tree-view/tree.ts | 26 ++++++++++++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/projects/angular/clarity.api.md b/projects/angular/clarity.api.md index af98cdd310..d299f4d950 100644 --- a/projects/angular/clarity.api.md +++ b/projects/angular/clarity.api.md @@ -4295,7 +4295,7 @@ export class ClrTooltipTrigger { // @public (undocumented) export class ClrTree implements AfterContentInit, OnDestroy { // Warning: (ae-forgotten-export) The symbol "TreeFocusManagerService" needs to be exported by the entry point index.d.ts - constructor(featuresService: TreeFeaturesService, focusManagerService: TreeFocusManagerService, { nativeElement }: ElementRef, renderer: Renderer2, ngZone: NgZone); + constructor(featuresService: TreeFeaturesService, focusManagerService: TreeFocusManagerService, renderer: Renderer2, el: ElementRef, ngZone: NgZone); // (undocumented) featuresService: TreeFeaturesService; // (undocumented) diff --git a/projects/angular/src/data/tree-view/tree.spec.ts b/projects/angular/src/data/tree-view/tree.spec.ts index 84c5b21e8e..f5addee71a 100644 --- a/projects/angular/src/data/tree-view/tree.spec.ts +++ b/projects/angular/src/data/tree-view/tree.spec.ts @@ -76,7 +76,7 @@ export default function (): void { }); it('adds the aria-multiselectable if tree is selectable and has children', function (this: Context) { - expect(this.clarityElement.getAttribute('aria-multiselectable')).toBe('false'); + expect(this.clarityElement.getAttribute('aria-multiselectable')).toBeNull(); this.getClarityProvider(TreeFeaturesService).selectable = true; this.hostComponent.hasChild = true; this.detectChanges(); diff --git a/projects/angular/src/data/tree-view/tree.ts b/projects/angular/src/data/tree-view/tree.ts index 57bdb57139..d4cb15488f 100644 --- a/projects/angular/src/data/tree-view/tree.ts +++ b/projects/angular/src/data/tree-view/tree.ts @@ -35,30 +35,30 @@ import { ClrTreeNode } from './tree-node'; host: { tabindex: '0', '[attr.role]': '"tree"', - '[attr.aria-multiselectable]': 'isMultiSelectable', }, }) export class ClrTree implements AfterContentInit, OnDestroy { @ContentChildren(ClrTreeNode) private rootNodes: QueryList>; private subscriptions: Subscription[] = []; + private _isMultiSelectable = false; constructor( public featuresService: TreeFeaturesService, private focusManagerService: TreeFocusManagerService, - { nativeElement }: ElementRef, - renderer: Renderer2, + private renderer: Renderer2, + private el: ElementRef, ngZone: NgZone ) { const subscription = ngZone.runOutsideAngular(() => - fromEvent(nativeElement, 'focusin').subscribe((event: FocusEvent) => { - if (event.target === nativeElement) { + fromEvent(el.nativeElement, 'focusin').subscribe((event: FocusEvent) => { + if (event.target === el.nativeElement) { // After discussing with the team, I've made it so that when the tree receives focus, the first visible node will be focused. // This will prevent from the page scrolling abruptly to the first selected node if it exist in a deeply nested tree. this.focusManagerService.focusFirstVisibleNode(); // when the first child gets focus, // tree should no longer have tabindex of 0. - renderer.removeAttribute(nativeElement, 'tabindex'); + renderer.removeAttribute(el.nativeElement, 'tabindex'); } }) ); @@ -72,13 +72,15 @@ export class ClrTree implements AfterContentInit, OnDestroy { } get isMultiSelectable() { - return this.featuresService.selectable && this.rootNodes.length > 0; + return this._isMultiSelectable; } ngAfterContentInit() { this.setRootNodes(); this.subscriptions.push( this.rootNodes.changes.subscribe(() => { + this.setMultiSelectable(); + this.setRootNodes(); }) ); @@ -88,6 +90,16 @@ export class ClrTree implements AfterContentInit, OnDestroy { this.subscriptions.forEach(sub => sub.unsubscribe()); } + private setMultiSelectable() { + if (this.featuresService.selectable && this.rootNodes.length > 0) { + this._isMultiSelectable = true; + this.renderer.setAttribute(this.el.nativeElement, 'aria-multiselectable', 'true'); + } else { + this._isMultiSelectable = false; + this.renderer.removeAttribute(this.el.nativeElement, 'aria-multiselectable'); + } + } + private setRootNodes(): void { // if node has no parent, it's a root node // for recursive tree, this.rootNodes registers also nested children