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