diff --git a/apps/design-land/src/app/app-routing.module.ts b/apps/design-land/src/app/app-routing.module.ts index e5516f046a..1d7271d38a 100644 --- a/apps/design-land/src/app/app-routing.module.ts +++ b/apps/design-land/src/app/app-routing.module.ts @@ -35,6 +35,7 @@ export const appRoutes: Routes = [ { path: 'quantity-field', loadChildren: () => import('./quantity-field/quantity-field.module').then(m => m.DesignLandQuantityFieldModule) }, { path: 'sidebar', loadChildren: () => import('./sidebar/sidebar.module').then(m => m.DesignLandSidebarModule) }, { path: 'radio', loadChildren: () => import('./radio/radio.module').then(m => m.DesignLandRadioModule) }, + { path: 'tree', loadChildren: () => import('./tree/tree.module').then(m => m.DesignLandTreeModule) }, { path: 'typography', loadChildren: () => import('./typography/typography.module').then(m => m.DesignLandTypographyModule) }, ]; diff --git a/apps/design-land/src/app/app.component.ts b/apps/design-land/src/app/app.component.ts index 86de244983..482d1105dc 100644 --- a/apps/design-land/src/app/app.component.ts +++ b/apps/design-land/src/app/app.component.ts @@ -2,6 +2,7 @@ import { Component, Injector, ComponentFactoryResolver, + ChangeDetectionStrategy, } from '@angular/core'; import { createCustomElement } from '@angular/elements'; @@ -18,6 +19,7 @@ import { MEDIA_GALLERY_EXAMPLES } from '@daffodil/design/media-gallery/examples' import { MODAL_EXAMPLES } from '@daffodil/design/modal/examples'; import { QUANTITY_FIELD_EXAMPLES } from '@daffodil/design/quantity-field/examples'; import { RADIO_EXAMPLES } from '@daffodil/design/radio/examples'; +import { TREE_EXAMPLES } from '@daffodil/design/tree/examples'; import { createCustomElementFromExample } from './core/elements/create-element-from-example'; @@ -25,6 +27,7 @@ import { createCustomElementFromExample } from './core/elements/create-element-f selector: 'design-land-app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DesignLandAppComponent { constructor( @@ -45,6 +48,7 @@ export class DesignLandAppComponent { ...MODAL_EXAMPLES, ...QUANTITY_FIELD_EXAMPLES, ...LIST_EXAMPLES, + ...TREE_EXAMPLES, ].map((componentExample) => createCustomElementFromExample(componentExample, injector)) .map((customElement) => { // Register the custom element with the browser. diff --git a/apps/design-land/src/app/tree/tree-routing.module.ts b/apps/design-land/src/app/tree/tree-routing.module.ts new file mode 100644 index 0000000000..feb62b90f0 --- /dev/null +++ b/apps/design-land/src/app/tree/tree-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { + Routes, + RouterModule, +} from '@angular/router'; + +import { DesignLandTreeComponent } from './tree.component'; + +export const treeRoutes: Routes = [ + { path: '', component: DesignLandTreeComponent }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(treeRoutes), + ], + exports: [ + RouterModule, + ], +}) +export class DesignLandTreeRoutingModule {} diff --git a/apps/design-land/src/app/tree/tree.component.html b/apps/design-land/src/app/tree/tree.component.html new file mode 100644 index 0000000000..87d985aeb6 --- /dev/null +++ b/apps/design-land/src/app/tree/tree.component.html @@ -0,0 +1,10 @@ + +

Tree

+

Tree is used to display hierarchial data.

+ +

Basic Tree

+ + +

Nested Tree

+ +
diff --git a/apps/design-land/src/app/tree/tree.component.scss b/apps/design-land/src/app/tree/tree.component.scss new file mode 100644 index 0000000000..ddb7262b3f --- /dev/null +++ b/apps/design-land/src/app/tree/tree.component.scss @@ -0,0 +1,11 @@ +daff-accordion { + max-width: 800px; +} + +.design-land-accordion-example { + margin-bottom: 40px; +} + +a { + font-weight: 400; +} diff --git a/apps/design-land/src/app/tree/tree.component.spec.ts b/apps/design-land/src/app/tree/tree.component.spec.ts new file mode 100644 index 0000000000..1667ea6e0c --- /dev/null +++ b/apps/design-land/src/app/tree/tree.component.spec.ts @@ -0,0 +1,31 @@ +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; + +import { DesignLandTreeComponent } from './tree.component'; + +describe('DesignLandTreeComponent', () => { + let component: DesignLandTreeComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DesignLandTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DesignLandTreeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/design-land/src/app/tree/tree.component.ts b/apps/design-land/src/app/tree/tree.component.ts new file mode 100644 index 0000000000..26489f46e3 --- /dev/null +++ b/apps/design-land/src/app/tree/tree.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'design-land-tree', + templateUrl: './tree.component.html', + styleUrls: ['./tree.component.scss'], +}) +export class DesignLandTreeComponent {} diff --git a/apps/design-land/src/app/tree/tree.module.ts b/apps/design-land/src/app/tree/tree.module.ts new file mode 100644 index 0000000000..bad68a3909 --- /dev/null +++ b/apps/design-land/src/app/tree/tree.module.ts @@ -0,0 +1,47 @@ +import { CommonModule } from '@angular/common'; +import { + ComponentFactoryResolver, + Injector, + NgModule, +} from '@angular/core'; +import { createCustomElement } from '@angular/elements'; + +import { DaffArticleModule } from '@daffodil/design'; +import { DaffTreeModule } from '@daffodil/design/tree'; +import { TREE_EXAMPLES } from '@daffodil/design/tree/examples'; + +import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module'; +import { DesignLandTreeRoutingModule } from './tree-routing.module'; +import { DesignLandTreeComponent } from './tree.component'; + +@NgModule({ + declarations: [ + DesignLandTreeComponent, + ], + imports: [ + CommonModule, + DesignLandTreeRoutingModule, + DesignLandExampleViewerModule, + DaffTreeModule, + DaffArticleModule, + ], +}) +export class DesignLandTreeModule { + constructor( + injector: Injector, + private componentFactoryResolver: ComponentFactoryResolver, + ) { + TREE_EXAMPLES + .map((classConstructor) => ({ + element: createCustomElement(classConstructor, { injector }), + class: classConstructor, + })) + .map((customElement) => { + // Register the custom element with the browser. + customElements.define( + this.componentFactoryResolver.resolveComponentFactory(customElement.class).selector + '-example', + customElement.element, + ); + }); + } +} diff --git a/libs/design/src/scss/theming/_theme.scss b/libs/design/src/scss/theming/_theme.scss index 8e7525f1d6..4bcf99f5d0 100644 --- a/libs/design/src/scss/theming/_theme.scss +++ b/libs/design/src/scss/theming/_theme.scss @@ -18,6 +18,7 @@ @import '../../molecules/paginator/paginator-theme'; @import '../../molecules/sidebar/sidebar/sidebar-theme'; @import '../../molecules/sidebar/sidebar-viewport/sidebar-viewport-theme'; +@import '../../../tree/src/tree-item/tree-item-theme.scss'; // // Generates the styles of a @daffodil/design theme. @@ -49,4 +50,5 @@ @include daff-paginator-theme($theme); @include daff-sidebar-theme($theme); @include daff-sidebar-viewport-theme($theme); + @include daff-tree-item-theme($theme); } diff --git a/libs/design/tree/examples/ng-package.json b/libs/design/tree/examples/ng-package.json new file mode 100644 index 0000000000..1c1d4695be --- /dev/null +++ b/libs/design/tree/examples/ng-package.json @@ -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"] + } +} \ No newline at end of file diff --git a/libs/design/tree/examples/package.json b/libs/design/tree/examples/package.json new file mode 100644 index 0000000000..c9c08733e7 --- /dev/null +++ b/libs/design/tree/examples/package.json @@ -0,0 +1,3 @@ +{ + "name": "@daffodil/design/tree/examples" +} diff --git a/libs/design/tree/examples/src/basic-tree/basic-tree.component.html b/libs/design/tree/examples/src/basic-tree/basic-tree.component.html new file mode 100644 index 0000000000..6393c60193 --- /dev/null +++ b/libs/design/tree/examples/src/basic-tree/basic-tree.component.html @@ -0,0 +1,26 @@ + + +
Foundations
+ +
Color
+
+ +
Typography
+
+
+ +
Packages
+ +
@daffodil/authorizenet
+ +
Installation
+
+
+ +
@daffodil/cart
+
+ +
@daffodil/category
+
+
+
\ No newline at end of file diff --git a/libs/design/tree/examples/src/basic-tree/basic-tree.component.ts b/libs/design/tree/examples/src/basic-tree/basic-tree.component.ts new file mode 100644 index 0000000000..8321fbfe79 --- /dev/null +++ b/libs/design/tree/examples/src/basic-tree/basic-tree.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'basic-tree', + templateUrl: './basic-tree.component.html', +}) +export class BasicTreeComponent {} diff --git a/libs/design/tree/examples/src/basic-tree/basic-tree.module.ts b/libs/design/tree/examples/src/basic-tree/basic-tree.module.ts new file mode 100644 index 0000000000..9fccf7332f --- /dev/null +++ b/libs/design/tree/examples/src/basic-tree/basic-tree.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { DaffTreeModule } from '@daffodil/design/tree'; + +import { BasicTreeComponent } from './basic-tree.component'; + + +@NgModule({ + declarations: [ + BasicTreeComponent, + ], + exports: [ + BasicTreeComponent, + ], + imports: [ + DaffTreeModule, + RouterModule, + ], +}) +export class BasicTreeModule { } diff --git a/libs/design/tree/examples/src/index.ts b/libs/design/tree/examples/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/design/tree/examples/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/design/tree/examples/src/nested-tree/nested-tree.component.html b/libs/design/tree/examples/src/nested-tree/nested-tree.component.html new file mode 100644 index 0000000000..011359d20d --- /dev/null +++ b/libs/design/tree/examples/src/nested-tree/nested-tree.component.html @@ -0,0 +1,44 @@ + + +
Design
+ +
Foundations
+ +
Color
+
+ +
Typography
+
+
+ +
Components
+ +
Atoms
+ +
Button
+
+ +
Forms
+
+
Checkbox
+
+ +
Form Field
+
+ +
Input
+
+ +
Radio
+
+ +
Select
+
+ +
Textarea
+
+ +
+
+
+
\ No newline at end of file diff --git a/libs/design/tree/examples/src/nested-tree/nested-tree.component.ts b/libs/design/tree/examples/src/nested-tree/nested-tree.component.ts new file mode 100644 index 0000000000..56049bacea --- /dev/null +++ b/libs/design/tree/examples/src/nested-tree/nested-tree.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'nested-tree', + templateUrl: './nested-tree.component.html', +}) +export class NestedTreeComponent {} diff --git a/libs/design/tree/examples/src/nested-tree/nested-tree.module.ts b/libs/design/tree/examples/src/nested-tree/nested-tree.module.ts new file mode 100644 index 0000000000..de63e3151c --- /dev/null +++ b/libs/design/tree/examples/src/nested-tree/nested-tree.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { DaffTreeModule } from '@daffodil/design/tree'; + +import { NestedTreeComponent } from './nested-tree.component'; + + +@NgModule({ + declarations: [ + NestedTreeComponent, + ], + exports: [ + NestedTreeComponent, + ], + imports: [ + DaffTreeModule, + RouterModule, + ], +}) +export class NestedTreeModule { } diff --git a/libs/design/tree/examples/src/public_api.ts b/libs/design/tree/examples/src/public_api.ts new file mode 100644 index 0000000000..220c57fede --- /dev/null +++ b/libs/design/tree/examples/src/public_api.ts @@ -0,0 +1,9 @@ +import { BasicTreeComponent } from './basic-tree/basic-tree.component'; +import { NestedTreeComponent } from './nested-tree/nested-tree.component'; +export { BasicTreeModule } from './basic-tree/basic-tree.module'; +export { NestedTreeModule } from './nested-tree/nested-tree.module'; + +export const TREE_EXAMPLES = [ + BasicTreeComponent, + NestedTreeComponent, +]; diff --git a/libs/design/tree/ng-package.json b/libs/design/tree/ng-package.json new file mode 100644 index 0000000000..95ab59542e --- /dev/null +++ b/libs/design/tree/ng-package.json @@ -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"] + } +} \ No newline at end of file diff --git a/libs/design/tree/package.json b/libs/design/tree/package.json new file mode 100644 index 0000000000..15ff4011ab --- /dev/null +++ b/libs/design/tree/package.json @@ -0,0 +1,3 @@ +{ + "name": "@daffodil/design/tree" +} diff --git a/libs/design/tree/src/README.md b/libs/design/tree/src/README.md new file mode 100644 index 0000000000..6ac8769459 --- /dev/null +++ b/libs/design/tree/src/README.md @@ -0,0 +1,50 @@ +# Tree +`DaffTreeComponent` is used to display hierarchial data. +s +## Usage +``` + + +
Design
+ +
Foundations
+ + Color + + + Typography + +
+ +
Components
+ + Atoms + + Button + + + Checkbox + + +
+
+ +
Packages
+ +
@daffodil/authorizenet
+ + Overview + + + Installation + +
+ + @daffodil/cart + + + @daffodil/category + +
+
+``` diff --git a/libs/design/tree/src/animation/tree-animation-state.spec.ts b/libs/design/tree/src/animation/tree-animation-state.spec.ts new file mode 100644 index 0000000000..da7b480c1c --- /dev/null +++ b/libs/design/tree/src/animation/tree-animation-state.spec.ts @@ -0,0 +1,12 @@ +import { getAnimationState } from './tree-animation-state'; + +describe('treeAnimationState Calculation', () => { + + it('should return `open` if it is open', () => { + expect(getAnimationState(true)).toEqual('open'); + }); + + it('should return `closed` if it is not show', () => { + expect(getAnimationState(false)).toEqual('closed'); + }); +}); diff --git a/libs/design/tree/src/animation/tree-animation-state.ts b/libs/design/tree/src/animation/tree-animation-state.ts new file mode 100644 index 0000000000..9b2b2e1727 --- /dev/null +++ b/libs/design/tree/src/animation/tree-animation-state.ts @@ -0,0 +1,7 @@ +export const getAnimationState = (open: boolean) => { + if(open){ + return 'open'; + } else { + return 'closed'; + } +}; diff --git a/libs/design/tree/src/animation/tree-animation.ts b/libs/design/tree/src/animation/tree-animation.ts new file mode 100644 index 0000000000..86c97437e8 --- /dev/null +++ b/libs/design/tree/src/animation/tree-animation.ts @@ -0,0 +1,29 @@ +import { + animate, + state, + style, + transition, + trigger, + AnimationTriggerMetadata, +} from '@angular/animations'; + +export const daffTreeAnimations: { + readonly openTree: AnimationTriggerMetadata; +} = { + openTree: trigger('openTree', [ + state('open', style({ + visibility: 'visible', + opacity: '1', + height: '*', + })), + state('closed,*', style({ + visibility: 'hidden', + overflow: 'hidden', + opacity: '0', + height: '0', + })), + transition('open <=> closed', + animate('150ms ease-in'), + ), + ]), +}; diff --git a/libs/design/tree/src/index.ts b/libs/design/tree/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/design/tree/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/design/tree/src/public_api.ts b/libs/design/tree/src/public_api.ts new file mode 100644 index 0000000000..fea6e392a5 --- /dev/null +++ b/libs/design/tree/src/public_api.ts @@ -0,0 +1,5 @@ +export { DaffTreeModule } from './tree.module'; +export * from './tree/tree.component'; +export * from './tree-item/tree-item.component'; +export * from './tree-item-title/tree-item-title.directive'; +export * from './tree-item-content/tree-item-content.directive'; diff --git a/libs/design/tree/src/tree-item-content/tree-item-content.directive.spec.ts b/libs/design/tree/src/tree-item-content/tree-item-content.directive.spec.ts new file mode 100644 index 0000000000..625aa5c15b --- /dev/null +++ b/libs/design/tree/src/tree-item-content/tree-item-content.directive.spec.ts @@ -0,0 +1,55 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffTreeItemContentDirective } from './tree-item-content.directive'; + +@Component({ + template: ` +
Content
+ `, +}) +class WrapperComponent {} + +describe('DaffTreeItemContentDirective', () => { + let treeItemContent: DaffTreeItemContentDirective; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffTreeItemContentDirective, + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + de = fixture.debugElement.query(By.css('[daffTreeItemContent]')); + treeItemContent = de.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(treeItemContent).toBeTruthy(); + }); + + describe('[daffTreeItemContent]', () => { + it('should add a class of "daff-tree-item__content" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-tree-item__content': true, + })); + }); + }); +}); diff --git a/libs/design/tree/src/tree-item-content/tree-item-content.directive.ts b/libs/design/tree/src/tree-item-content/tree-item-content.directive.ts new file mode 100644 index 0000000000..218ba65632 --- /dev/null +++ b/libs/design/tree/src/tree-item-content/tree-item-content.directive.ts @@ -0,0 +1,15 @@ +import { + Directive, + HostBinding, +} from '@angular/core'; + +@Directive({ + selector: '[daffTreeItemContent]', +}) +export class DaffTreeItemContentDirective { + + /** + * @docs-private + */ + @HostBinding('class.daff-tree-item__content') class = true; +} diff --git a/libs/design/tree/src/tree-item-title/tree-item-title.directive.spec.ts b/libs/design/tree/src/tree-item-title/tree-item-title.directive.spec.ts new file mode 100644 index 0000000000..c64c234992 --- /dev/null +++ b/libs/design/tree/src/tree-item-title/tree-item-title.directive.spec.ts @@ -0,0 +1,55 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffTreeItemTitleDirective } from './tree-item-title.directive'; + +@Component({ + template: ` +
Title
+ `, +}) +class WrapperComponent {} + +describe('DaffTreeItemTitleDirective', () => { + let treeItemTitle: DaffTreeItemTitleDirective; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffTreeItemTitleDirective, + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + de = fixture.debugElement.query(By.css('[daffTreeItemTitle]')); + treeItemTitle = de.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(treeItemTitle).toBeTruthy(); + }); + + describe('[daffTreeItemTitle]', () => { + it('should add a class of "daff-tree-item__title" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-tree-item__title': true, + })); + }); + }); +}); diff --git a/libs/design/tree/src/tree-item-title/tree-item-title.directive.ts b/libs/design/tree/src/tree-item-title/tree-item-title.directive.ts new file mode 100644 index 0000000000..3aac9124a7 --- /dev/null +++ b/libs/design/tree/src/tree-item-title/tree-item-title.directive.ts @@ -0,0 +1,15 @@ +import { + Directive, + HostBinding, +} from '@angular/core'; + +@Directive({ + selector: '[daffTreeItemTitle]', +}) +export class DaffTreeItemTitleDirective { + + /** + * @docs-private + */ + @HostBinding('class.daff-tree-item__title') class = true; +} diff --git a/libs/design/tree/src/tree-item/tree-item-theme.scss b/libs/design/tree/src/tree-item/tree-item-theme.scss new file mode 100644 index 0000000000..82b565b2f8 --- /dev/null +++ b/libs/design/tree/src/tree-item/tree-item-theme.scss @@ -0,0 +1,31 @@ +@mixin daff-tree-item-theme($theme) { + $primary: map-get($theme, primary); + $secondary: map-get($theme, secondary); + $tertiary: map-get($theme, tertiary); + $base: daff-map-deep-get($theme, 'core.base'); + $base-contrast: daff-map-deep-get($theme, 'core.base-contrast'); + $white: daff-map-deep-get($theme, 'core.white'); + $black: daff-map-deep-get($theme, 'core.black'); + + .daff-tree-item { + $root: &; + &__header { + color: daff-illuminate($base-contrast, $daff-gray, 4); + + &:hover { + background: daff-illuminate($base, $daff-gray, 2); + } + } + + &--selected { + > #{$root}__header { + background: daff-illuminate($base, $daff-gray, 2); + color: $base-contrast; + + &::before { + background-color: daff-color($primary); + } + } + } + } +} diff --git a/libs/design/tree/src/tree-item/tree-item.component.html b/libs/design/tree/src/tree-item/tree-item.component.html new file mode 100644 index 0000000000..0ded0a0b90 --- /dev/null +++ b/libs/design/tree/src/tree-item/tree-item.component.html @@ -0,0 +1,12 @@ +
+ + + + + + + +
+
+ +
diff --git a/libs/design/tree/src/tree-item/tree-item.component.scss b/libs/design/tree/src/tree-item/tree-item.component.scss new file mode 100644 index 0000000000..16d3ee3046 --- /dev/null +++ b/libs/design/tree/src/tree-item/tree-item.component.scss @@ -0,0 +1,72 @@ +@import 'daff-util'; + +:host(.daff-tree-item) { + $root: '.daff-tree-item'; + display: block; + text-decoration: none; + + &#{$root}--level-1 { + #{$root}__header { + padding-left: 32px; + } + } + + &#{$root}--level-2 { + #{$root}__header { + padding-left: 48px; + } + } + + &#{$root}--level-3 { + #{$root}__header { + padding-left: 64px; + } + } + + &#{$root}--level-1, + &#{$root}--level-2, + &#{$root}--level-3 { + #{$root}__header { + font-weight: 400; + } + } + + &#{$root}--selected { + #{$root}__header { + @include embolden(); + } + } + + ::ng-deep { + #{$root}__title { + @include single-line-ellipsis(); + } + } +} + +.daff-tree-item { + &__header { + @include clickable(); + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + position: relative; + padding: 12px 16px; + width: 100%; + transition: background-color 150ms; + + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 4px; + } + + .daff-suffix { + margin-left: 8px; + } + } +} diff --git a/libs/design/tree/src/tree-item/tree-item.component.spec.ts b/libs/design/tree/src/tree-item/tree-item.component.spec.ts new file mode 100644 index 0000000000..5c6750f101 --- /dev/null +++ b/libs/design/tree/src/tree-item/tree-item.component.spec.ts @@ -0,0 +1,333 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +import { DaffTreeComponent } from '../tree/tree.component'; +import { DaffTreeItemComponent } from './tree-item.component'; + + +@Component({ template: ` + + +

