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