Skip to content

Commit

Permalink
feat(design): add tree component (#1622)
Browse files Browse the repository at this point in the history
Co-authored-by: Elain Tsai <[email protected]>
  • Loading branch information
damienwebdev and xelaint committed Jul 26, 2023
1 parent a61dc02 commit f211e48
Show file tree
Hide file tree
Showing 46 changed files with 1,663 additions and 3 deletions.
2 changes: 1 addition & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "libs/design/src/test.ts",
"main": "libs/design/test.ts",
"codeCoverage": true,
"tsConfig": "libs/design/tsconfig.spec.json",
"karmaConfig": "libs/design/karma.conf.js",
Expand Down
2 changes: 2 additions & 0 deletions libs/design/scss/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
@use '../src/molecules/sidebar/sidebar/sidebar-theme' as sidebar;
@use '../src/molecules/sidebar/sidebar-viewport/sidebar-viewport-theme' as sidebar-viewport;
@use '../scss/state/skeleton/mixins' as skeleton;
@use '../tree/src/tree-theme' as tree;

//
// Generates the styles of a @daffodil/design theme.
Expand Down Expand Up @@ -79,4 +80,5 @@
@include paginator.daff-paginator-theme($theme);
@include sidebar.daff-sidebar-theme($theme);
@include sidebar-viewport.daff-sidebar-viewport-theme($theme);
@include tree.daff-tree-theme($theme);
}
File renamed without changes.
38 changes: 38 additions & 0 deletions libs/design/tree/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Tree

Trees are used to visualize hierarchial information. They are often used to display navigational structures like nested lists of links.

## Overview

The `DaffTreeComponent` renders a tree structure. Typically, this is a structure of `<a>` and `<button>` elements that allow users to either navigate to a page, or explore the tree to find an item inside the tree that they want to navigate to.

Instead of defining a recursive tree structure of components, which is often prohibitively slow when rendering large trees, the `DaffTreeComponent` renders a flattened tree, using padding to indicate the nesting level of the tree elements.

Generally, tree usage consists of taking existing tree data, converting it to the `DaffTreeData` format, setting the `tree` input on the `DaffTreeComponent`, and providing templates for the cases where the tree element has children or not.

## Features

The `DaffTreeComponent` controls the rendering of the structure of the tree and provides template slots so that you can control the ultimate UI rendered for each node.

Currently, we support two kind of templates `daffTreeItemWithChildrenTpl` and `daffTreeItemTpl`. These templates allow you to control the content of each tree node. In the case of `daffTreeItemWithChildrenTpl` a `click` handler will be automatically applied (along with an icon indicating the expanded state) to the template to allow users to automatically open and close the node.

```html
<ng-template #daffTreeItemWithChildrenTpl let-node>
<button daffTreeItem [node]="node">{{ node.title }} </button>
</ng-template>

<ng-template #daffTreeItemTpl let-node>
<a daffTreeItem [node]="node" [routerLink]="node.url">{{ node.title }}</a>
</ng-template>
```

## Usage

### Basic Tree

<design-land-example-viewer-container example="basic-tree">
</design-land-example-viewer-container>

## Accessibility

The `DaffTreeComponent` follows the specification for a [disclosure navigation menu](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/) instead of a [tree view](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
9 changes: 9 additions & 0 deletions libs/design/tree/examples/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/design/examples",
"deleteDestPath": false,
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../../src/scss"]
}
}
3 changes: 3 additions & 0 deletions libs/design/tree/examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@daffodil/design/tree/examples"
}
10 changes: 10 additions & 0 deletions libs/design/tree/examples/src/basic-tree/basic-tree.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<ul daff-tree [tree]="tree">
<ng-template #daffTreeItemWithChildrenTpl let-node>
<button daffTreeItem [node]="node">{{ node.title }} </button>
</ng-template>

<ng-template #daffTreeItemTpl let-node>
<a daffTreeItem [node]="node" [routerLink]="node.url">{{ node.title }}</a>
</ng-template>
</ul>

39 changes: 39 additions & 0 deletions libs/design/tree/examples/src/basic-tree/basic-tree.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';

import { DaffTreeData } from '@daffodil/design/tree';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'basic-tree',
templateUrl: './basic-tree.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BasicTreeComponent {
tree: DaffTreeData<unknown> = {
title: 'Root',
items: [
{
title: 'Example Children',
items: [
{ title: 'Example Child', url: '#', id: '', items: [], data: {}},
],
url: '#',
id: '',
data: {},
},
{
title: 'Example Link',
items: [],
url: '#',
id: '',
data: {},
},
],
url: '',
id: '',
data: {},
};
}
22 changes: 22 additions & 0 deletions libs/design/tree/examples/src/basic-tree/basic-tree.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';

import { DaffTreeModule } from '@daffodil/design/tree';

import { BasicTreeComponent } from './basic-tree.component';

@NgModule({
declarations: [
BasicTreeComponent,
],
exports: [
BasicTreeComponent,
],
imports: [
RouterModule,
DaffTreeModule,
FontAwesomeModule,
],
})
export class BasicTreeModule { }
1 change: 1 addition & 0 deletions libs/design/tree/examples/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
6 changes: 6 additions & 0 deletions libs/design/tree/examples/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { BasicTreeComponent } from './basic-tree/basic-tree.component';
export { BasicTreeModule } from './basic-tree/basic-tree.module';
export { BasicTreeComponent };
export const TREE_EXAMPLES = [
BasicTreeComponent,
];
9 changes: 9 additions & 0 deletions libs/design/tree/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/design/tree",
"deleteDestPath": false,
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../src/scss"]
}
}
3 changes: 3 additions & 0 deletions libs/design/tree/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@daffodil/design/tree"
}
1 change: 1 addition & 0 deletions libs/design/tree/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
3 changes: 3 additions & 0 deletions libs/design/tree/src/interfaces/recursive-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type RecursiveTreeKeyOfType<T> = keyof {
[P in keyof T as T[P] extends T[]? P: never]: T[]
};
13 changes: 13 additions & 0 deletions libs/design/tree/src/interfaces/tree-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* A basic tree type supporting supplemental data on a tree node.
*
* Tree elements are often slightly more than just basic titles and child items.
* There may be other important data that needs to be available at render time.
*/
export interface DaffTreeData<T> {
title: string;
url: string;
id: string;
items: DaffTreeData<T>[];
data: T;
}
12 changes: 12 additions & 0 deletions libs/design/tree/src/interfaces/tree-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DaffTreeData } from './tree-data';

/**
* A DaffTreeUi is the internal data structure used during tree rendering.
*
* This is an internal implementation detail type that.
*/
export interface DaffTreeUi<T> extends DaffTreeData<T> {
open: boolean;
items: DaffTreeUi<T>[];
parent: DaffTreeUi<T>;
}
6 changes: 6 additions & 0 deletions libs/design/tree/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { DaffTreeModule } from './tree.module';
export { DaffTreeComponent } from './tree/tree.component';
export { DaffTreeItemDirective } from './tree-item/tree-item.directive';
export { DaffTreeData } from './interfaces/tree-data';
export { DaffTreeUi } from './interfaces/tree-ui';
export { daffTransformTreeInPlace } from './utils/transform-in-place';
8 changes: 8 additions & 0 deletions libs/design/tree/src/tree-item/tree-item.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DaffTreeItemDirective } from './tree-item.directive';

describe('DaffTreeItemDirective', () => {
it('should create an instance', () => {
// const directive = new DaffTreeItemDirective();
// expect(directive).toBeTruthy();
});
});
161 changes: 161 additions & 0 deletions libs/design/tree/src/tree-item/tree-item.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { DOCUMENT } from '@angular/common';
import {
Directive,
HostBinding,
HostListener,
Inject,
Input,
} from '@angular/core';

import { DaffTreeNotifierService } from '../tree/tree-notifier.service';
import { DaffTreeFlatNode } from '../utils/flatten-tree';

/**
* The `DaffTreeItemDirective` allows you to demarcate the elements which are
* tree-children that interact with the parent tree.
*
* They can be used like:
*
* ```html
* <ul daff-tree [tree]="tree">
* <ng-template #daffTreeItemWithChildrenTpl let-node>
* <button daffTreeItem [node]="node">{{ node.title }} </button>
* </ng-template>
*
* <ng-template #daffTreeItemTpl let-node>
* <a daffTreeItem [node]="node" [routerLink]="node.url">{{ node.title }}</a>
* </ng-template>
* </ul>
* ```
*
* where `tree` is a {@link DaffTreeData} and `daff-tree` is a {@link DaffTreeComponent}.
*
*/
@Directive({
selector: '[daffTreeItem]',
})
export class DaffTreeItemDirective {

/**
* The css class of the daff-tree.
*
* @docs-private
*/
@HostBinding('class.daff-tree-item') class = true;

/**
* The css class of a DaffTreeItemDirective that has children.
*
* @docs-private
*/
@HostBinding('class.daff-tree-item__parent') classParent = false;

/**
* The html `id` of the tree item. This is derived from the {@link DaffTreeData}.
*
* @docs-private
*/
@HostBinding('attr.id') id;

/**
* Accessibility property, notifying users about whether
* or not the tree item is open.
*
* @docs-private
*/
@HostBinding('attr.aria-expanded') ariaExpanded: string;

/**
* A css variable indicating the depth of the tree.
* You can use this to style your templates if you want to
* use different designs at different depths.
*/
@HostBinding('style.--depth') depth: number;

/**
* The CSS class indicating whether or not the tree is `selected`.
*/
@HostBinding('class.selected') get selectedClass() {
return this.selected;
};

/**
* The CSS class indicating whether or not the tree is `open`.
*/
@HostBinding('class.open') openClass = false;

/**
* The {@link DaffTreeFlatNode} associated with this specific tree item.
*
* @docs-private
*/
private _node: DaffTreeFlatNode;

/**
* The {@link DaffTreeFlatNode} associated with this specific tree item.
*/
@Input()
get node() {
return this._node;
};
set node(val: DaffTreeFlatNode) {
this._node = val;
this.id = 'tree-' + this._node.id;
this.ariaExpanded = this._node._treeRef.open ? 'true' : 'false';
this.depth = this._node.level;
this.classParent = this._node.hasChildren;
this.openClass = this._node._treeRef.open;
}

/**
* Whether or not the tree item is the currently active item.
* Note that there is no requirement there there only be one active item at a time.
*/
@Input() selected = false;

constructor(
@Inject(DOCUMENT) private document: any,
private treeNotifier: DaffTreeNotifierService,
) {}

/**
* @docs-private
*/
@HostListener('keydown.escape')
onEscape() {
this.toggleParent(this.node);
}

/**
* @docs-private
*/
@HostListener('click')
onClick() {
if(this.node.hasChildren) {
this.toggleTree(this.node);
}
this.treeNotifier.notify();
}

/**
* Toggle the open state of the tree's parent.
*/
toggleParent(node: DaffTreeFlatNode) {
if(node._treeRef?.parent.parent === undefined) {
return;
}
node._treeRef.parent.open = !node._treeRef.parent.open;
(<Document>this.document).getElementById('tree-' + node._treeRef.parent.id).focus();
}

/**
* Toggle the open state of this specific subtree tree.
*/
toggleTree(node: DaffTreeFlatNode) {
if(node._treeRef.open === false) {
node._treeRef.open = true;
} else {
node._treeRef.open = false;
}
}
}
Loading

0 comments on commit f211e48

Please sign in to comment.