Skip to content

Commit

Permalink
fix(tree-view): add aria multiselectable attribute when needed (backp…
Browse files Browse the repository at this point in the history
…ort to 16.x) (#1572)

Backport 71b0056 from #1563. <br> ## 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

Co-authored-by: Valentin Mladenov <valentin.mladenov@broadcom.com>
  • Loading branch information
github-actions[bot] and valentin-mladenov authored Sep 30, 2024
1 parent 39701f3 commit 2c5aa4c
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 9 deletions.
2 changes: 1 addition & 1 deletion projects/angular/clarity.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4295,7 +4295,7 @@ export class ClrTooltipTrigger {
// @public (undocumented)
export class ClrTree<T> implements AfterContentInit, OnDestroy {
// Warning: (ae-forgotten-export) The symbol "TreeFocusManagerService" needs to be exported by the entry point index.d.ts
constructor(featuresService: TreeFeaturesService<T>, focusManagerService: TreeFocusManagerService<T>, { nativeElement }: ElementRef<HTMLElement>, renderer: Renderer2, ngZone: NgZone);
constructor(featuresService: TreeFeaturesService<T>, focusManagerService: TreeFocusManagerService<T>, renderer: Renderer2, el: ElementRef<HTMLElement>, ngZone: NgZone);
// (undocumented)
featuresService: TreeFeaturesService<T>;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion projects/angular/src/data/tree-view/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 19 additions & 7 deletions projects/angular/src/data/tree-view/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,30 @@ import { ClrTreeNode } from './tree-node';
host: {
tabindex: '0',
'[attr.role]': '"tree"',
'[attr.aria-multiselectable]': 'isMultiSelectable',
},
})
export class ClrTree<T> implements AfterContentInit, OnDestroy {
@ContentChildren(ClrTreeNode) private rootNodes: QueryList<ClrTreeNode<T>>;

private subscriptions: Subscription[] = [];
private _isMultiSelectable = false;

constructor(
public featuresService: TreeFeaturesService<T>,
private focusManagerService: TreeFocusManagerService<T>,
{ nativeElement }: ElementRef<HTMLElement>,
renderer: Renderer2,
private renderer: Renderer2,
private el: ElementRef<HTMLElement>,
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');
}
})
);
Expand All @@ -72,13 +72,15 @@ export class ClrTree<T> 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();
})
);
Expand All @@ -88,6 +90,16 @@ export class ClrTree<T> 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
Expand Down

0 comments on commit 2c5aa4c

Please sign in to comment.