Size and Fit

+
no content
+
+
+` }) +class UsageWrapperComponent { + initiallyOpenValue: boolean; +} + +describe('DaffTreeItemComponent', () => { + + describe('usage', () => { + let fixture: ComponentFixture; + let wrapper: UsageWrapperComponent; + let treeHeader: DebugElement; + let daffTreeItem: DaffTreeItemComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FontAwesomeModule, + ], + declarations: [ + UsageWrapperComponent, + DaffTreeItemComponent, + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UsageWrapperComponent); + wrapper = fixture.componentInstance; + + fixture.detectChanges(); + + daffTreeItem = fixture.debugElement.query(By.css('daff-tree-item')).componentInstance; + treeHeader = fixture.debugElement.query(By.css('.daff-tree-item__header')); + }); + + it('should create', () => { + expect(daffTreeItem).toBeTruthy(); + }); + + it('should set _open to false by default', () => { + expect(daffTreeItem._open).toEqual(false); + }); + + it('should set _animationState to void by default', () => { + expect(daffTreeItem._animationState).toEqual('void'); + }); + + it('should be able to accept an initiallyOpen input', () => { + wrapper.initiallyOpenValue = false; + + fixture.detectChanges(); + + expect(daffTreeItem.initiallyOpen).toEqual(false); + + wrapper.initiallyOpenValue = true; + + fixture.detectChanges(); + + expect(daffTreeItem.initiallyOpen).toEqual(true); + }); + + describe('ngOnInit', () => { + + describe('when initiallyOpen is true', () => { + + beforeEach(() => { + wrapper.initiallyOpenValue = true; + fixture.detectChanges(); + }); + + it('should set _open to true', () => { + daffTreeItem.ngOnInit(); + expect(daffTreeItem._open).toBeTruthy(); + }); + }); + + describe('when initiallyOpen is set to undefineds', () => { + + beforeEach(() => { + wrapper.initiallyOpenValue = undefined; + fixture.detectChanges(); + }); + + it('should set open to false', () => { + daffTreeItem.ngOnInit(); + expect(daffTreeItem._open).toBeFalsy(); + }); + }); + }); + + describe('when tree header is clicked', () => { + + beforeEach(() => { + spyOn(daffTreeItem, 'toggleOpen'); + + treeHeader.nativeElement.click(); + }); + + it('should call toggleOpen', () => { + expect(daffTreeItem.toggleOpen).toHaveBeenCalledWith(); + }); + }); + + describe('toggleOpen', () => { + it('should toggle open', () => { + daffTreeItem._open = false; + + daffTreeItem.toggleOpen(); + expect(daffTreeItem._open).toEqual(true); + + daffTreeItem.toggleOpen(); + expect(daffTreeItem._open).toEqual(false); + }); + + it('should toggle _animationState between void and open', () => { + daffTreeItem.toggleOpen(); + expect(daffTreeItem._animationState).toEqual('open'); + + daffTreeItem.toggleOpen(); + expect(daffTreeItem._animationState).toEqual('void'); + }); + }); + }); + + @Component({ template: ` + + + + +

Size and Fit

+
no content
+
+
+
+
+ ` }) + class FirstChildWrapperComponent { + initiallyOpenValue: boolean; + } + + describe('when s are nested in s', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FontAwesomeModule, + ], + declarations: [ + FirstChildWrapperComponent, + DaffTreeItemComponent, + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FirstChildWrapperComponent); + + fixture.detectChanges(); + }); + + it('should reset `s level to 0 with each new ', () => { + const treeItemUnderTest: DaffTreeItemComponent = + fixture.debugElement.queryAll(By.css('daff-tree-item'))[1].componentInstance; + + expect(treeItemUnderTest._level).toEqual(0); + }); + }); + + @Component({ template: ` + + +

Size and Fit

+ +

Size and Fit

+
+
+
+ ` }) + class NotFirstChildWrapperComponent { + initiallyOpenValue: boolean; + } + + describe('when it is not the first child tree-item', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FontAwesomeModule, + ], + declarations: [ + NotFirstChildWrapperComponent, + DaffTreeItemComponent, + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotFirstChildWrapperComponent); + + fixture.detectChanges(); + }); + + it('should have a level that matches the depth of the tree-item', () => { + const treeItemUnderTest: DaffTreeItemComponent = + fixture.debugElement.queryAll(By.css('daff-tree-item'))[1].componentInstance; + + expect(treeItemUnderTest._level).toEqual(1); + }); + }); + + @Component({ template: ` + + +

Size and Fit

+ +

Size and Fit

+
+
+
+ ` }) + class HasChildWrapperComponent { + initiallyOpenValue: boolean; + } + + describe('when it has a child tree-item', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FontAwesomeModule, + ], + declarations: [ + HasChildWrapperComponent, + DaffTreeItemComponent, + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HasChildWrapperComponent); + + fixture.detectChanges(); + }); + + it('should add a chevron to the tree item title', () => { + const suffix = fixture.debugElement.query(By.css('[daffSuffix]')); + + expect(suffix).toBeDefined(); + }); + }); + + @Component({ template: ` + + +

Size and Fit

+
+
+ ` }) + class HasNoChildWrapperComponent { + initiallyOpenValue: boolean; + } + + describe('when it does not have a child tree-item', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FontAwesomeModule, + ], + declarations: [ + HasNoChildWrapperComponent, + DaffTreeItemComponent, + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HasNoChildWrapperComponent); + + fixture.detectChanges(); + }); + + it('should not add a chevron to the tree item title', () => { + const suffix = fixture.debugElement.query(By.css('[daffSuffix]')); + + expect(suffix).toBeNull(); + }); + }); +}); + diff --git a/libs/design/tree/src/tree-item/tree-item.component.ts b/libs/design/tree/src/tree-item/tree-item.component.ts new file mode 100644 index 0000000000..d25e51a2f4 --- /dev/null +++ b/libs/design/tree/src/tree-item/tree-item.component.ts @@ -0,0 +1,155 @@ +import { + Component, + Input, + OnInit, + HostBinding, + SkipSelf, + Optional, + ContentChildren, + QueryList, + ChangeDetectionStrategy, + ChangeDetectorRef, + AfterContentChecked, +} from '@angular/core'; +import { + faChevronUp, + faChevronDown, +} from '@fortawesome/free-solid-svg-icons'; + +import { daffTreeAnimations } from '../animation/tree-animation'; +import { getAnimationState } from '../animation/tree-animation-state'; +import { DaffTreeComponent } from '../tree/tree.component'; + +@Component({ + selector: 'daff-tree-item, a[daff-tree-item]', + templateUrl: './tree-item.component.html', + styleUrls: ['./tree-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + daffTreeAnimations.openTree, + ], +}) +export class DaffTreeItemComponent implements OnInit, AfterContentChecked { + /** + * @docs-private + */ + faChevronUp = faChevronUp; + /** + * @docs-private + */ + faChevronDown = faChevronDown; + + /** + * @docs-private + */ + @HostBinding('class') get classes() { + return [ + 'daff-tree-item', + 'daff-tree-item--level-' + this._level, + ]; + } + + /** + * @docs-private + */ + @HostBinding('class.daff-tree-item--selected') get selectedClass() { + return this.selected; + } + + constructor( + private tree: DaffTreeComponent, + private cd: ChangeDetectorRef, + @SkipSelf() @Optional() private treeItemParent: DaffTreeItemComponent, + ) { } + + /** + * Can be used to open the tree item upon initial render, but lets subsequent + * "open" states be controlled by the component without external + * state management. + */ + @Input() initiallyOpen = false; + + /** + * When a tree item is selected, this means that either this element, + * or some child element (recursively), is selected. + */ + @Input() selected = false; + + @ContentChildren(DaffTreeItemComponent, { descendants: true }) + + /** + * @docs-private + */ + _treeItemChild: QueryList; + + @ContentChildren(DaffTreeItemComponent) + + /** + * @docs-private + */ + _directChildren: QueryList; + + get hasChildren(): boolean { + return this._treeItemChild.length > 0; + } + + /** + * @docs-private + */ + openTree() { + if (!this.hasChildren) { + return; + } + + this._open = true; + this._animationState = getAnimationState(this._open); + } + + /** + * @docs-private + */ + _interacted = false; + + ngAfterContentChecked() { + const selectedChildren = this._treeItemChild.filter((item) => item.selected); + if (selectedChildren.length > 0 && this._interacted === false) { + this.openTree(); + this.cd.markForCheck(); + } + } + + /** + * @docs-private + */ + _open = false; + + /** + * @docs-private + */ + _animationState: string; + + /** + * @docs-private + */ + _level = 0; + + /** + * @docs-private + */ + ngOnInit() { + if (this.treeItemParent && this.tree === this.treeItemParent.tree) { + this._level = this.treeItemParent._level + 1; + } + this._open = this.initiallyOpen; + } + + toggleOpen() { + if (!this.hasChildren) { + return; + } + + this._open = !this._open; + this._interacted = true; + this._animationState = getAnimationState(this._open); + } +} diff --git a/libs/design/tree/src/tree.module.ts b/libs/design/tree/src/tree.module.ts new file mode 100644 index 0000000000..fd14606bd5 --- /dev/null +++ b/libs/design/tree/src/tree.module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +import { DaffPrefixSuffixModule } from '@daffodil/design'; + +import { DaffTreeItemContentDirective } from './tree-item-content/tree-item-content.directive'; +import { DaffTreeItemTitleDirective } from './tree-item-title/tree-item-title.directive'; +import { DaffTreeItemComponent } from './tree-item/tree-item.component'; +import { DaffTreeComponent } from './tree/tree.component'; + +@NgModule({ + imports: [ + CommonModule, + + FontAwesomeModule, + DaffPrefixSuffixModule, + ], + declarations: [ + DaffTreeComponent, + DaffTreeItemComponent, + DaffTreeItemTitleDirective, + DaffTreeItemContentDirective, + ], + exports: [ + DaffTreeComponent, + DaffTreeItemComponent, + DaffTreeItemTitleDirective, + DaffTreeItemContentDirective, + ], +}) +export class DaffTreeModule { } diff --git a/libs/design/tree/src/tree/tree.component.scss b/libs/design/tree/src/tree/tree.component.scss new file mode 100644 index 0000000000..2486878d86 --- /dev/null +++ b/libs/design/tree/src/tree/tree.component.scss @@ -0,0 +1,4 @@ +:host { + display: block; + width: 100%; +} diff --git a/libs/design/tree/src/tree/tree.component.spec.ts b/libs/design/tree/src/tree/tree.component.spec.ts new file mode 100644 index 0000000000..a802db6a6d --- /dev/null +++ b/libs/design/tree/src/tree/tree.component.spec.ts @@ -0,0 +1,32 @@ +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; + +import { DaffTreeComponent } from './tree.component'; + +describe('DaffTreeComponent', () => { + let component: DaffTreeComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffTreeComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DaffTreeComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/design/tree/src/tree/tree.component.ts b/libs/design/tree/src/tree/tree.component.ts new file mode 100644 index 0000000000..8224dbdca3 --- /dev/null +++ b/libs/design/tree/src/tree/tree.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'daff-tree', + template: '', + styleUrls: ['./tree.component.scss'], +}) + +export class DaffTreeComponent { + +}