+
+
+ `,
+})
+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}`,
+ });
+ }
+ }
+}
diff --git a/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts
new file mode 100644
index 0000000000..9294f21ac2
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts
@@ -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 {
+}
+
\ No newline at end of file
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row.ts b/modules/benchmarks/src/expanding_rows/expanding_row.ts
new file mode 100644
index 0000000000..ce098dfe8b
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row.ts
@@ -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('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;
+
+ /**
+ * 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: `
+
+
+
`,
+ 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();
+
+ /**
+ * 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();
+
+ /** Triggered when index property changes. */
+ indexChange = new EventEmitter();
+
+ /**
+ * 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();
+ }
+ }
+}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts b/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts
new file mode 100644
index 0000000000..fb0546b28a
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts
@@ -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 {}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_css.ts b/modules/benchmarks/src/expanding_rows/expanding_row_css.ts
new file mode 100644
index 0000000000..6eff25fba7
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_css.ts
@@ -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;
+ }`;
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts b/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts
new file mode 100644
index 0000000000..ee514f6f40
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts
@@ -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: `
+
+
+
`,
+ 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();
+ }
+}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts b/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts
new file mode 100644
index 0000000000..b67cc1f02d
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts
@@ -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: `
+
+
+
`,
+ 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();
+ }
+}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_host.ts b/modules/benchmarks/src/expanding_rows/expanding_row_host.ts
new file mode 100644
index 0000000000..64f41bd146
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_host.ts
@@ -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 template to identify the row.
+ * The [cfcExpandingRowHost] directive also uses this class to check if a given
+ * HTMLElement is within an .
+ */
+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: `
+
+
+
+
+
`,
+ 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();
+
+ /** 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;
+
+ /**
+ * 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++;
+ });
+ });
+ }
+}
+
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_module.ts b/modules/benchmarks/src/expanding_rows/expanding_row_module.ts
new file mode 100644
index 0000000000..13ced09aa4
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_module.ts
@@ -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 {
+}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts b/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts
new file mode 100644
index 0000000000..2e724c889f
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts
@@ -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: `
+
+
+
.
+
+ Row {{expandingRow.index + 1}} in list of expanding rows.
+
+
+ Use arrow keys to navigate.
+
+
`,
+ 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 [];
+ }
+}
diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts b/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts
new file mode 100644
index 0000000000..e1b1e334fc
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts
@@ -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;
+}
diff --git a/modules/benchmarks/src/expanding_rows/index.html b/modules/benchmarks/src/expanding_rows/index.html
new file mode 100644
index 0000000000..b4e258faa0
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
Change Detection Benchmark
+
...
+
+ loading...
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/benchmarks/src/expanding_rows/index.ts b/modules/benchmarks/src/expanding_rows/index.ts
new file mode 100644
index 0000000000..6dd9db9d86
--- /dev/null
+++ b/modules/benchmarks/src/expanding_rows/index.ts
@@ -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}`;
+}
\ No newline at end of file