test(ivy): add expanding_rows performance benchmark which runs in ViewEngine and Ivy (#30449)
PR Close #30449
This commit is contained in:
		
							parent
							
								
									1714451a6d
								
							
						
					
					
						commit
						b1ba2d6f4e
					
				
							
								
								
									
										74
									
								
								modules/benchmarks/src/expanding_rows/BUILD.bazel
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								modules/benchmarks/src/expanding_rows/BUILD.bazel
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | |||||||
|  | package(default_visibility = ["//modules/benchmarks:__subpackages__"]) | ||||||
|  | 
 | ||||||
|  | load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle") | ||||||
|  | load("@npm_bazel_typescript//:index.bzl", "ts_devserver") | ||||||
|  | load("//modules/benchmarks:benchmark_test.bzl", "benchmark_test") | ||||||
|  | 
 | ||||||
|  | ng_module( | ||||||
|  |     name = "application_lib", | ||||||
|  |     srcs = glob( | ||||||
|  |         ["**/*.ts"], | ||||||
|  |         exclude = ["**/*_spec.ts"], | ||||||
|  |     ), | ||||||
|  |     deps = [ | ||||||
|  |         "//packages:types", | ||||||
|  |         "//packages/common", | ||||||
|  |         "//packages/core", | ||||||
|  |         "//packages/platform-browser", | ||||||
|  |         "@npm//rxjs", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | ng_module( | ||||||
|  |     name = "application_spec", | ||||||
|  |     srcs = glob(["**/*_spec.ts"]), | ||||||
|  |     deps = [ | ||||||
|  |         "//packages:types", | ||||||
|  |         "//packages/common", | ||||||
|  |         "//packages/core", | ||||||
|  |         "@npm//reflect-metadata", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | ng_rollup_bundle( | ||||||
|  |     name = "bundle", | ||||||
|  |     entry_point = "modules/benchmarks/src/expanding_rows/index.js", | ||||||
|  |     deps = [ | ||||||
|  |         ":application_lib", | ||||||
|  |         "@npm//rxjs", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | ts_devserver( | ||||||
|  |     name = "prodserver", | ||||||
|  |     static_files = [ | ||||||
|  |         ":bundle.min_debug.js", | ||||||
|  |         ":bundle.min.js", | ||||||
|  |         "@npm//node_modules/zone.js:dist/zone.js", | ||||||
|  |         "index.html", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | ts_devserver( | ||||||
|  |     name = "devserver", | ||||||
|  |     entry_module = "angular/modules/benchmarks/src/expanding_rows/index", | ||||||
|  |     index_html = "index.html", | ||||||
|  |     scripts = [ | ||||||
|  |         "@npm//node_modules/tslib:tslib.js", | ||||||
|  |         "//tools/rxjs:rxjs_umd_modules", | ||||||
|  |     ], | ||||||
|  |     serving_path = "/index.js", | ||||||
|  |     static_files = [ | ||||||
|  |         "@npm//node_modules/zone.js:dist/zone.js", | ||||||
|  |         "index.html", | ||||||
|  |     ], | ||||||
|  |     deps = [":application_lib"], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | benchmark_test( | ||||||
|  |     name = "perf", | ||||||
|  |     server = ":prodserver", | ||||||
|  |     deps = [ | ||||||
|  |         ":application_spec", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
							
								
								
									
										76
									
								
								modules/benchmarks/src/expanding_rows/benchmark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								modules/benchmarks/src/expanding_rows/benchmark.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {CommonModule} from '@angular/common'; | ||||||
|  | import {AfterViewInit, Component, NgModule, ViewChild, ViewEncapsulation} from '@angular/core'; | ||||||
|  | import {BrowserModule} from '@angular/platform-browser'; | ||||||
|  | 
 | ||||||
|  | import {BenchmarkModule} from './benchmark_module'; | ||||||
|  | import {BenchmarkableExpandingRow} from './benchmarkable_expanding_row'; | ||||||
|  | import {BenchmarkableExpandingRowModule} from './benchmarkable_expanding_row_module'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'benchmark-root', | ||||||
|  |   encapsulation: ViewEncapsulation.None, | ||||||
|  |   template: ` | ||||||
|  |     <h2>cfc-expanding-row initialization benchmark</h2> | ||||||
|  | 
 | ||||||
|  |     <section> | ||||||
|  |       <button id="reset" (click)="reset()">Reset</button> | ||||||
|  |       <button (click)="handleInitClick()">Init</button> | ||||||
|  |       <button id="run" (click)="runAll()">Run All</button> | ||||||
|  |     </section> | ||||||
|  | 
 | ||||||
|  |     <benchmark-area> | ||||||
|  |       <benchmarkable-expanding-row></benchmarkable-expanding-row> | ||||||
|  |     </benchmark-area>`, | ||||||
|  | }) | ||||||
|  | export class InitializationRoot implements AfterViewInit { | ||||||
|  |   @ViewChild(BenchmarkableExpandingRow, {static: true}) | ||||||
|  |   expandingRow !: BenchmarkableExpandingRow; | ||||||
|  | 
 | ||||||
|  |   ngAfterViewInit() {} | ||||||
|  | 
 | ||||||
|  |   reset() { this.expandingRow.reset(); } | ||||||
|  | 
 | ||||||
|  |   async runAll() { | ||||||
|  |     await execTimed('initialization_benchmark', async() => { await this.doInit(); }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async handleInitClick() { await this.doInit(); } | ||||||
|  | 
 | ||||||
|  |   private async doInit() { | ||||||
|  |     await execTimed('initial_load', async() => { this.expandingRow.init(); }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @NgModule({ | ||||||
|  |   declarations: [InitializationRoot], | ||||||
|  |   exports: [InitializationRoot], | ||||||
|  |   imports: [ | ||||||
|  |     CommonModule, | ||||||
|  |     BenchmarkableExpandingRowModule, | ||||||
|  |     BenchmarkModule, | ||||||
|  |     BrowserModule, | ||||||
|  |   ], | ||||||
|  |   bootstrap: [InitializationRoot], | ||||||
|  | }) | ||||||
|  | // Component benchmarks must export a BenchmarkModule.
 | ||||||
|  | export class ExpandingRowBenchmarkModule { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function execTimed(description: string, func: () => Promise<void>) { | ||||||
|  |   console.time(description); | ||||||
|  |   await func(); | ||||||
|  |   await nextTick(200); | ||||||
|  |   console.timeEnd(description); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function nextTick(delay = 1) { | ||||||
|  |   return new Promise((res, rej) => { setTimeout(() => { res(); }, delay); }); | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								modules/benchmarks/src/expanding_rows/benchmark_module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								modules/benchmarks/src/expanding_rows/benchmark_module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {ErrorHandler} from '@angular/core'; | ||||||
|  | import {Component, Injectable, NgModule} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'benchmark-area', | ||||||
|  |   template: '<ng-content></ng-content>', | ||||||
|  |   styles: [` | ||||||
|  |     :host { | ||||||
|  |       padding: 1; | ||||||
|  |       margin: 1; | ||||||
|  |       background-color: white; | ||||||
|  |       width: 1000px; | ||||||
|  |       display: block; | ||||||
|  |     }`],
 | ||||||
|  |   host: { | ||||||
|  |     'class': 'cfc-ng2-region', | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | export class BenchmarkArea { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | declare interface ExtendedWindow extends Window { | ||||||
|  |   benchmarkErrors?: string[]; | ||||||
|  | } | ||||||
|  | const extendedWindow = window as ExtendedWindow; | ||||||
|  | 
 | ||||||
|  | @Injectable({providedIn: 'root'}) | ||||||
|  | export class BenchmarkErrorHandler implements ErrorHandler { | ||||||
|  |   handleError(error: Error) { | ||||||
|  |     if (!extendedWindow.benchmarkErrors) { | ||||||
|  |       extendedWindow.benchmarkErrors = []; | ||||||
|  |     } | ||||||
|  |     extendedWindow.benchmarkErrors.push(error.message); | ||||||
|  |     console.error(error); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @NgModule({ | ||||||
|  |   declarations: [BenchmarkArea], | ||||||
|  |   exports: [BenchmarkArea], | ||||||
|  |   providers: [ | ||||||
|  |     {provide: ErrorHandler, useClass: BenchmarkErrorHandler}, | ||||||
|  |   ] | ||||||
|  | }) | ||||||
|  | export class BenchmarkModule { | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								modules/benchmarks/src/expanding_rows/benchmark_spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								modules/benchmarks/src/expanding_rows/benchmark_spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {$} from 'protractor'; | ||||||
|  | import {openTreeBenchmark, runTreeBenchmark} from './tree_perf_test_utils'; | ||||||
|  | 
 | ||||||
|  | describe('benchmarks', () => { | ||||||
|  | 
 | ||||||
|  |   it('should work for createOnly', done => { | ||||||
|  |     runTreeBenchmark({ | ||||||
|  |       // This cannot be called "createOnly" because the actual destroy benchmark
 | ||||||
|  |       // has the "createOnly" id already. See: https://github.com/angular/angular/pull/21503
 | ||||||
|  |       id: 'createOnlyForReal', | ||||||
|  |       prepare: () => $('#destroyDom').click(), | ||||||
|  |       work: () => $('#createDom').click() | ||||||
|  |     }).then(done, done.fail); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | }); | ||||||
| @ -0,0 +1,73 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {Component} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | export interface MlbTeam { | ||||||
|  |   name: string; | ||||||
|  |   id: number; | ||||||
|  |   division: string; | ||||||
|  |   stadium: string; | ||||||
|  |   projection: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'benchmarkable-expanding-row', | ||||||
|  |   template: ` | ||||||
|  |     <cfc-expanding-row-host *ngIf="showExpandingRow"> | ||||||
|  |     <cfc-expanding-row *ngFor="let team of teams" [rowId]="$any(team.id)"> | ||||||
|  |       <cfc-expanding-row-summary> | ||||||
|  |         Team {{team.id}} | ||||||
|  |       </cfc-expanding-row-summary> | ||||||
|  |       <cfc-expanding-row-details-caption> | ||||||
|  |         {{team.name}} | ||||||
|  |         <a href="https://www.google.com" class="cfc-demo-expanding-row-caption-link"> | ||||||
|  |           {{team.id}} | ||||||
|  |         </a> | ||||||
|  |       </cfc-expanding-row-details-caption> | ||||||
|  |       <cfc-expanding-row-details-content> | ||||||
|  |         <ul ace-list> | ||||||
|  |           <li>Division: {{team.division}}</li> | ||||||
|  |           <li> | ||||||
|  |             <a href="https://www.google.com">{{team.stadium}}</a> | ||||||
|  |           </li> | ||||||
|  |           <li>Projected Record: {{team.projection}}</li> | ||||||
|  |         </ul> | ||||||
|  |       </cfc-expanding-row-details-content> | ||||||
|  |     </cfc-expanding-row> | ||||||
|  |   </cfc-expanding-row-host>`, | ||||||
|  | }) | ||||||
|  | export class BenchmarkableExpandingRow { | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   showExpandingRow!: boolean; | ||||||
|  | 
 | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   teams!: MlbTeam[]; | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   private fakeTeams!: MlbTeam[]; | ||||||
|  | 
 | ||||||
|  |   init(): void { | ||||||
|  |     this.teams = this.fakeTeams; | ||||||
|  |     this.showExpandingRow = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   reset(numItems = 5000): void { | ||||||
|  |     this.showExpandingRow = false; | ||||||
|  | 
 | ||||||
|  |     this.fakeTeams = []; | ||||||
|  |     for (let i = 0; i < numItems; i++) { | ||||||
|  |       this.fakeTeams.push({ | ||||||
|  |         name: `name ${i}`, | ||||||
|  |         id: i, | ||||||
|  |         division: `division ${i}`, | ||||||
|  |         stadium: `stadium ${i}`, | ||||||
|  |         projection: `projection ${i}`, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {CommonModule} from '@angular/common'; | ||||||
|  | import {NgModule} from '@angular/core'; | ||||||
|  | import {ExpandingRowModule} from './expanding_row_module'; | ||||||
|  | 
 | ||||||
|  | import {BenchmarkableExpandingRow} from './benchmarkable_expanding_row'; | ||||||
|  | 
 | ||||||
|  | @NgModule({ | ||||||
|  |   declarations: [BenchmarkableExpandingRow], | ||||||
|  |   exports: [BenchmarkableExpandingRow], | ||||||
|  |   imports: [ | ||||||
|  |     CommonModule, | ||||||
|  |     ExpandingRowModule, | ||||||
|  |   ], | ||||||
|  | }) | ||||||
|  | export class BenchmarkableExpandingRowModule { | ||||||
|  | } | ||||||
|  |   | ||||||
							
								
								
									
										371
									
								
								modules/benchmarks/src/expanding_rows/expanding_row.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										371
									
								
								modules/benchmarks/src/expanding_rows/expanding_row.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,371 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, InjectionToken, Input, Output, QueryList, ViewChild} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRowSummary} from './expanding_row_summary'; | ||||||
|  | import {ExpandingRowToggleEvent} from './expanding_row_toggle_event'; | ||||||
|  | import { expanding_row_css } from './expanding_row_css'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Injection token to break cylic dependency between ExpandingRow and | ||||||
|  |  * ExpandingRowHost | ||||||
|  |  */ | ||||||
|  | export const EXPANDING_ROW_HOST_INJECTION_TOKEN = | ||||||
|  |     new InjectionToken<ExpandingRowHostBase>('ExpandingRowHost'); | ||||||
|  | 
 | ||||||
|  | /** The base class for ExpandingRowHost component to break cylic dependency. */ | ||||||
|  | export interface ExpandingRowHostBase { | ||||||
|  |   /** | ||||||
|  |    * A reference to all child cfc-expanding-row elements. We will need for | ||||||
|  |    * keyboard accessibility and scroll adjustments. For example, we need to know | ||||||
|  |    * which row is previous row when user presses "left arrow" on a focused row. | ||||||
|  |    */ | ||||||
|  |   contentRows: QueryList<ExpandingRow>; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Keeps track of the last row that had focus before focus left the list | ||||||
|  |    * of expanding rows. | ||||||
|  |    */ | ||||||
|  |   lastFocusedRow?: ExpandingRow; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles summary element click on a cfc-expanding-row component. Note | ||||||
|  |    * that summary element is visible only when the row is collapsed. So this | ||||||
|  |    * event will fired prior to expansion of a collapsed row. Scroll adjustment | ||||||
|  |    * below makes sure mouse stays on the caption element when the collapsed | ||||||
|  |    * row expands. | ||||||
|  |    */ | ||||||
|  |   handleRowSummaryClick(row: ExpandingRow): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Check if element is blacklisted.  Blacklisted elements will not collapse an | ||||||
|  |    * open row when clicked. | ||||||
|  |    */ | ||||||
|  |   isBlacklisted(element: HTMLElement|null): boolean; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles caption element click on a cfc-expanding-row component. Note | ||||||
|  |    * that caption element is visible only when the row is expanded. So this | ||||||
|  |    * means we will collapse the expanded row. The scroll adjustment below | ||||||
|  |    * makes sure that the mouse stays under the summary of the expanded row | ||||||
|  |    * when the row collapses. | ||||||
|  |    */ | ||||||
|  |   handleRowCaptionClick(row: ExpandingRow): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles expansion of a row. When a new row expands, we need to remove | ||||||
|  |    * previous expansion and collapse. We also need to save the currently | ||||||
|  |    * expanded row so that we can collapse this row once another row expands. | ||||||
|  |    */ | ||||||
|  |   handleRowExpand(row: ExpandingRow): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles focus on a row. When a new row gets focus (note that this is | ||||||
|  |    * different from expansion), we need to remove previous focus and expansion. | ||||||
|  |    * We need to save the reference to this focused row so that we can unfocus | ||||||
|  |    * this row when another row is focused. | ||||||
|  |    */ | ||||||
|  |   handleRowFocus(row: ExpandingRow): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Function that is called by expanding row summary to focus on the last | ||||||
|  |    * focusable element before the list of expanding rows. | ||||||
|  |    */ | ||||||
|  |   focusOnPreviousFocusableElement(): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Function that is called by expanding row summary to focus on the next | ||||||
|  |    * focusable element after the list of expanding rows. | ||||||
|  |    */ | ||||||
|  |   focusOnNextFocusableElement(): void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This component is used to render a single expanding row. It should contain | ||||||
|  |  * cfc-expanding-row-summary, cfc-expanding-row-details-caption and | ||||||
|  |  * cfc-expanding-row-details-content components. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |   selector: 'cfc-expanding-row', | ||||||
|  |   styles: [expanding_row_css], | ||||||
|  |   template: ` | ||||||
|  |   <div #expandingRowMainElement | ||||||
|  |        class="cfc-expanding-row" | ||||||
|  |        cdkMonitorSubtreeFocus | ||||||
|  |        [attr.tabindex]="isExpanded ? '0' : '-1'" | ||||||
|  |        [class.cfc-expanding-row-has-focus]="isFocused" | ||||||
|  |        [class.cfc-expanding-row-is-expanded]="isExpanded" | ||||||
|  |        ve="CfcExpandingRow"> | ||||||
|  |     <ng-content></ng-content> | ||||||
|  |   </div>`,
 | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class ExpandingRow { | ||||||
|  |   /** | ||||||
|  |    * The identifier for this node provided by the user code. We need this | ||||||
|  |    * while we are emitting onToggle event. | ||||||
|  |    */ | ||||||
|  |   @Input() rowId!: string; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * An ElementRef to the main element in this component. We need a reference | ||||||
|  |    * to this element to compute the height. The height of cfc-expanding-row | ||||||
|  |    * is used in [cfcExpandingRowHost] directive for scroll adjustments. | ||||||
|  |    */ | ||||||
|  |   @ViewChild('expandingRowMainElement', {static: true}) | ||||||
|  |   expandingRowMainElement!: ElementRef; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This @Output event emitter will be triggered when the user expands or | ||||||
|  |    * collapses this node. | ||||||
|  |    */ | ||||||
|  |   @Output() onToggle = new EventEmitter<ExpandingRowToggleEvent>(); | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A boolean indicating if this node is expanded. This value is used to | ||||||
|  |    * hide/show summary, caption, and content of the expanding row. There should | ||||||
|  |    * only be one expanded row within [cfcExpandingRowHost] directive. And if | ||||||
|  |    * there is an expanded row, there shouldn't be any focused rows. | ||||||
|  |    */ | ||||||
|  |   set isExpanded(value: boolean) { | ||||||
|  |     const changed: boolean = this.isExpandedInternal !== value; | ||||||
|  |     this.isExpandedInternal = value; | ||||||
|  | 
 | ||||||
|  |     if (changed) { | ||||||
|  |       this.isExpandedChange.emit(); | ||||||
|  |       this.changeDetectorRef.markForCheck(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** TS getter for isExpanded property. */ | ||||||
|  |   get isExpanded(): boolean { | ||||||
|  |     return this.isExpandedInternal; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Triggered when isExpanded property changes. */ | ||||||
|  |   isExpandedChange = new EventEmitter<void>(); | ||||||
|  | 
 | ||||||
|  |   /** Triggered when index property changes. */ | ||||||
|  |   indexChange = new EventEmitter<void>(); | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A boolean indicating if this node is focused. This value is used to add | ||||||
|  |    * a CSS class that should render a blue border on the right. There should | ||||||
|  |    * only be one focused row in [cfcExpandingRowHost] directive. | ||||||
|  |    */ | ||||||
|  |   set isFocused(value: boolean) { | ||||||
|  |     this.isFocusedInternal = value; | ||||||
|  |     this.changeDetectorRef.markForCheck(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** TS getter for isFocused property. */ | ||||||
|  |   get isFocused(): boolean { | ||||||
|  |     return this.isFocusedInternal; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** The index of the row in the context of the entire collection. */ | ||||||
|  |   set index(value: number) { | ||||||
|  |     const changed: boolean = this.indexInternal !== value; | ||||||
|  |     this.indexInternal = value; | ||||||
|  | 
 | ||||||
|  |     if (changed) { | ||||||
|  |       this.indexChange.emit(); | ||||||
|  |       this.changeDetectorRef.markForCheck(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** TS getter for index property. */ | ||||||
|  |   get index(): number { | ||||||
|  |     return this.indexInternal; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We should probably rename this to summaryContentChild. Because technically | ||||||
|  |    * this is not a @ViewChild that is in a template. This will be transcluded. | ||||||
|  |    * Note that we are not using @ContentChild directive here. The @ContentChild | ||||||
|  |    * will cause cyclic reference if the class definition for ExpandingRowSummary | ||||||
|  |    * component is not in the same file as ExpandingRow. | ||||||
|  |    */ | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   summaryViewChild!: ExpandingRowSummary; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We compute the collapsed height (which is just height of | ||||||
|  |    * cfc-expanding-row-summary component) in this component. This is used in | ||||||
|  |    * [cfcExpandingRowHost] for scroll adjustment calculation. | ||||||
|  |    */ | ||||||
|  |   collapsedHeight = -1; | ||||||
|  | 
 | ||||||
|  |   /** Internal storage for isExpanded public property. */ | ||||||
|  |   private isExpandedInternal = false; | ||||||
|  | 
 | ||||||
|  |   /** Internal storage for isFocused public property. */ | ||||||
|  |   private isFocusedInternal = false; | ||||||
|  | 
 | ||||||
|  |   /** Internal storage for index public property. */ | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   private indexInternal!: number; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This holds a reference to [cfcExpandingRowHost] directive. We need | ||||||
|  |    * this reference to notify the host when this row expands/collapses or is | ||||||
|  |    * focused. | ||||||
|  |    */ | ||||||
|  |   constructor( | ||||||
|  |       public elementRef: ElementRef, | ||||||
|  |       @Inject(EXPANDING_ROW_HOST_INJECTION_TOKEN) public expandingRowHost: | ||||||
|  |           ExpandingRowHostBase, | ||||||
|  |       private readonly changeDetectorRef: ChangeDetectorRef) {} | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles click on cfc-expanding-row-summary component. This will expand | ||||||
|  |    * this row and collapse the previously expanded row. The collapse & blur | ||||||
|  |    * is handled in [cfcExpandingRowHost] directive. | ||||||
|  |    */ | ||||||
|  |   handleSummaryClick(): void { | ||||||
|  |     this.collapsedHeight = this.elementRef.nativeElement | ||||||
|  |                                .querySelector('.cfc-expanding-row-summary') | ||||||
|  |                                .offsetHeight; | ||||||
|  |     this.expandingRowHost.handleRowSummaryClick(this); | ||||||
|  |     this.expand(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * When user tabs into child cfc-expanding-row-summary component. This method | ||||||
|  |    * will make sure we focuse on this row, and blur on previously focused row. | ||||||
|  |    */ | ||||||
|  |   handleSummaryFocus(): void { | ||||||
|  |     this.focus(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * cfc-expanding-row-details-caption component will call this function to | ||||||
|  |    * notify click on its host element. Note that caption is only shown when | ||||||
|  |    * the row is expanded. Hence this will collapse this row and put the focus | ||||||
|  |    * on it. | ||||||
|  |    * If a blacklisted element exists in the caption, clicking that element will | ||||||
|  |    * not trigger the row collapse. | ||||||
|  |    */ | ||||||
|  |   handleCaptionClick(event: MouseEvent): void { | ||||||
|  |     if (this.expandingRowHost.isBlacklisted( | ||||||
|  |             event.target as {} as HTMLElement)) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.expandingRowHost.handleRowCaptionClick(this); | ||||||
|  |     this.collapse(); | ||||||
|  |     this.focus(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets the height of this component. This height is used in parent | ||||||
|  |    * [cfcExpandingRowHost] directive to compute scroll adjustment. | ||||||
|  |    */ | ||||||
|  |   getHeight(): number { | ||||||
|  |     return this.expandingRowMainElement.nativeElement.offsetHeight; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Expands this row. This will notify the host so that it can collapse | ||||||
|  |    * previously expanded row. This function also emits onToggle @Output event | ||||||
|  |    * to the user code. | ||||||
|  |    */ | ||||||
|  |   expand(): void { | ||||||
|  |     this.isExpanded = true; | ||||||
|  |     this.expandingRowHost.handleRowExpand(this); | ||||||
|  | 
 | ||||||
|  |     // setTimeout here makes sure we scroll this row into view after animation.
 | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.expandingRowMainElement.nativeElement.focus(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.onToggle.emit({rowId: this.rowId, isExpand: true}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Collapses this row. Setting isExpanded to false will make sure we hide | ||||||
|  |    * the caption and details, and show cfc-expanding-row-summary component. | ||||||
|  |    * This also emits onToggle @Output event to the user code. | ||||||
|  |    */ | ||||||
|  |   collapse(): void { | ||||||
|  |     this.isExpanded = false; | ||||||
|  |     this.onToggle.emit({rowId: this.rowId, isExpand: false}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Blurs this row. This should remove the blue border on the left if there | ||||||
|  |    * is any. This function will remove DOM focus on the | ||||||
|  |    * cfc-expanding-row-summary | ||||||
|  |    * component. | ||||||
|  |    */ | ||||||
|  |   blur(): void { | ||||||
|  |     this.isFocused = false; | ||||||
|  |     this.summaryViewChild.blur(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Focuses this row. This should put blue border on the left. If there is | ||||||
|  |    * any previous focus/selection, those should be gone. Parent | ||||||
|  |    * [cfcExpandingRowHost] component takes care of that. | ||||||
|  |    */ | ||||||
|  |   focus(): void { | ||||||
|  |     this.isFocused = true; | ||||||
|  |     this.expandingRowHost.handleRowFocus(this); | ||||||
|  | 
 | ||||||
|  |     // Summary child is not present currently. We need to NG2 to update the
 | ||||||
|  |     // template.
 | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.summaryViewChild.focus(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We listen for TAB press here to make sure we trap the focus on the | ||||||
|  |    * expanded | ||||||
|  |    * row. If the row is not expanded, we don't care about this event since focus | ||||||
|  |    * trap should work for expanded rows only. | ||||||
|  |    */ | ||||||
|  |   @HostListener('keydown', ['$event']) | ||||||
|  |   handleKeyDown(event: KeyboardEvent) { | ||||||
|  |     const charCode = event.which || event.keyCode; | ||||||
|  | 
 | ||||||
|  |     switch (charCode) { | ||||||
|  |       case 9: | ||||||
|  |         if (!this.isExpanded) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.trapFocus(event); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * When this row is expanded, this function traps the focus between focusable | ||||||
|  |    * elements contained in this row. | ||||||
|  |    */ | ||||||
|  |   private trapFocus(event: KeyboardEvent): void { | ||||||
|  |     const rowElement: HTMLElement = this.expandingRowMainElement.nativeElement; | ||||||
|  |     const focusableEls: HTMLElement[] = []; | ||||||
|  |     let lastFocusableEl: HTMLElement = rowElement; | ||||||
|  | 
 | ||||||
|  |     if (focusableEls.length) { | ||||||
|  |       lastFocusableEl = focusableEls[focusableEls.length - 1]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (event.target === lastFocusableEl && !event.shiftKey) { | ||||||
|  |       rowElement.focus(); | ||||||
|  |       event.preventDefault(); | ||||||
|  |     } else if (event.target === rowElement && event.shiftKey) { | ||||||
|  |       lastFocusableEl.focus(); | ||||||
|  |       event.preventDefault(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {Directive} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This directive is used to flag an element to NOT trigger collapsing an | ||||||
|  |  * expanded row | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |   selector: '[cfcExpandingRowBlacklist]', | ||||||
|  | }) | ||||||
|  | export class ExpandingRowBlacklist {} | ||||||
							
								
								
									
										87
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_css.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_css.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | export const expanding_row_css = ` | ||||||
|  |   ::ng-deep [cfcExpandingRowHost] { | ||||||
|  |     display: block; | ||||||
|  |     margin-bottom: 2; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   :host(cfc-expanding-row), | ||||||
|  |   :host(cfc-expanding-row-summary), | ||||||
|  |   :host(cfc-expanding-row-details-caption), | ||||||
|  |   :host(cfc-expanding-row-details-content) { | ||||||
|  |     display: block; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row { | ||||||
|  |     background: white; | ||||||
|  |     border-top: 1 solid black; | ||||||
|  |     box-shadow: 0 1 1 gray; | ||||||
|  |     transition: margin 1 1; | ||||||
|  |     will-change: margin; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row.cfc-expanding-row-is-expanded { | ||||||
|  |     margin: 1 (-1); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row:focus { | ||||||
|  |     outline: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-summary { | ||||||
|  | 
 | ||||||
|  |     display: flex; | ||||||
|  |     border-left: 6 solid transparent; | ||||||
|  |     cursor: pointer; | ||||||
|  |     padding: 6 2; | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-summary:focus { | ||||||
|  |     outline: none; | ||||||
|  |     border-left-color: $cfc-color-active; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Adjust icons to be positioned correctly in the row.
 | ||||||
|  |   .cfc-expanding-row-summary::ng-deep cfc-icon { | ||||||
|  |     margin-top: 3; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-details-caption { | ||||||
|  |     display: flex; | ||||||
|  |     cursor: pointer; | ||||||
|  |     padding: 4 2; | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-details-caption::ng-deep a, | ||||||
|  |   .cfc-expanding-row-details-caption::ng-deep a:visited, | ||||||
|  |   .cfc-expanding-row-details-caption::ng-deep a .cfc-external-link-content { | ||||||
|  |     border-color: $cfc-color-text-primary-inverse; | ||||||
|  |     color: $cfc-color-text-primary-inverse; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Adjust icons to be positioned correctly in the row.
 | ||||||
|  |   ::ng-deep cfc-icon { | ||||||
|  |     margin-top: 3; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-details-content { | ||||||
|  |     padding: 2; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-details-content::ng-deep .ace-kv-list.cfc-full-bleed { | ||||||
|  |     width: 200px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   .cfc-expanding-row-accessibility-text { | ||||||
|  |     display: none; | ||||||
|  |   }`;
 | ||||||
| @ -0,0 +1,58 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnDestroy} from '@angular/core'; | ||||||
|  | import {Subject} from 'rxjs'; | ||||||
|  | import {takeUntil} from 'rxjs/operators'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRow} from './expanding_row'; | ||||||
|  | import { expanding_row_css } from './expanding_row_css'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This component should be within cfc-expanding-row component. The caption | ||||||
|  |  * is only visible when the row is expanded. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |   selector: 'cfc-expanding-row-details-caption', | ||||||
|  |   styles: [expanding_row_css], | ||||||
|  |   template: ` | ||||||
|  |     <div *ngIf="expandingRow.isExpanded" | ||||||
|  |         (click)="expandingRow.handleCaptionClick($event)" | ||||||
|  |         [style.backgroundColor]="color" | ||||||
|  |         class="cfc-expanding-row-details-caption"> | ||||||
|  |       <ng-content></ng-content> | ||||||
|  |     </div>`,
 | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class ExpandingRowDetailsCaption implements OnDestroy { | ||||||
|  |   /** The background color of this component. */ | ||||||
|  |   @Input() color: string = 'blue'; | ||||||
|  | 
 | ||||||
|  |   /** This is triggered when this component is destroyed. */ | ||||||
|  |   private readonly onDestroy = new Subject(); | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We need a reference to parent cfc-expanding-row component here to hide | ||||||
|  |    * this component when the row is collapsed. We also need to relay clicks | ||||||
|  |    * to the parent component. | ||||||
|  |    */ | ||||||
|  |   constructor( | ||||||
|  |       @Host() public expandingRow: ExpandingRow, | ||||||
|  |       changeDetectorRef: ChangeDetectorRef) { | ||||||
|  |     this.expandingRow.isExpandedChange.pipe(takeUntil(this.onDestroy)) | ||||||
|  |         .subscribe(() => { | ||||||
|  |           changeDetectorRef.markForCheck(); | ||||||
|  |         }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** When component is destroyed, unlisten to isExpanded. */ | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.onDestroy.next(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,50 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, OnDestroy} from '@angular/core'; | ||||||
|  | import {Subscription} from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRow} from './expanding_row'; | ||||||
|  | import { expanding_row_css } from './expanding_row_css'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This component should be within cfc-expanding-row component. Note that the | ||||||
|  |  * content is visible only when the row is expanded. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |   styles: [expanding_row_css], | ||||||
|  |   selector: 'cfc-expanding-row-details-content', | ||||||
|  |   template: ` | ||||||
|  |     <div class="cfc-expanding-row-details-content" | ||||||
|  |         *ngIf="expandingRow.isExpanded"> | ||||||
|  |       <ng-content></ng-content> | ||||||
|  |     </div>`,
 | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class ExpandingRowDetailsContent implements OnDestroy { | ||||||
|  |   /** Used for unsubscribing to changes in isExpanded parent property. */ | ||||||
|  |   private isExpandedChangeSubscription: Subscription; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We need a reference to parent cfc-expanding-row component to make sure we | ||||||
|  |    * hide this component if the row is collapsed. | ||||||
|  |    */ | ||||||
|  |   constructor( | ||||||
|  |       @Host() public expandingRow: ExpandingRow, | ||||||
|  |       changeDetectorRef: ChangeDetectorRef) { | ||||||
|  |     this.isExpandedChangeSubscription = | ||||||
|  |         this.expandingRow.isExpandedChange.subscribe(() => { | ||||||
|  |           changeDetectorRef.markForCheck(); | ||||||
|  |         }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Unsubscribe from changes in parent isExpanded property. */ | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.isExpandedChangeSubscription.unsubscribe(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										519
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_host.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_host.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,519 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {AfterContentInit, AfterViewInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, HostListener, Input, OnDestroy, Output, QueryList, ViewChild} from '@angular/core'; | ||||||
|  | import {Subscription} from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | import {EXPANDING_ROW_HOST_INJECTION_TOKEN, ExpandingRow, ExpandingRowHostBase} from './expanding_row'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * We use this class in <cfc-expanding-row/> template to identify the row. | ||||||
|  |  * The [cfcExpandingRowHost] directive also uses this class to check if a given | ||||||
|  |  * HTMLElement is within an <cfc-expanding-row/>. | ||||||
|  |  */ | ||||||
|  | const EXPANDING_ROW_CLASS_NAME = 'cfc-expanding-row'; | ||||||
|  | 
 | ||||||
|  | /** Throttle duration in milliseconds for repeated key presses. */ | ||||||
|  | export const EXPANDING_ROW_KEYPRESS_THORTTLE_MS = 50; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This type union is created to make arguments of handleUpOrDownPress* | ||||||
|  |  * methods in ExpandingRowHost class more readable. | ||||||
|  |  */ | ||||||
|  | type UpOrDown = 'up'|'down'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This is the wrapper directive for the cfc-expanding-row components. Note that | ||||||
|  |  * we wanted to make this a directive instead of component because child | ||||||
|  |  * cfc-expanding-row components does not have to be a direct child. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |   selector: 'cfc-expanding-row-host', | ||||||
|  |   template: ` | ||||||
|  |     <div #firstFocusable | ||||||
|  |       (focus)="focusOnLastFocusedRow()" | ||||||
|  |       tabindex="0"> | ||||||
|  |     </div> | ||||||
|  |     <ng-content></ng-content> | ||||||
|  |     <div #lastFocusable | ||||||
|  |       (focus)="focusOnLastFocusedRow()" | ||||||
|  |       tabindex="0"> | ||||||
|  |     </div>`,
 | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  |   providers: [ | ||||||
|  |     {provide: EXPANDING_ROW_HOST_INJECTION_TOKEN, useExisting: ExpandingRowHost} | ||||||
|  |   ], | ||||||
|  | }) | ||||||
|  | export class ExpandingRowHost implements AfterViewInit, OnDestroy, | ||||||
|  |                                          ExpandingRowHostBase { | ||||||
|  |   /** | ||||||
|  |    * An HTML selector (e.g. "body") for the scroll element. We need this to | ||||||
|  |    * make some scroll adjustments. | ||||||
|  |    */ | ||||||
|  |   @Input() scrollElementSelector = '.cfc-panel-body-scrollable'; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * An HTML selector (e.g. "body") for the click root. While the row is | ||||||
|  |    * expanded, and user clicks outside of the expanded row, we collapse this row | ||||||
|  |    * But to do this, we need to know the clickable area. | ||||||
|  |    */ | ||||||
|  |   @Input() clickRootElementSelector = 'cfc-panel-body'; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * The @Output will be triggered when the user wants to focus on the | ||||||
|  |    * previously expanded row, and we are already at the first row. The logs team | ||||||
|  |    * will use this to prepend data on demand. | ||||||
|  |    */ | ||||||
|  |   @Output() onPrepend = new EventEmitter<void>(); | ||||||
|  | 
 | ||||||
|  |   /** A reference to the last focusable element in list of expanding rows. */ | ||||||
|  |   @ViewChild('lastFocusable', {static: true}) lastFocusableElement!: ElementRef; | ||||||
|  | 
 | ||||||
|  |   /** A reference to the first focusable element in list of expanding rows. */ | ||||||
|  |   @ViewChild('firstFocusable', {static: true}) | ||||||
|  |   firstFocusableElement!: ElementRef; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A reference to all child cfc-expanding-row elements. We will need for | ||||||
|  |    * keyboard accessibility and scroll adjustments. For example, we need to know | ||||||
|  |    * which row is previous row when user presses "left arrow" on a focused row. | ||||||
|  |    */ | ||||||
|  |   @ContentChildren(forwardRef(() => ExpandingRow), {descendants: true}) | ||||||
|  |   contentRows!: QueryList<ExpandingRow>; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Keeps track of the last row that had focus before focus left the list | ||||||
|  |    * of expanding rows. | ||||||
|  |    */ | ||||||
|  |   lastFocusedRow?: ExpandingRow = undefined; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Focused rows just show a blue left border. This node is not expanded. We | ||||||
|  |    * need to keep a reference to the focused row to unfocus when another row | ||||||
|  |    * is focused. | ||||||
|  |    */ | ||||||
|  |   private focusedRow?: ExpandingRow = undefined; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This is the expanded row. If there is an expanded row there shouldn't be | ||||||
|  |    * any focused rows. We need a reference to this. For example we need to | ||||||
|  |    * collapse the currently expanded row, if another row is expanded. | ||||||
|  |    */ | ||||||
|  |   private expandedRow?: ExpandingRow = undefined; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This is just handleRootMouseUp.bind(this). handleRootMouseUp handles | ||||||
|  |    * click events on root element (defined by clickRootElementSelector @Input) | ||||||
|  |    * Since we attach the click listener dynamically, we need to keep this | ||||||
|  |    * function around. This enables us to detach the click listener when | ||||||
|  |    * component is destroyed. | ||||||
|  |    */ | ||||||
|  |   private handleRootMouseUpBound: EventListenerObject = | ||||||
|  |       this.handleRootMouseUp.bind(this); | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 16px is the margin animation we have on cfc-expanding-row component. | ||||||
|  |    * We need this value to compute scroll adjustments. | ||||||
|  |    */ | ||||||
|  |   private static rowMargin = 16; | ||||||
|  | 
 | ||||||
|  |   /** Subscription to changes in the expanding rows. */ | ||||||
|  |   // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
 | ||||||
|  |   private rowChangeSubscription!: Subscription; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * When component initializes we need to attach click listener to the root | ||||||
|  |    * element. This click listener will allows us to collapse the | ||||||
|  |    * currently expanded row when user clicks outside of it. | ||||||
|  |    */ | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     const clickRootElement: HTMLElement = this.getClickRootElement(); | ||||||
|  | 
 | ||||||
|  |     if (!clickRootElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     clickRootElement.addEventListener('mouseup', this.handleRootMouseUpBound); | ||||||
|  | 
 | ||||||
|  |     this.rowChangeSubscription = this.contentRows.changes.subscribe(() => { | ||||||
|  |       this.recalcRowIndexes(); | ||||||
|  |     }); | ||||||
|  |     this.recalcRowIndexes(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Detaches the click listener on the root element. Note that we are attaching | ||||||
|  |    * this listener on ngAfterViewInit function. | ||||||
|  |    */ | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     const clickRootElement: HTMLElement = this.getClickRootElement(); | ||||||
|  | 
 | ||||||
|  |     if (!clickRootElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     clickRootElement.removeEventListener( | ||||||
|  |         'mouseup', this.handleRootMouseUpBound); | ||||||
|  | 
 | ||||||
|  |     if (this.rowChangeSubscription) { | ||||||
|  |       this.rowChangeSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles caption element click on a cfc-expanding-row component. Note | ||||||
|  |    * that caption element is visible only when the row is expanded. So this | ||||||
|  |    * means we will collapse the expanded row. The scroll adjustment below | ||||||
|  |    * makes sure that the mouse stays under the summary of the expanded row | ||||||
|  |    * when the row collapses. | ||||||
|  |    */ | ||||||
|  |   handleRowCaptionClick(row: ExpandingRow): void { | ||||||
|  |     const scrollAdjustment: number = -ExpandingRowHost.rowMargin; | ||||||
|  |     const scrollElement: HTMLElement = this.getScrollElement() as HTMLElement; | ||||||
|  |     if (!scrollElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     scrollElement.scrollTop += scrollAdjustment; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles summary element click on a cfc-expanding-row component. Note | ||||||
|  |    * that summary element is visible only when the row is collapsed. So this | ||||||
|  |    * event will fired prior to expansion of a collapsed row. Scroll adjustment | ||||||
|  |    * below makes sure mouse stays on the caption element when the collapsed | ||||||
|  |    * row expands. | ||||||
|  |    */ | ||||||
|  |   handleRowSummaryClick(row: ExpandingRow): void { | ||||||
|  |     const hadPreviousSelection: boolean = !!this.expandedRow; | ||||||
|  |     const previousSelectedRowIndex: number = | ||||||
|  |         this.getRowIndex(this.expandedRow as ExpandingRow); | ||||||
|  |     const newSelectedRowIndex: number = this.getRowIndex(row); | ||||||
|  |     const previousCollapsedHeight: number = | ||||||
|  |         this.getSelectedRowCollapsedHeight(); | ||||||
|  |     const previousExpansionHeight = this.getSelectedRowExpandedHeight(); | ||||||
|  | 
 | ||||||
|  |     if (this.expandedRow) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let scrollAdjustment = 0; | ||||||
|  |     const scrollElement: HTMLElement = this.getScrollElement() as HTMLElement; | ||||||
|  |     if (!scrollElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (previousExpansionHeight > 0 && previousCollapsedHeight >= 0) { | ||||||
|  |       scrollAdjustment = previousExpansionHeight - previousCollapsedHeight; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const newSelectionIsInfrontOfPrevious: boolean = | ||||||
|  |         newSelectedRowIndex > previousSelectedRowIndex; | ||||||
|  |     const multiplier = newSelectionIsInfrontOfPrevious ? -1 : 0; | ||||||
|  |     scrollAdjustment = | ||||||
|  |         scrollAdjustment * multiplier + ExpandingRowHost.rowMargin; | ||||||
|  | 
 | ||||||
|  |     scrollElement.scrollTop += scrollAdjustment; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles expansion of a row. When a new row expands, we need to remove | ||||||
|  |    * previous expansion and collapse. We also need to save the currently | ||||||
|  |    * expanded row so that we can collapse this row once another row expands. | ||||||
|  |    */ | ||||||
|  |   handleRowExpand(row: ExpandingRow): void { | ||||||
|  |     this.removePreviousFocus(); | ||||||
|  |     this.removePreviousExpansion(); | ||||||
|  |     this.expandedRow = row; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles focus on a row. When a new row gets focus (note that this is | ||||||
|  |    * different from expansion), we need to remove previous focus and expansion. | ||||||
|  |    * We need to save the reference to this focused row so that we can unfocus | ||||||
|  |    * this row when another row is focused. | ||||||
|  |    */ | ||||||
|  |   handleRowFocus(row: ExpandingRow): void { | ||||||
|  |     // Do not blur then refocus the row if it's already selected.
 | ||||||
|  |     if (row === this.focusedRow) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.removePreviousFocus(); | ||||||
|  |     this.removePreviousExpansion(); | ||||||
|  |     this.focusedRow = row; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Called when shift+tabbing from the first focusable element after the list | ||||||
|  |    * of expanding rows or tabbing from the last focusable element before. | ||||||
|  |    */ | ||||||
|  |   focusOnLastFocusedRow(): void { | ||||||
|  |     if (!this.lastFocusedRow) { | ||||||
|  |       this.lastFocusedRow = this.contentRows.toArray()[0]; | ||||||
|  |     } | ||||||
|  |     this.lastFocusedRow.focus(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Function that is called by expanding row summary to focus on the last | ||||||
|  |    * focusable element before the list of expanding rows. | ||||||
|  |    */ | ||||||
|  |   focusOnPreviousFocusableElement(): void { | ||||||
|  |     this.lastFocusedRow = this.focusedRow; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Function that is called by expanding row summary to focus on the next | ||||||
|  |    * focusable element after the list of expanding rows. | ||||||
|  |    */ | ||||||
|  |   focusOnNextFocusableElement(): void { | ||||||
|  |     this.lastFocusedRow = this.focusedRow; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles keydown event on the host. We are just concerned with up, | ||||||
|  |    * down arrow, ESC, and ENTER presses here. Note that Up/Down presses | ||||||
|  |    * can be repeated. | ||||||
|  |    * | ||||||
|  |    * - Up: Focuses on the row above. | ||||||
|  |    * - Down: Focuses on the row below. | ||||||
|  |    * - Escape: Collapses the expanded row. | ||||||
|  |    * - Enter: Expands the focused row. | ||||||
|  |    */ | ||||||
|  |   @HostListener('keydown', ['$event']) | ||||||
|  |   handleKeyDown(event: KeyboardEvent) { | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Recursively returns true if target HTMLElement is within a | ||||||
|  |    * cfc-expanding-row component. It will return false otherwise. | ||||||
|  |    * We need this function in handleRootMouseUp to collapse the expanded row | ||||||
|  |    * when user clicks outside of all expanded rows. | ||||||
|  |    */ | ||||||
|  |   private isTargetInRow(target: HTMLElement): boolean { | ||||||
|  |     return target.classList.contains(EXPANDING_ROW_CLASS_NAME); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets the click root element that is described by clickRootElementSelector | ||||||
|  |    * @Input value. | ||||||
|  |    */ | ||||||
|  |   private getClickRootElement(): HTMLElement { | ||||||
|  |     return document.querySelector(this.clickRootElementSelector) as HTMLElement; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles all of the mouseup events on the click root. When user clicks | ||||||
|  |    * outside of an expanded row, we need to collapse that row. | ||||||
|  |    * We trigger collapse by calling handleCaptionClick() on the expanded row. | ||||||
|  |    */ | ||||||
|  |   private handleRootMouseUp(event: MouseEvent): void { | ||||||
|  |     if (!this.expandedRow) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this.isTargetInRow(event.target as {} as HTMLElement)) { | ||||||
|  |       this.expandedRow.handleCaptionClick(event); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Check if element is blacklisted.  Blacklisted elements will not collapse an | ||||||
|  |    * open row when clicked. | ||||||
|  |    */ | ||||||
|  |   isBlacklisted(element: HTMLElement|null): boolean { | ||||||
|  |     const clickRoot = this.getClickRootElement(); | ||||||
|  |     while (element && element !== clickRoot) { | ||||||
|  |       if (element.hasAttribute('cfcexpandingrowblacklist')) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       element = element.parentElement; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes focus state from a previously focused row. We blur this row and | ||||||
|  |    * set the focusedRow to undefined in this method. This usually happens when | ||||||
|  |    * another row is focused. | ||||||
|  |    */ | ||||||
|  |   private removePreviousFocus(): void { | ||||||
|  |     if (this.focusedRow) { | ||||||
|  |       this.focusedRow.blur(); | ||||||
|  |       this.focusedRow = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes the expanded state from a previously expanded row. We collapse this | ||||||
|  |    * row and set the expandedRow to undefined in this method. This usually | ||||||
|  |    * happens when another row is expanded. | ||||||
|  |    */ | ||||||
|  |   private removePreviousExpansion(): void { | ||||||
|  |     if (this.expandedRow) { | ||||||
|  |       this.expandedRow.collapse(); | ||||||
|  |       this.expandedRow = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets the collapsed height of the currently expanded row. We need this for | ||||||
|  |    * scroll adjustments. Note that collapsed height of a cfc-expanding-row | ||||||
|  |    * component is equal to height of cfc-expanding-row-summary component within | ||||||
|  |    * the row. | ||||||
|  |    */ | ||||||
|  |   private getSelectedRowCollapsedHeight(): number { | ||||||
|  |     if (this.expandedRow) { | ||||||
|  |       return this.expandedRow.collapsedHeight; | ||||||
|  |     } else { | ||||||
|  |       return -1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets the current height of the expanded row. We need this value for the | ||||||
|  |    * scroll adjustment computation. | ||||||
|  |    */ | ||||||
|  |   private getSelectedRowExpandedHeight(): number { | ||||||
|  |     if (this.expandedRow) { | ||||||
|  |       return this.expandedRow.getHeight(); | ||||||
|  |     } else { | ||||||
|  |       return -1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets the HTML element described by scrollElementSelector @Input value. | ||||||
|  |    * We need this value for scroll adjustments. | ||||||
|  |    */ | ||||||
|  |   private getScrollElement(): HTMLElement|undefined { | ||||||
|  |     if (!this.scrollElementSelector) { | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return document.querySelector(this.scrollElementSelector) as HTMLElement; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles escape presses on the host element. Escape removes previous focus | ||||||
|  |    * if there is one. If there is an expanded row, escape row collapses this | ||||||
|  |    * row and focuses on it. A subsequent escape press will blur this row. | ||||||
|  |    */ | ||||||
|  |   private handleEscapePress(): void { | ||||||
|  |     this.removePreviousFocus(); | ||||||
|  | 
 | ||||||
|  |     if (this.expandedRow) { | ||||||
|  |       this.expandedRow.collapse(); | ||||||
|  |       this.expandedRow.focus(); | ||||||
|  |       this.expandedRow = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles enter keypress. If there is a focused row, an enter key press on | ||||||
|  |    * host element will expand this row. | ||||||
|  |    */ | ||||||
|  |   private handleEnterPress(): void { | ||||||
|  |     if (document.activeElement !== this.focusedRowSummary()) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this.focusedRow) { | ||||||
|  |       this.focusedRow.expand(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Returns the HTMLElement that is the currently focused row summary. */ | ||||||
|  |   private focusedRowSummary(): HTMLElement|undefined { | ||||||
|  |     return this.focusedRow ? | ||||||
|  |         this.focusedRow.summaryViewChild.mainElementRef.nativeElement : | ||||||
|  |         undefined; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the index of a given row. This enables us to figure out the row | ||||||
|  |    * above/below the focused row. | ||||||
|  |    */ | ||||||
|  |   private getRowIndex(rowToLookFor: ExpandingRow): number { | ||||||
|  |     return rowToLookFor ? rowToLookFor.index : -1; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles up/down arrow presses on the host element. Up arrow press will | ||||||
|  |    * focus/expand on the row above. Down arrow press will focus/expand the row | ||||||
|  |    * below. If we have a focus on the current row, this function will focus on | ||||||
|  |    * the computed (the one above or below) row. If host has an expanded row, | ||||||
|  |    * this function will expand the computed row. | ||||||
|  |    */ | ||||||
|  |   private handleUpOrDownPressOnce(upOrDown: UpOrDown, event: KeyboardEvent): | ||||||
|  |       void { | ||||||
|  |     event.preventDefault(); | ||||||
|  | 
 | ||||||
|  |     // If row is expanded but focus is inside the expanded element, arrow
 | ||||||
|  |     // key presses should not do anything.
 | ||||||
|  |     if (this.expandedRow && | ||||||
|  |         document.activeElement !== | ||||||
|  |             this.expandedRow.expandingRowMainElement.nativeElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If focus is inside a collapsed row header, arrow key presses should not
 | ||||||
|  |     // do anything.
 | ||||||
|  |     if (this.focusedRow && | ||||||
|  |         document.activeElement !== this.focusedRowSummary()) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     // We only want screen reader to read the message the first time we enter
 | ||||||
|  |     // the list of expanding rows, so we must reset the variable here
 | ||||||
|  |     this.lastFocusedRow = undefined; | ||||||
|  | 
 | ||||||
|  |     const rowToLookFor: ExpandingRow|undefined = | ||||||
|  |         this.expandedRow || this.focusedRow; | ||||||
|  |     if (!rowToLookFor) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const isFocus: boolean = (rowToLookFor === this.focusedRow); | ||||||
|  | 
 | ||||||
|  |     const rowIndex: number = this.getRowIndex(rowToLookFor); | ||||||
|  |     const contentRowsArray: ExpandingRow[] = this.contentRows.toArray(); | ||||||
|  | 
 | ||||||
|  |     if (rowIndex < 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const potentialIndex: number = (upOrDown === 'up' ? -1 : +1) + rowIndex; | ||||||
|  |     if (potentialIndex < 0) { | ||||||
|  |       this.onPrepend.emit(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (potentialIndex >= contentRowsArray.length) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const potentialRow: ExpandingRow = contentRowsArray[potentialIndex]; | ||||||
|  |     if (isFocus) { | ||||||
|  |       potentialRow.focus(); | ||||||
|  |     } else { | ||||||
|  |       potentialRow.expand(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Updates all of the rows with their new index.
 | ||||||
|  |   private recalcRowIndexes() { | ||||||
|  |     let index = 0; | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.contentRows.forEach((row: ExpandingRow) => { | ||||||
|  |         row.index = index++; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -0,0 +1,42 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {CommonModule} from '@angular/common'; | ||||||
|  | import {NgModule} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRow} from './expanding_row'; | ||||||
|  | import {ExpandingRowBlacklist} from './expanding_row_blacklist'; | ||||||
|  | import {ExpandingRowDetailsCaption} from './expanding_row_details_caption'; | ||||||
|  | import {ExpandingRowDetailsContent} from './expanding_row_details_content'; | ||||||
|  | import {ExpandingRowHost} from './expanding_row_host'; | ||||||
|  | import {ExpandingRowSummary} from './expanding_row_summary'; | ||||||
|  | 
 | ||||||
|  | /** The main module for the cfc-expanding-row component. */ | ||||||
|  | @NgModule({ | ||||||
|  |   declarations: [ | ||||||
|  |     ExpandingRow, | ||||||
|  |     ExpandingRowDetailsCaption, | ||||||
|  |     ExpandingRowDetailsContent, | ||||||
|  |     ExpandingRowHost, | ||||||
|  |     ExpandingRowSummary, | ||||||
|  |     ExpandingRowBlacklist, | ||||||
|  |   ], | ||||||
|  |   exports: [ | ||||||
|  |     ExpandingRow, | ||||||
|  |     ExpandingRowDetailsCaption, | ||||||
|  |     ExpandingRowDetailsContent, | ||||||
|  |     ExpandingRowHost, | ||||||
|  |     ExpandingRowSummary, | ||||||
|  |     ExpandingRowBlacklist, | ||||||
|  |   ], | ||||||
|  |   imports: [ | ||||||
|  |     CommonModule, | ||||||
|  |   ], | ||||||
|  | }) | ||||||
|  | export class ExpandingRowModule { | ||||||
|  | } | ||||||
							
								
								
									
										219
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_summary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								modules/benchmarks/src/expanding_rows/expanding_row_summary.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Host, HostListener, OnDestroy, ViewChild} from '@angular/core'; | ||||||
|  | import {Subscription} from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRow} from './expanding_row'; | ||||||
|  | import { expanding_row_css } from './expanding_row_css'; | ||||||
|  | 
 | ||||||
|  | const KEY_CODE_TAB = 9; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This component should be used within cfc-expanding-row component. Note that | ||||||
|  |  * summary is visible only when the row is collapsed. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |   selector: 'cfc-expanding-row-summary', | ||||||
|  |   styles: [expanding_row_css], | ||||||
|  |   template: ` | ||||||
|  |     <div *ngIf="!expandingRow.isExpanded" | ||||||
|  |       #expandingRowSummaryMainElement | ||||||
|  |       class="cfc-expanding-row-summary" | ||||||
|  |       tabindex="-1" | ||||||
|  |       (click)="expandingRow.handleSummaryClick()" | ||||||
|  |       (focus)="handleFocus()"> | ||||||
|  |       <ng-content></ng-content> | ||||||
|  |       <div class="cfc-expanding-row-accessibility-text">.</div> | ||||||
|  |       <div class="cfc-expanding-row-accessibility-text" | ||||||
|  |           i18n="This is the label used to indicate that the user is in a list of expanding rows."> | ||||||
|  |         Row {{expandingRow.index + 1}} in list of expanding rows. | ||||||
|  |       </div> | ||||||
|  |       <div *ngIf="isPreviouslyFocusedRow()" | ||||||
|  |           class="cfc-expanding-row-accessibility-text" | ||||||
|  |           i18n="This is the label used for the first row in list of expanding rows."> | ||||||
|  |         Use arrow keys to navigate. | ||||||
|  |       </div> | ||||||
|  |   </div>`,
 | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class ExpandingRowSummary implements OnDestroy { | ||||||
|  |   /** | ||||||
|  |    * A reference to the main element. This element should be focusable. We need | ||||||
|  |    * reference to compute collapsed height of the row. We also use this | ||||||
|  |    * reference for focus and blur methods below. | ||||||
|  |    */ | ||||||
|  |   @ViewChild('expandingRowSummaryMainElement', {static: false}) | ||||||
|  |   mainElementRef!: ElementRef; | ||||||
|  | 
 | ||||||
|  |   /** Subscription for changes in parent isExpanded property. */ | ||||||
|  |   private isExpandedSubscription: Subscription; | ||||||
|  | 
 | ||||||
|  |   /** Subscription for changes in parent index property. */ | ||||||
|  |   private indexSubscription: Subscription; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * We need the parent cfc-expanding-row component here to hide this element | ||||||
|  |    * when the row is expanded. cfc-expanding-row-details-caption element | ||||||
|  |    * will act as a header for expanded rows. We also need to relay tab-in and | ||||||
|  |    * click events to the parent. | ||||||
|  |    */ | ||||||
|  |   constructor( | ||||||
|  |       @Host() public expandingRow: ExpandingRow, | ||||||
|  |       changeDetectorRef: ChangeDetectorRef) { | ||||||
|  |     this.expandingRow.summaryViewChild = this; | ||||||
|  |     this.isExpandedSubscription = | ||||||
|  |         this.expandingRow.isExpandedChange.subscribe(() => { | ||||||
|  |           changeDetectorRef.markForCheck(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     this.indexSubscription = this.expandingRow.indexChange.subscribe(() => { | ||||||
|  |       changeDetectorRef.markForCheck(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   /** When component is destroyed, unlisten to isExpanded. */ | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     if (this.isExpandedSubscription) { | ||||||
|  |       this.isExpandedSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     if (this.indexSubscription) { | ||||||
|  |       this.indexSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles focus event on the element. We basically want to detect any focus | ||||||
|  |    * in this component and relay this information to parent cfc-expanding-row | ||||||
|  |    * component. | ||||||
|  |    */ | ||||||
|  |   handleFocus(): void { | ||||||
|  |     // Clicking causes a focus event to occur before the click event. Filter
 | ||||||
|  |     // out click events using the cdkFocusMonitor.
 | ||||||
|  |     //
 | ||||||
|  |     // TODO(b/62385992) Use the KeyboardFocusService to detect focus cause
 | ||||||
|  |     // instead of creating multiple monitors on a page.
 | ||||||
|  |     if (this.expandingRow.expandingRowMainElement.nativeElement.classList | ||||||
|  |             .contains('cdk-mouse-focused')) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this.expandingRow.isFocused && !this.expandingRow.isExpanded) { | ||||||
|  |       this.expandingRow.handleSummaryFocus(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles tab & shift+tab presses on expanding row summaries in case there | ||||||
|  |    * are tabbable elements inside the summaries. | ||||||
|  |    */ | ||||||
|  |   @HostListener('keydown', ['$event']) | ||||||
|  |   handleKeyDown(event: KeyboardEvent) { | ||||||
|  |     const charCode = event.which || event.keyCode; | ||||||
|  |     if (charCode === KEY_CODE_TAB) { | ||||||
|  |       this.handleTabKeypress(event); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Handles tab and shift+tab presses inside expanding row summaries; | ||||||
|  |    * | ||||||
|  |    * From inside collapsed row summary: | ||||||
|  |    * - Tab: If focus was on the last focusable child, should shift focus to | ||||||
|  |    *        the next focusable element outside the list of expanding rows. | ||||||
|  |    * - Shift+tab: If focus was on first focusable child, should shift focus to | ||||||
|  |    *              the main collapsed row summary element | ||||||
|  |    *              If focus was on main collapsed row summary element, should | ||||||
|  |    *              shift focus to the last focusable element before the list of | ||||||
|  |    *              expanding rows. | ||||||
|  |    */ | ||||||
|  |   handleTabKeypress(event: KeyboardEvent): void { | ||||||
|  |     const focusableChildren = this.getFocusableChildren(); | ||||||
|  | 
 | ||||||
|  |     if (focusableChildren.length === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Shift+tab on expanding row summary should focus on last focusable element
 | ||||||
|  |     // before expanding row list. Otherwise, if shift+tab is pressed on first
 | ||||||
|  |     // focusable child inside expanding row summary, it should focus on main
 | ||||||
|  |     // expanding row summary element.
 | ||||||
|  |     if (event.shiftKey && | ||||||
|  |         document.activeElement === this.mainElementRef.nativeElement) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.expandingRow.expandingRowHost.focusOnPreviousFocusableElement(); | ||||||
|  |       return; | ||||||
|  |     } else if ( | ||||||
|  |         event.shiftKey && document.activeElement === focusableChildren[0]) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.expandingRow.focus(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If tab is pressed on the last focusable element inside an expanding row
 | ||||||
|  |     // summary, focus should be set to the next focusable element after the list
 | ||||||
|  |     // of expanding rows.
 | ||||||
|  |     if (!event.shiftKey && | ||||||
|  |         document.activeElement === | ||||||
|  |             focusableChildren[focusableChildren.length - 1]) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.expandingRow.expandingRowHost.focusOnNextFocusableElement(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Finds the row that had focus before focus left the list of expanding rows | ||||||
|  |    * and checks if the current row summary is that row. | ||||||
|  |    */ | ||||||
|  |   isPreviouslyFocusedRow(): boolean { | ||||||
|  |     if (!this.expandingRow.expandingRowHost.contentRows) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const expandingRowHost = this.expandingRow.expandingRowHost; | ||||||
|  | 
 | ||||||
|  |     if (!this.mainElementRef || !expandingRowHost.lastFocusedRow) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!expandingRowHost.lastFocusedRow.summaryViewChild.mainElementRef) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If the current expanding row summary was the last focused one before
 | ||||||
|  |     // focus exited the list, then return true to trigger the screen reader
 | ||||||
|  |     if (this.mainElementRef.nativeElement === | ||||||
|  |         expandingRowHost.lastFocusedRow.summaryViewChild.mainElementRef | ||||||
|  |             .nativeElement) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Puts the DOM focus on the main element. */ | ||||||
|  |   focus(): void { | ||||||
|  |     if (this.mainElementRef && | ||||||
|  |         document.activeElement !== this.mainElementRef.nativeElement) { | ||||||
|  |       this.mainElementRef.nativeElement.focus(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Removes the DOM focus on the main element. */ | ||||||
|  |   blur(): void { | ||||||
|  |     if (!this.mainElementRef) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.mainElementRef.nativeElement.blur(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Returns array of focusable elements within this component. */ | ||||||
|  |   private getFocusableChildren(): HTMLElement[] { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This interface is used to send toggle (expand/collapse) events to the user | ||||||
|  |  * code. | ||||||
|  |  */ | ||||||
|  | export interface ExpandingRowToggleEvent { | ||||||
|  |   /** The identifier of the row that was toggled. */ | ||||||
|  |   rowId: string; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A boolean indicating whether or not this row was expanded. This is set to | ||||||
|  |    * false if the row was collapsed. | ||||||
|  |    */ | ||||||
|  |   isExpand: boolean; | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								modules/benchmarks/src/expanding_rows/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								modules/benchmarks/src/expanding_rows/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  | 
 | ||||||
|  | <head> | ||||||
|  |   <!-- Prevent the browser from requesting any favicon. --> | ||||||
|  |   <link rel="icon" href="data:,"> | ||||||
|  | </head> | ||||||
|  | 
 | ||||||
|  | <body> | ||||||
|  | 
 | ||||||
|  |   <h1>Change Detection Benchmark</h1> | ||||||
|  |   <div id="rendererMode">...</div> | ||||||
|  | 
 | ||||||
|  |   <benchmark-root>loading...</benchmark-root> | ||||||
|  | 
 | ||||||
|  |   <script> | ||||||
|  |     addEventListener('DOMContentLoaded', () => { | ||||||
|  |       window.ngDevMode = location.hash == '#ngDevMode'; | ||||||
|  |       // DevServer has automatic bootstrap code, so if we already have <scripts> than we don't need to bootstrap | ||||||
|  |       var alreadyBootstraped = document.querySelectorAll('script').length > 1; // 1 for ourselves | ||||||
|  | 
 | ||||||
|  |       if (!alreadyBootstraped) { | ||||||
|  |         function loadScript(url) { | ||||||
|  |           var script = document.createElement('script'); | ||||||
|  |           script.src = url; | ||||||
|  |           document.body.append(script); | ||||||
|  |         } | ||||||
|  |         loadScript('/npm/node_modules/zone.js/dist/zone.js'); | ||||||
|  |         loadScript(document.location.search.endsWith('debug') ? 'bundle.min_debug.js' : 'bundle.min.js'); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   </script> | ||||||
|  | </body> | ||||||
|  | 
 | ||||||
|  | </html> | ||||||
							
								
								
									
										19
									
								
								modules/benchmarks/src/expanding_rows/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								modules/benchmarks/src/expanding_rows/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {platformBrowser} from '@angular/platform-browser'; | ||||||
|  | 
 | ||||||
|  | import {ExpandingRowBenchmarkModule} from './benchmark'; | ||||||
|  | import {ExpandingRowBenchmarkModuleNgFactory} from './benchmark.ngfactory'; | ||||||
|  | 
 | ||||||
|  | setMode(ExpandingRowBenchmarkModule.hasOwnProperty('ngModuleDef') ? 'Ivy' : 'ViewEngine'); | ||||||
|  | platformBrowser().bootstrapModuleFactory(ExpandingRowBenchmarkModuleNgFactory); | ||||||
|  | 
 | ||||||
|  | function setMode(name: string): void { | ||||||
|  |   document.querySelector('#rendererMode') !.textContent = `Render Mode: ${name}`; | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user