This is an example for feature request to NG-ZORRO
Issue: NG-ZORRO/ng-zorro-antd#5288
Ref: https://github.com/LS-NotInUse/NzTreeKeyboardControl
Last active
May 26, 2020 06:32
-
-
Save LouisSung/03dd2b0bf1d89cfc0bd8e8f17a607fec to your computer and use it in GitHub Desktop.
Example for adding keyboard control for NzTree (NG-ZORRO) https://github.com/NG-ZORRO/ng-zorro-antd/issues/5288
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div [style.width.px]="300" [style.margin.px]="16"> | |
<nz-input-group> | |
<label><input type="text" nz-input placeholder="Search" [(ngModel)]="searchValue"></label> | |
</nz-input-group> | |
<nz-tree nzShowLine #aTree [nzData]="treeNodeOptions" [nzSearchValue]="searchValue" | |
(keydown)="onKeyDown($event.key)" (nzExpandChange)="treeFocus()" | |
(nzClick)="onTreeNodeClick($event)" (nzDblClick)="onTreeNodeDblClick($event)"> | |
</nz-tree> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {AfterViewInit, Component, ElementRef, OnDestroy, Renderer2, ViewChild} from '@angular/core'; | |
import {NzFormatEmitEvent, NzTreeComponent, NzTreeNode, NzTreeNodeBaseComponent} from 'ng-zorro-antd'; | |
import {staticNodeOptions} from './tree-node-options'; | |
@Component({ | |
selector: 'app-root', | |
templateUrl: './app.component.html' | |
}) | |
export class AppComponent implements AfterViewInit, OnDestroy { | |
@ViewChild('aTree') public treeComponent?: NzTreeComponent; | |
@ViewChild('aTree', {read: ElementRef}) public treeRef?: ElementRef<HTMLElement>; | |
public treeNodeOptions = staticNodeOptions; | |
public searchValue = ''; | |
private treeInputElement?: HTMLInputElement; | |
private focusedNode?: NzTreeNode; | |
private readonly boundEvents: Array<() => void> = []; | |
constructor(private readonly renderer: Renderer2) {} | |
// prettier-ignore | |
public ngAfterViewInit(): void { | |
this.focusedNode = this.treeComponent?.getTreeNodes()[0]; | |
this.treeInputElement = this.treeRef?.nativeElement.querySelector('input') as HTMLInputElement; | |
this.boundEvents.push(this.renderer.listen( | |
this.treeInputElement, 'focus', (event: FocusEvent) => { if (event.relatedTarget !== null) { this.treeFocus(); } } | |
)); | |
this.boundEvents.push(this.renderer.listen(this.treeInputElement, 'blur', () => { this.treeFocus(false); })); | |
} | |
public ngOnDestroy(): void { | |
// prettier-ignore | |
for (const unbind of this.boundEvents) { unbind(); } | |
} | |
// eslint-disable-next-line max-lines-per-function, complexity | |
public onKeyDown(key: string): void { | |
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape', '*', '+', '-'].includes(key)) { | |
return; | |
} | |
const currentFocusedNode = this.focusedNode as NzTreeNode; | |
const parentNode = currentFocusedNode.parentNode; | |
const sibling = (parentNode === null ? this.treeComponent?.getTreeNodes() : parentNode.children) as Array<NzTreeNode>; | |
const nodeIndex = sibling.findIndex((x: NzTreeNode) => x.key === currentFocusedNode.key); | |
let focusOrBlur = true; | |
currentFocusedNode.isSelected = false; | |
// prettier-ignore | |
switch (key) { | |
case 'ArrowUp': keyArrowUp.call(this); break; | |
case 'ArrowDown': keyArrowDown.call(this); break; | |
case 'ArrowLeft': keyArrowLeft.call(this); break; | |
case 'ArrowRight': keyArrowRight.call(this); break; | |
case 'Enter': keyEnter.call(this); break; | |
case 'Escape': focusOrBlur = false; break; | |
case '*': this.recursivelyNodeCollapseExpand(currentFocusedNode, true, false); break; | |
case '+': this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false); break; | |
case '-': this.recursivelyNodeCollapseExpand(currentFocusedNode); break; | |
default: break; | |
} | |
this.treeFocus(focusOrBlur, true); | |
function keyArrowUp(this: AppComponent): void { | |
// eslint-disable-next-line arrow-body-style | |
const recursivelyGetPreviousNode = (targetNode: NzTreeNode): NzTreeNode => { | |
return targetNode.isExpanded && targetNode.children.length > 0 | |
? recursivelyGetPreviousNode(targetNode.children[targetNode.children.length - 1]) | |
: targetNode; | |
}; | |
this.focusedNode = nodeIndex > 0 ? recursivelyGetPreviousNode(sibling[nodeIndex - 1]) : parentNode ?? this.focusedNode; | |
} | |
function keyArrowDown(this: AppComponent): void { | |
// eslint-disable-next-line complexity | |
const recursivelyGetNextNode = ( | |
targetNode: NzTreeNode, | |
rollbackToParent: boolean = false, | |
skipChildren: boolean = false | |
): NzTreeNode | null => { | |
const tmpParent = targetNode.parentNode; | |
const tmpSibling = tmpParent ? tmpParent.children : (this.treeComponent?.getTreeNodes() as Array<NzTreeNode>); | |
const tmpIndex = tmpSibling.findIndex((x: NzTreeNode) => x.key === targetNode.key); | |
if (rollbackToParent) { | |
return tmpIndex < tmpSibling.length - 1 ? targetNode : tmpParent ? recursivelyGetNextNode(tmpParent, true) : null; | |
} else if (targetNode.isExpanded && targetNode.children.length > 0 && !skipChildren) { | |
return targetNode.children[0]; | |
} else if (tmpIndex < tmpSibling.length - 1) { | |
return tmpSibling[tmpIndex + 1]; | |
} else if (tmpParent) { | |
const tmpRollback = recursivelyGetNextNode(tmpParent, true); | |
return tmpRollback ? recursivelyGetNextNode(tmpRollback, false, true) : null; | |
} else { | |
return null; | |
} | |
}; | |
this.focusedNode = recursivelyGetNextNode(currentFocusedNode) ?? this.focusedNode; | |
} | |
function keyArrowRight(this: AppComponent): void { | |
if (!currentFocusedNode.isExpanded && currentFocusedNode.children.length > 0) { | |
this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false); | |
} else { | |
keyArrowDown.call(this); | |
} | |
} | |
function keyArrowLeft(this: AppComponent): void { | |
if (currentFocusedNode.isExpanded) { | |
this.recursivelyNodeCollapseExpand(currentFocusedNode); | |
} else { | |
this.focusedNode = parentNode ? parentNode : this.treeComponent?.getTreeNodes()[0]; | |
} | |
} | |
function keyEnter(this: AppComponent): void { | |
if (currentFocusedNode.isLeaf) { | |
return; | |
} | |
if (currentFocusedNode.isExpanded) { | |
this.recursivelyNodeCollapseExpand(currentFocusedNode); | |
} else { | |
this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false); | |
} | |
} | |
} | |
public onTreeNodeClick(event: NzFormatEmitEvent): void { | |
const clickedNode = event.node as NzTreeNode; | |
if (this.focusedNode !== clickedNode) { | |
(this.focusedNode as NzTreeNode).isSelected = false; | |
this.focusedNode = clickedNode; | |
} | |
this.treeFocus(); | |
} | |
public onTreeNodeDblClick(event: NzFormatEmitEvent): void { | |
if (!event.node?.isLeaf && event.node) { | |
this.recursivelyNodeCollapseExpand(event.node, event.node?.isExpanded, event.node?.isExpanded); | |
} | |
} | |
public treeFocus(focusOrBlur: boolean = true, scrollIntoView: boolean = false): void { | |
if (focusOrBlur) { | |
this.treeInputElement?.focus(); | |
if (scrollIntoView) { | |
// scroll after expand or collapse is finished | |
setTimeout(() => { | |
(this.focusedNode?.component as TreeNodeComponent)?.elementRef.nativeElement | |
.querySelector('nz-tree-node-switcher') | |
?.scrollIntoView({block: 'nearest', inline: 'nearest'}); | |
}); | |
} | |
} else { | |
this.treeInputElement?.blur(); | |
} | |
(this.focusedNode as NzTreeNode).isSelected = focusOrBlur; | |
} | |
private recursivelyNodeCollapseExpand(treeNode: NzTreeNode, recursive: boolean = true, collapseOrExpand: boolean = true): void { | |
if (treeNode.isExpanded === collapseOrExpand || (!collapseOrExpand && recursive)) { | |
treeNode.isExpanded = !collapseOrExpand; | |
if (recursive) { | |
for (const childNode of treeNode.children) { | |
this.recursivelyNodeCollapseExpand(childNode, recursive, collapseOrExpand); | |
} | |
} | |
} | |
} | |
} | |
interface TreeNodeComponent extends NzTreeNodeBaseComponent { | |
elementRef: ElementRef<HTMLElement>; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const staticNodeOptions = [ | |
{ | |
title: 'AAA', | |
key: '/AAA', | |
children: [ | |
{ | |
title: '111', | |
key: '/AAA/111', | |
children: [ | |
{ | |
title: 'aaa', | |
key: '/AAA/111/aaa', | |
children: [ | |
{key: '/AAA/111/aaa§i', title: 'i', isLeaf: true}, | |
{key: '/AAA/111/aaa§ii', title: 'ii', isLeaf: true}, | |
{key: '/AAA/111/aaa§iii', title: 'iii', isLeaf: true} | |
], | |
isLeaf: false | |
}, | |
{ | |
title: 'bbb', | |
key: '/AAA/111/bbb', | |
children: [ | |
{ | |
title: 'i', | |
key: '/AAA/111/bbb/i', | |
children: [ | |
{key: '/AAA/111/bbb/i§!!!', title: '!!!', isLeaf: true}, | |
{key: '/AAA/111/bbb/i§@@@', title: '@@@', isLeaf: true}, | |
{key: '/AAA/111/bbb/i§###', title: '###', isLeaf: true} | |
], | |
isLeaf: false | |
}, | |
{key: '/AAA/111/bbb§iv', title: 'iv', preview: '70x70', isLeaf: true}, | |
{key: '/AAA/111/bbb§v', title: 'v', preview: '70x70', isLeaf: true} | |
], | |
isLeaf: false | |
} | |
], | |
isLeaf: false | |
}, | |
{title: '222', key: '/AAA/222', children: [], isLeaf: true}, | |
{title: '333', key: '/AAA/333', children: [], isLeaf: true}, | |
{title: '444', key: '/AAA/444', children: [], isLeaf: true}, | |
{ | |
title: '555', | |
key: '/AAA/555', | |
children: [ | |
{ | |
title: 'ccc', | |
key: '/AAA/555/ccc', | |
children: [{key: '/AAA/555/ccc§vi', title: 'vi', preview: '15x32', isLeaf: true}], | |
isLeaf: false | |
} | |
], | |
isLeaf: false | |
}, | |
{ | |
title: '666', | |
key: '/AAA/666', | |
children: [ | |
{ | |
title: 'ddd', | |
key: '/AAA/666/ddd', | |
children: [{key: '/AAA/666/ddd§vii', title: 'vii', preview: '20x30', isLeaf: true}], | |
isLeaf: false | |
} | |
], | |
isLeaf: false | |
}, | |
{title: '777', key: '/AAA/777', children: [], isLeaf: true}, | |
{title: '888', key: '/AAA/888', children: [], isLeaf: true}, | |
{title: '999', key: '/AAA/999', children: [], isLeaf: true}, | |
{title: '1111', key: '/AAA/1111', children: [], isLeaf: true}, | |
{title: '2222', key: '/AAA/2222', children: [], isLeaf: true}, | |
{title: '3333', key: '/AAA/3333', children: [], isLeaf: true}, | |
{title: '000', key: '/AAA/000', children: [], isLeaf: true} | |
], | |
isLeaf: false | |
}, | |
{ | |
title: 'BBB', | |
key: '/BBB', | |
children: [ | |
{ | |
title: '4444', | |
key: '/BBB/4444', | |
children: [ | |
{ | |
title: 'eee', | |
key: '/BBB/4444/eee', | |
children: [ | |
{ | |
key: '/BBB/4444/eee§viii', | |
title: 'viii', | |
preview: '30x30', | |
spec: {'Dummy Spec 1': 'Hello', 'Dummy Spec 2': 'Sysmaker'}, | |
isLeaf: true | |
} | |
], | |
isLeaf: false | |
} | |
], | |
isLeaf: false | |
}, | |
{title: '5555', key: '/BBB/5555', children: [], isLeaf: true}, | |
{title: '6666', key: '/BBB/6666', children: [], isLeaf: true}, | |
{title: '7777', key: '/BBB/7777', children: [], isLeaf: true}, | |
{title: '8888', key: '/BBB/8888', children: [], isLeaf: true}, | |
{title: '9999', key: '/BBB/9999', children: [], isLeaf: true}, | |
{ | |
title: '11111', | |
key: '/BBB/11111', | |
children: [ | |
{ | |
title: 'fff', | |
key: '/BBB/11111/fff', | |
children: [{key: '/BBB/11111/fff§ix', title: 'ix', preview: '16x20', isLeaf: true}], | |
isLeaf: false | |
} | |
], | |
isLeaf: false | |
}, | |
{title: '22222', key: '/BBB/22222', children: [], isLeaf: true}, | |
{title: '33333', key: '/BBB/33333', children: [], isLeaf: true}, | |
{title: '44444', key: '/BBB/44444', children: [], isLeaf: true}, | |
{title: '55555', key: '/BBB/55555', children: [], isLeaf: true}, | |
{title: '0000', key: '/BBB/0000', children: [], isLeaf: true} | |
], | |
isLeaf: false | |
}, | |
{ | |
title: 'CCC', | |
key: '/CCC', | |
children: [ | |
{key: '/CCC§66666', title: '66666', isLeaf: true}, | |
{key: '/CCC§77777', title: '77777', isLeaf: true} | |
], | |
isLeaf: false | |
} | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment