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
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -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); });
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
|
@ -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 {}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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…
Reference in New Issue