Skip to content

Commit

Permalink
feat(design): add tree component
Browse files Browse the repository at this point in the history
Co-authored-by: Elain Tsai <xelaint@gmail.com>
  • Loading branch information
damienwebdev and xelaint committed Jun 6, 2023
1 parent 4b5cd50 commit d346f39
Show file tree
Hide file tree
Showing 31 changed files with 693 additions and 0 deletions.
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);
}
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"
}
24 changes: 24 additions & 0 deletions libs/design/tree/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Tree
Tree is a navigable data structure component that can be used to visualize hierarchial information.

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

Instead of defining a recursive tree structure, 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

`<ul daff-tree>` will automatically open the tree (upon initial tree render only) if the page url matches the url of the tree item. Importantly, if multiple tree items match, each tree will open. This implementation uses the `Location` service of Angular, not the `Router`, and matching is solely based upon path.

## Usage

### Basic Tree

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

## Accessibility

The `DaffTreeComponent` follows 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/). This is because tree views are non-normative and are not widely used across the internet.
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';
7 changes: 7 additions & 0 deletions libs/design/tree/src/interfaces/tree-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface DaffTreeData<T> {
title: string;
url: string;
id: string;
items: DaffTreeData<T>[];
data: T;
}
7 changes: 7 additions & 0 deletions libs/design/tree/src/interfaces/tree-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DaffTreeData } from './tree-data';

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 * from './tree/tree.component';
export * 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();
});
});
84 changes: 84 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,84 @@
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';

@Directive({
selector: '[daffTreeItem]',
})
export class DaffTreeItemDirective {

@HostBinding('class.daff-tree-item') class = true;

@HostBinding('class.daff-tree-item__parent') classParent = false;

@HostBinding('attr.id') id;

@HostBinding('attr.aria-expanded') ariaExpanded: string;

@HostBinding('style.--depth') depth: number;

@HostBinding('class.selected') get selectedClass() {
return this.selected;
};

@HostBinding('class.open') openClass = false;

private _node: DaffTreeFlatNode;

@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;
}

@Input() selected = false;

@HostListener('keydown.escape')
onEscape() {
this.toggleParent(this.node);
}

@HostListener('click')
onClick() {
if(this.node.hasChildren) {
this.toggleTree(this.node);
}
this.treeNotifier.notify();
}

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

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();
}

toggleTree(node: DaffTreeFlatNode) {
if(node._treeRef.open === false) {
node._treeRef.open = true;
} else {
node._treeRef.open = false;
}
}
}
38 changes: 38 additions & 0 deletions libs/design/tree/src/tree-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@use 'sass:map';
@use '../../scss/theming';
@use '../../scss/core';

@mixin daff-tree-theme($theme) {
$primary: map.get($theme, primary);
$secondary: map.get($theme, secondary);
$tertiary: map.get($theme, tertiary);
$base: core.daff-map-deep-get($theme, 'core.base');
$base-contrast: core.daff-map-deep-get($theme, 'core.base-contrast');
$white: core.daff-map-deep-get($theme, 'core.white');
$black: core.daff-map-deep-get($theme, 'core.black');
$gray: core.daff-map-deep-get($theme, 'core.gray');

.daff-tree-item {
$root: &;

background-color: $base;
color: theming.daff-illuminate($base-contrast, $gray, 2);

&:hover {
background-color: theming.daff-illuminate($base, $gray, 2);
}

&:after {
border-color: currentColor;
}

&.selected {
background-color: theming.daff-illuminate($base, $gray, 2);
color: $base-contrast;

&:before {
background-color: theming.daff-color($primary);
}
}
}
}
20 changes: 20 additions & 0 deletions libs/design/tree/src/tree.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

import { DaffTreeItemDirective } from './tree-item/tree-item.directive';
import { DaffTreeComponent } from './tree/tree.component';

@NgModule({
declarations: [
DaffTreeComponent,
DaffTreeItemDirective,
],
imports: [
CommonModule,
],
exports: [
DaffTreeComponent,
DaffTreeItemDirective,
],
})
export class DaffTreeModule { }
20 changes: 20 additions & 0 deletions libs/design/tree/src/tree/tree-notifier.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
Inject,
OnDestroy,
} from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Inject({})
export class DaffTreeNotifierService implements OnDestroy {
private _notice: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);

notice$ = this._notice.asObservable();

notify() {
this._notice.next(true);
}

ngOnDestroy(): void {
this._notice.complete();
}
}
7 changes: 7 additions & 0 deletions libs/design/tree/src/tree/tree.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<ng-container *ngFor="let node of flatTree; trackBy: trackByTreeElement">
<li [attr.aria-level]="node.level">
<ng-container
*ngTemplateOutlet="node.hasChildren ? withChildrenTemplate : treeItemTemplate; context: { $implicit: node }">
</ng-container>
</li>
</ng-container>
Loading

0 comments on commit d346f39

Please sign in to comment.