mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(popover-edit): accessible row hover content (#15917)
This commit is contained in:
committed by
Joey Perrott
parent
ab76eaa821
commit
461d5390d9
@@ -6,16 +6,41 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, Subject, timer} from 'rxjs';
|
||||
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
|
||||
import {Injectable, NgZone} from '@angular/core';
|
||||
import {combineLatest, MonoTypeOperatorFunction, Observable, pipe, Subject} from 'rxjs';
|
||||
import {
|
||||
audit,
|
||||
auditTime,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
share,
|
||||
skip,
|
||||
startWith,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
|
||||
import {closest} from './polyfill';
|
||||
import {EditRef} from './edit-ref';
|
||||
|
||||
/** The delay between mouse out events and hiding hover content. */
|
||||
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
|
||||
/** The delay applied to mouse events before hiding or showing hover content. */
|
||||
const MOUSE_EVENT_DELAY_MS = 40;
|
||||
|
||||
/** The delay for reacting to focus/blur changes. */
|
||||
const FOCUS_DELAY = 0;
|
||||
|
||||
/**
|
||||
* The possible states for hover content:
|
||||
* OFF - Not rendered.
|
||||
* FOCUSABLE - Rendered in the dom and stylyed for its contents to be focusable but invisible.
|
||||
* ON - Rendered and fully visible.
|
||||
*/
|
||||
export const enum HoverContentState {
|
||||
OFF = 0,
|
||||
FOCUSABLE,
|
||||
ON,
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for sharing delegated events and state for triggering table edits.
|
||||
@@ -28,7 +53,13 @@ export class EditEventDispatcher {
|
||||
/** A subject that indicates which table row is currently hovered. */
|
||||
readonly hovering = new Subject<Element|null>();
|
||||
|
||||
/** A subject that emits mouse move events for table rows. */
|
||||
/** A subject that indicates which table row currently contains focus. */
|
||||
readonly focused = new Subject<Element|null>();
|
||||
|
||||
/** A subject that indicates all elements in the table matching ROW_SELECTOR. */
|
||||
readonly allRows = new Subject<NodeList>();
|
||||
|
||||
/** A subject that emits mouse move events from the table indicating the targeted row. */
|
||||
readonly mouseMove = new Subject<Element|null>();
|
||||
|
||||
/** The EditRef for the currently active edit lens (if any). */
|
||||
@@ -37,13 +68,76 @@ export class EditEventDispatcher {
|
||||
}
|
||||
private _editRef: EditRef<any>|null = null;
|
||||
|
||||
// Optimization: Precompute common pipeable operators used per row/cell.
|
||||
private readonly _distinctUntilChanged =
|
||||
distinctUntilChanged<Element|HoverContentState|boolean|null>();
|
||||
private readonly _startWithNull = startWith<Element|null>(null);
|
||||
private readonly _distinctShare = pipe(
|
||||
this._distinctUntilChanged as MonoTypeOperatorFunction<HoverContentState>,
|
||||
share(),
|
||||
);
|
||||
private readonly _startWithNullDistinct = pipe(
|
||||
this._startWithNull,
|
||||
this._distinctUntilChanged as MonoTypeOperatorFunction<Element|null>,
|
||||
);
|
||||
|
||||
/** An observable that emits the row containing focus or an active edit. */
|
||||
readonly editingOrFocused = combineLatest(
|
||||
this.editing.pipe(
|
||||
map(cell => closest(cell, ROW_SELECTOR)),
|
||||
this._startWithNull,
|
||||
),
|
||||
this.focused.pipe(this._startWithNull),
|
||||
).pipe(
|
||||
map(([editingRow, focusedRow]) => focusedRow || editingRow),
|
||||
this._distinctUntilChanged as MonoTypeOperatorFunction<Element|null>,
|
||||
auditTime(FOCUS_DELAY), // Use audit to skip over blur events to the next focused element.
|
||||
this._distinctUntilChanged as MonoTypeOperatorFunction<Element|null>,
|
||||
share(),
|
||||
);
|
||||
|
||||
/** Tracks rows that contain hover content with a reference count. */
|
||||
private _rowsWithHoverContent = new WeakMap<Element, number>();
|
||||
|
||||
/** The table cell that has an active edit lens (or null). */
|
||||
private _currentlyEditing: Element|null = null;
|
||||
|
||||
private readonly _hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
|
||||
private readonly _editingDistinct = this.editing.pipe(distinctUntilChanged(), share());
|
||||
/** The combined set of row hover content states organized by row. */
|
||||
private readonly _hoveredContentStateDistinct = combineLatest(
|
||||
this._getFirstRowWithHoverContent(),
|
||||
this._getLastRowWithHoverContent(),
|
||||
this.editingOrFocused,
|
||||
this.hovering.pipe(
|
||||
distinctUntilChanged(),
|
||||
audit(row => this.mouseMove.pipe(
|
||||
filter(mouseMoveRow => row === mouseMoveRow),
|
||||
this._startWithNull,
|
||||
debounceTime(MOUSE_EVENT_DELAY_MS)),
|
||||
),
|
||||
this._startWithNullDistinct,
|
||||
),
|
||||
).pipe(
|
||||
skip(1), // Skip the initial emission of [null, null, null, null].
|
||||
map(computeHoverContentState),
|
||||
distinctUntilChanged(areMapEntriesEqual),
|
||||
// Optimization: Enter the zone before share() so that we trigger a single
|
||||
// ApplicationRef.tick for all row updates.
|
||||
this._enterZone(),
|
||||
share(),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
private readonly _editingDistinct = this.editing.pipe(
|
||||
distinctUntilChanged(),
|
||||
this._enterZone(),
|
||||
share(),
|
||||
);
|
||||
|
||||
// Optimization: Share row events observable with subsequent callers.
|
||||
// At startup, calls will be sequential by row.
|
||||
private _lastSeenRow: Element|null = null;
|
||||
private _lastSeenRowHoverOrFocus: Observable<HoverContentState>|null = null;
|
||||
|
||||
constructor(private readonly _ngZone: NgZone) {
|
||||
this._editingDistinct.subscribe(cell => {
|
||||
this._currentlyEditing = cell;
|
||||
});
|
||||
@@ -58,7 +152,7 @@ export class EditEventDispatcher {
|
||||
|
||||
return this._editingDistinct.pipe(
|
||||
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
|
||||
distinctUntilChanged(),
|
||||
this._distinctUntilChanged as MonoTypeOperatorFunction<boolean>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,20 +182,123 @@ export class EditEventDispatcher {
|
||||
this._editRef = null;
|
||||
}
|
||||
|
||||
/** Adds the specified table row to be tracked for first/last row comparisons. */
|
||||
registerRowWithHoverContent(row: Element): void {
|
||||
this._rowsWithHoverContent.set(row, (this._rowsWithHoverContent.get(row) || 0) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference decrements and ultimately removes the specified table row from first/last row
|
||||
* comparisons.
|
||||
*/
|
||||
deregisterRowWithHoverContent(row: Element): void {
|
||||
const refCount = this._rowsWithHoverContent.get(row) || 0;
|
||||
|
||||
if (refCount <= 1) {
|
||||
this._rowsWithHoverContent.delete(row);
|
||||
} else {
|
||||
this._rowsWithHoverContent.set(row, refCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an Observable that emits true when the specified element's row
|
||||
* is being hovered over and false when not. Hovering is defined as when
|
||||
* the mouse has momentarily stopped moving over the cell.
|
||||
* contains the focused element or is being hovered over and false when not.
|
||||
* Hovering is defined as when the mouse has momentarily stopped moving over the cell.
|
||||
*/
|
||||
hoveringOnRow(element: Element|EventTarget): Observable<boolean> {
|
||||
let row: Element|null = null;
|
||||
hoverOrFocusOnRow(row: Element): Observable<HoverContentState> {
|
||||
if (row !== this._lastSeenRow) {
|
||||
this._lastSeenRow = row;
|
||||
this._lastSeenRowHoverOrFocus = this._hoveredContentStateDistinct.pipe(
|
||||
map(state => state.get(row) || HoverContentState.OFF),
|
||||
this._distinctShare,
|
||||
);
|
||||
}
|
||||
|
||||
return this._hoveringDistinct.pipe(
|
||||
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
|
||||
audit(
|
||||
(hovering) => hovering ? this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
|
||||
timer(DEFAULT_MOUSE_OUT_DELAY_MS)),
|
||||
distinctUntilChanged(),
|
||||
return this._lastSeenRowHoverOrFocus!;
|
||||
}
|
||||
|
||||
/**
|
||||
* RxJS operator that enters the Angular zone, used to reduce boilerplate in
|
||||
* re-entering the zone for stream pipelines.
|
||||
*/
|
||||
private _enterZone<T>(): MonoTypeOperatorFunction<T> {
|
||||
return (source: Observable<T>) =>
|
||||
new Observable<T>((observer) => source.subscribe({
|
||||
next: (value) => this._ngZone.run(() => observer.next(value)),
|
||||
error: (err) => observer.error(err),
|
||||
complete: () => observer.complete()
|
||||
}));
|
||||
}
|
||||
|
||||
private _getFirstRowWithHoverContent(): Observable<Element|null> {
|
||||
return this._mapAllRowsToSingleRow(rows => {
|
||||
for (let i = 0, row; row = rows[i]; i++) {
|
||||
if (this._rowsWithHoverContent.has(row as Element)) {
|
||||
return row as Element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private _getLastRowWithHoverContent(): Observable<Element|null> {
|
||||
return this._mapAllRowsToSingleRow(rows => {
|
||||
for (let i = rows.length - 1, row; row = rows[i]; i--) {
|
||||
if (this._rowsWithHoverContent.has(row as Element)) {
|
||||
return row as Element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private _mapAllRowsToSingleRow(mapper: (rows: NodeList) => Element|null):
|
||||
Observable<Element|null> {
|
||||
return this.allRows.pipe(
|
||||
map(mapper),
|
||||
this._startWithNullDistinct,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function computeHoverContentState([firstRow, lastRow, activeRow, hoverRow]: Array<Element|null>):
|
||||
Map<Element, HoverContentState> {
|
||||
const hoverContentState = new Map<Element, HoverContentState>();
|
||||
|
||||
// Add focusable rows.
|
||||
for (const focussableRow of [
|
||||
firstRow,
|
||||
lastRow,
|
||||
activeRow && activeRow.previousElementSibling,
|
||||
activeRow && activeRow.nextElementSibling,
|
||||
]) {
|
||||
if (focussableRow) {
|
||||
hoverContentState.set(focussableRow as Element, HoverContentState.FOCUSABLE);
|
||||
}
|
||||
}
|
||||
|
||||
// Add/overwrite with fully visible rows.
|
||||
for (const onRow of [activeRow, hoverRow]) {
|
||||
if (onRow) {
|
||||
hoverContentState.set(onRow, HoverContentState.ON);
|
||||
}
|
||||
}
|
||||
|
||||
return hoverContentState;
|
||||
}
|
||||
|
||||
function areMapEntriesEqual<K, V>(a: Map<K, V>, b: Map<K, V>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: use Map.prototype.entries once we're off IE11.
|
||||
for (const aKey of Array.from(a.keys())) {
|
||||
if (b.get(aKey) !== a.get(aKey)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,22 @@ export function matches(element: Element, selector: string): boolean {
|
||||
(element as any)['msMatchesSelector'](selector);
|
||||
}
|
||||
|
||||
/** IE 11 compatible closest implementation. */
|
||||
export function closest(element: EventTarget|Element|null|undefined, selector: string): Element|
|
||||
null {
|
||||
/** IE 11 compatible closest implementation that is able to start from non-Element Nodes. */
|
||||
export function closest(element: EventTarget|Element|null|undefined, selector: string):
|
||||
Element|null {
|
||||
if (!(element instanceof Node)) { return null; }
|
||||
|
||||
let curr: Node|null = element;
|
||||
while (curr != null && !(curr instanceof Element)) {
|
||||
curr = curr.parentNode;
|
||||
}
|
||||
|
||||
return curr && (hasNativeClosest ?
|
||||
curr.closest(selector) : polyfillClosest(curr, selector)) as Element|null;
|
||||
}
|
||||
|
||||
/** Polyfill for browsers without Element.closest. */
|
||||
function polyfillClosest(element: Element, selector: string): Element|null {
|
||||
let curr: Node|null = element;
|
||||
while (curr != null && !(curr instanceof Element && matches(curr, selector))) {
|
||||
curr = curr.parentNode;
|
||||
@@ -25,3 +36,5 @@ export function closest(element: EventTarget|Element|null|undefined, selector: s
|
||||
|
||||
return (curr || null) as Element|null;
|
||||
}
|
||||
|
||||
const hasNativeClosest = !!Element.prototype.closest;
|
||||
|
||||
@@ -10,7 +10,12 @@ import {BidiModule, Direction} from '@angular/cdk/bidi';
|
||||
import {OverlayContainer} from '@angular/cdk/overlay';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {CdkPopoverEditColspan, CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index';
|
||||
import {
|
||||
CdkPopoverEditColspan,
|
||||
CdkPopoverEditModule,
|
||||
HoverContentState,
|
||||
PopoverEditClickOutBehavior,
|
||||
} from './index';
|
||||
|
||||
const NAME_EDIT_TEMPLATE = `
|
||||
<div style="background-color: white;">
|
||||
@@ -76,7 +81,8 @@ abstract class BaseTestComponent {
|
||||
row.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
tick(31);
|
||||
// Wait for the mouse hover debounce in edit-event-dispatcher.
|
||||
tick(41);
|
||||
}
|
||||
|
||||
getRows() {
|
||||
@@ -92,6 +98,16 @@ abstract class BaseTestComponent {
|
||||
this.getEditCell(rowIndex, cellIndex).focus();
|
||||
}
|
||||
|
||||
hoverContentStateForRow(rowIndex = 0) {
|
||||
const openButton = this.getOpenButton(rowIndex);
|
||||
|
||||
if (!openButton) {
|
||||
return HoverContentState.OFF;
|
||||
}
|
||||
return parseInt(getComputedStyle(openButton.parentNode as Element).opacity || '', 10) === 0 ?
|
||||
HoverContentState.FOCUSABLE : HoverContentState.ON;
|
||||
}
|
||||
|
||||
getOpenButton(rowIndex = 0) {
|
||||
return this.getEditCell(rowIndex).querySelector('.open') as HTMLElement|null;
|
||||
}
|
||||
@@ -325,7 +341,7 @@ describe('CDK Popover Edit', () => {
|
||||
let fixture: ComponentFixture<BaseTestComponent>;
|
||||
let overlayContainer: OverlayContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CdkTableModule, CdkPopoverEditModule, CommonModule, FormsModule, BidiModule],
|
||||
declarations: [componentClass],
|
||||
@@ -336,7 +352,8 @@ describe('CDK Popover Edit', () => {
|
||||
fixture = TestBed.createComponent(componentClass);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
tick(10);
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// The overlay container's `ngOnDestroy` won't be called between test runs so we need
|
||||
@@ -345,41 +362,107 @@ describe('CDK Popover Edit', () => {
|
||||
overlayContainer.ngOnDestroy();
|
||||
});
|
||||
|
||||
describe('row hover content', () => {
|
||||
it('makes the first and last rows focusable but invisible', fakeAsync(() => {
|
||||
const rows = component.getRows();
|
||||
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(rows.length - 1))
|
||||
.toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('triggering edit', () => {
|
||||
it('shows and hides on-hover content only after a delay', fakeAsync(() => {
|
||||
const [row0, row1] = component.getRows();
|
||||
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
const [row0, row1] = component.getRows();
|
||||
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
tick(20);
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
tick(20);
|
||||
tick(20);
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
tick(20);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
tick(11);
|
||||
tick(31);
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.ON);
|
||||
|
||||
row1.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
expect(component.getOpenButton(1)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
|
||||
tick(31);
|
||||
tick(41);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.getOpenButton(1)).toEqual(jasmine.any(HTMLElement));
|
||||
}));
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.ON);
|
||||
}));
|
||||
|
||||
it('shows hover content for the focused row and makes the rows above and below focusable',
|
||||
fakeAsync(() => {
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(2);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(4);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.getEditCell(4).blur();
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
|
||||
it('shows hover content for the editing row and makes the rows above and below ' +
|
||||
'focusable unless focus is in a different table row in which case it takes priority',
|
||||
fakeAsync(() => {
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.openLens(2);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(4);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('triggering edit', () => {
|
||||
it('opens edit from on-hover button', fakeAsync(() => {
|
||||
component.triggerHoverState();
|
||||
component.clickOpenButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('opens edit from Enter on focued cell', fakeAsync(() => {
|
||||
@@ -387,6 +470,7 @@ describe('CDK Popover Edit', () => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -475,6 +559,7 @@ describe('CDK Popover Edit', () => {
|
||||
// Focus should have moved to the top of the lens.
|
||||
expect(document.activeElement).toBe(focusableElements[1]);
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('moves focus to the next cell when focus leaves end of lens with cdkPopoverEditTabOut',
|
||||
@@ -491,6 +576,7 @@ describe('CDK Popover Edit', () => {
|
||||
// Focus should have moved to the next editable cell.
|
||||
expect(document.activeElement).toBe(component.getEditCell(1, 1));
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it(`moves focus to the previous cell when focus leaves end of lens with
|
||||
@@ -507,6 +593,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
// Focus should have moved to the next editable cell.
|
||||
expect(document.activeElement).toBe(component.getEditCell(0, 1));
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -516,6 +603,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('positions the lens at the top left corner and spans the full width of the cell',
|
||||
@@ -528,6 +616,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
expect(paneRect.width).toBe(cellRect.width);
|
||||
expect(paneRect.left).toBe(cellRect.left);
|
||||
expect(paneRect.top).toBe(cellRect.top);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('adjusts the positioning of the lens based on colspan', fakeAsync(() => {
|
||||
@@ -559,6 +648,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
expect(paneRect.top).toBe(cellRects[0].top);
|
||||
expect(paneRect.left).toBe(cellRects[0].left);
|
||||
expect(paneRect.right).toBe(cellRects[2].right);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('updates the form and submits, closing the lens', fakeAsync(() => {
|
||||
@@ -569,9 +659,9 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
|
||||
component.clickSubmitButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not close the lens on submit when form is invalid', fakeAsync(() => {
|
||||
@@ -583,6 +673,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes lens on submit when form is invalid with ' +
|
||||
@@ -596,6 +687,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on close', fakeAsync(() => {
|
||||
@@ -604,6 +696,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes and reopens a lens with modified value persisted', fakeAsync(() => {
|
||||
@@ -615,12 +708,13 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('resets the lens to original value', fakeAsync(() => {
|
||||
@@ -632,6 +726,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('resets the lens to previously submitted value', fakeAsync(() => {
|
||||
@@ -651,6 +746,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on escape', fakeAsync(() => {
|
||||
@@ -660,6 +756,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
new KeyboardEvent('keyup', {bubbles: true, key: 'Escape'}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not close the lens on click within lens', fakeAsync(() => {
|
||||
@@ -668,6 +765,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.getInput()!.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on outside click', fakeAsync(() => {
|
||||
@@ -679,8 +777,8 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('submits the lens on outside click with ' +
|
||||
@@ -694,7 +792,8 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does nothing on outside click with ' +
|
||||
@@ -708,13 +807,15 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('sets focus on the first input in the lens', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(document.activeElement).toBe(component.getInput());
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('returns focus to the edited cell after closing', fakeAsync(() => {
|
||||
@@ -723,6 +824,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell());
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not focus to the edited cell after closing if another element ' +
|
||||
@@ -733,6 +835,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.getEditCell(1).dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell(1));
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('should pass the directionality to the overlay', fakeAsync(() => {
|
||||
@@ -742,10 +845,10 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.getEditBoundingBox()!.getAttribute('dir')).toBe('rtl');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -774,3 +877,10 @@ function getCells(row: Element): HTMLElement[] {
|
||||
|
||||
return getElements(row, '.cdk-cell, td');
|
||||
}
|
||||
|
||||
// Common actions like mouse events and focus/blur cause timers to be fired off.
|
||||
// When not testing this behavior directly, use this function to clear any timers that were
|
||||
// created in passing.
|
||||
function clearLeftoverTimers() {
|
||||
tick(100);
|
||||
}
|
||||
|
||||
@@ -13,3 +13,6 @@ export * from './lens-directives';
|
||||
export * from './popover-edit-module';
|
||||
export * from './popover-edit-position-strategy-factory';
|
||||
export * from './table-directives';
|
||||
|
||||
export {CELL_SELECTOR as _CELL_SELECTOR} from './constants';
|
||||
export {closest as _closest} from './polyfill';
|
||||
|
||||
@@ -19,11 +19,20 @@ import {
|
||||
ViewContainerRef,
|
||||
HostListener,
|
||||
} from '@angular/core';
|
||||
import {fromEvent, merge, ReplaySubject} from 'rxjs';
|
||||
import {debounceTime, filter, map, mapTo, startWith, takeUntil} from 'rxjs/operators';
|
||||
import {fromEvent, fromEventPattern, merge, ReplaySubject} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
mapTo,
|
||||
share,
|
||||
startWith,
|
||||
takeUntil,
|
||||
throttleTime,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
|
||||
import {EditEventDispatcher} from './edit-event-dispatcher';
|
||||
import {EditEventDispatcher, HoverContentState} from './edit-event-dispatcher';
|
||||
import {EditServices} from './edit-services';
|
||||
import {FocusDispatcher} from './focus-dispatcher';
|
||||
import {
|
||||
@@ -43,11 +52,8 @@ export interface CdkPopoverEditColspan {
|
||||
after?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The delay between the mouse entering a row and the mouse stopping its movement before
|
||||
* showing on-hover content.
|
||||
*/
|
||||
const DEFAULT_MOUSE_MOVE_DELAY_MS = 30;
|
||||
/** Used for rate-limiting mousemove events. */
|
||||
const MOUSE_MOVE_THROTTLE_TIME_MS = 10;
|
||||
|
||||
/**
|
||||
* A directive that must be attached to enable editability on a table.
|
||||
@@ -82,6 +88,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
|
||||
map((event: UIEvent) => closest(event.target, selector));
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
// Track mouse movement over the table to hide/show hover content.
|
||||
fromEvent<MouseEvent>(element, 'mouseover').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
toClosest(ROW_SELECTOR),
|
||||
@@ -92,10 +99,41 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
|
||||
).subscribe(this.editEventDispatcher.hovering);
|
||||
fromEvent<MouseEvent>(element, 'mousemove').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
debounceTime(DEFAULT_MOUSE_MOVE_DELAY_MS),
|
||||
throttleTime(MOUSE_MOVE_THROTTLE_TIME_MS),
|
||||
toClosest(ROW_SELECTOR),
|
||||
).subscribe(this.editEventDispatcher.mouseMove);
|
||||
|
||||
// Track focus within the table to hide/show/make focusable hover content.
|
||||
fromEventPattern<FocusEvent>(
|
||||
(handler) => element.addEventListener('focus', handler, true),
|
||||
(handler) => element.removeEventListener('focus', handler, true)
|
||||
).pipe(
|
||||
takeUntil(this.destroyed),
|
||||
toClosest(ROW_SELECTOR),
|
||||
share(),
|
||||
).subscribe(this.editEventDispatcher.focused);
|
||||
fromEventPattern<FocusEvent>(
|
||||
(handler) => element.addEventListener('blur', handler, true),
|
||||
(handler) => element.removeEventListener('blur', handler, true)
|
||||
).pipe(
|
||||
takeUntil(this.destroyed),
|
||||
mapTo(null),
|
||||
share(),
|
||||
).subscribe(this.editEventDispatcher.focused);
|
||||
|
||||
// Keep track of rows within the table. This is used to know which rows with hover content
|
||||
// are first or last in the table. They are kept focusable in case focus enters from above
|
||||
// or below the table.
|
||||
this.ngZone.onStable.pipe(
|
||||
takeUntil(this.destroyed),
|
||||
// Optimization: ignore dom changes while focus is within the table as we already
|
||||
// ensure that rows above and below the focused/active row are tabbable.
|
||||
withLatestFrom(this.editEventDispatcher.editingOrFocused),
|
||||
filter(([_, activeRow]) => activeRow == null),
|
||||
map(() => element.querySelectorAll(ROW_SELECTOR)),
|
||||
share(),
|
||||
).subscribe(this.editEventDispatcher.allRows);
|
||||
|
||||
fromEvent<KeyboardEvent>(element, 'keyup').pipe(
|
||||
takeUntil(this.destroyed),
|
||||
filter(event => event.key === 'Enter'),
|
||||
@@ -200,19 +238,17 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
|
||||
this.services.editEventDispatcher.editingCell(this.elementRef.nativeElement!)
|
||||
.pipe(takeUntil(this.destroyed))
|
||||
.subscribe((open) => {
|
||||
this.services.ngZone.run(() => {
|
||||
if (open && this.template) {
|
||||
if (!this.overlayRef) {
|
||||
this._createEditOverlay();
|
||||
}
|
||||
|
||||
this._showEditOverlay();
|
||||
} else if (this.overlayRef) {
|
||||
this._maybeReturnFocusToCell();
|
||||
|
||||
this.overlayRef.detach();
|
||||
if (open && this.template) {
|
||||
if (!this.overlayRef) {
|
||||
this._createEditOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
this._showEditOverlay();
|
||||
} else if (this.overlayRef) {
|
||||
this._maybeReturnFocusToCell();
|
||||
|
||||
this.overlayRef.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +272,7 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
|
||||
this.template!,
|
||||
this.viewContainerRef,
|
||||
{$implicit: this.context}));
|
||||
this.focusTrap!.focusInitialElementWhenReady();
|
||||
this.focusTrap!.focusInitialElement();
|
||||
|
||||
// Update the size of the popup initially and on subsequent changes to
|
||||
// scroll position and viewport size.
|
||||
@@ -321,16 +357,7 @@ export class CdkPopoverEditTabOut<C> extends CdkPopoverEdit<C> {
|
||||
|
||||
/**
|
||||
* A structural directive that shows its contents when the table row containing
|
||||
* it is hovered.
|
||||
*
|
||||
* Note that the contents of this directive are invisible to screen readers.
|
||||
* Typically this is used to show a button that launches the edit popup, which
|
||||
* is ok because screen reader users can trigger edit by pressing Enter on a focused
|
||||
* table cell.
|
||||
*
|
||||
* If this directive contains buttons for functionality other than opening edit then
|
||||
* care should be taken to make sure that this functionality is also exposed in
|
||||
* an accessible way.
|
||||
* it is hovered or when an element in the row has focus.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[cdkRowHoverContent]',
|
||||
@@ -339,13 +366,18 @@ export class CdkRowHoverContent implements AfterViewInit, OnDestroy {
|
||||
protected readonly destroyed = new ReplaySubject<void>();
|
||||
protected viewRef: EmbeddedViewRef<any>|null = null;
|
||||
|
||||
private _row?: Element;
|
||||
|
||||
constructor(
|
||||
protected readonly services: EditServices, protected readonly elementRef: ElementRef,
|
||||
protected readonly templateRef: TemplateRef<any>,
|
||||
protected readonly viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._listenForHoverEvents();
|
||||
this._row = closest(this.elementRef.nativeElement!, ROW_SELECTOR)!;
|
||||
|
||||
this.services.editEventDispatcher.registerRowWithHoverContent(this._row);
|
||||
this._listenForHoverAndFocusEvents();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -355,35 +387,59 @@ export class CdkRowHoverContent implements AfterViewInit, OnDestroy {
|
||||
if (this.viewRef) {
|
||||
this.viewRef.destroy();
|
||||
}
|
||||
|
||||
if (this._row) {
|
||||
this.services.editEventDispatcher.deregisterRowWithHoverContent(this._row);
|
||||
}
|
||||
}
|
||||
|
||||
protected initElement(element: HTMLElement): void;
|
||||
protected initElement(): void {
|
||||
/**
|
||||
* Called immediately after the hover content is created and added to the dom.
|
||||
* In the CDK version, this is a noop but subclasses such as MatRowHoverContent use this
|
||||
* to prepare/style the inserted element.
|
||||
*/
|
||||
protected initElement(_: HTMLElement): void {
|
||||
}
|
||||
|
||||
protected prepareElement(element: HTMLElement): void;
|
||||
protected prepareElement(): void {
|
||||
/**
|
||||
* Called when the hover content needs to be focusable to preserve a reasonable tab ordering
|
||||
* but should not yet be shown.
|
||||
*/
|
||||
protected makeElementHiddenButFocusable(element: HTMLElement): void {
|
||||
element.style.opacity = '0';
|
||||
}
|
||||
|
||||
private _listenForHoverEvents(): void {
|
||||
this.services.editEventDispatcher.hoveringOnRow(this.elementRef.nativeElement!)
|
||||
/**
|
||||
* Called when the hover content needs to be focusable to preserve a reasonable tab ordering
|
||||
* but should not yet be shown.
|
||||
*/
|
||||
protected makeElementVisible(element: HTMLElement): void {
|
||||
element.style.opacity = '';
|
||||
}
|
||||
|
||||
private _listenForHoverAndFocusEvents(): void {
|
||||
this.services.editEventDispatcher.hoverOrFocusOnRow(this._row!)
|
||||
.pipe(takeUntil(this.destroyed))
|
||||
.subscribe(isHovering => {
|
||||
this.services.ngZone.run(() => {
|
||||
if (isHovering) {
|
||||
if (!this.viewRef) {
|
||||
// Not doing any positioning in CDK version. Material version
|
||||
// will absolutely position on right edge of cell.
|
||||
this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef, {});
|
||||
this.initElement(this.viewRef.rootNodes[0] as HTMLElement);
|
||||
} else {
|
||||
this.viewContainerRef.insert(this.viewRef);
|
||||
}
|
||||
this.prepareElement(this.viewRef.rootNodes[0] as HTMLElement);
|
||||
} else if (this.viewRef) {
|
||||
this.viewContainerRef.detach(this.viewContainerRef.indexOf(this.viewRef));
|
||||
.subscribe(eventState => {
|
||||
// When in FOCUSABLE state, add the hover content to the dom but make it transparent so
|
||||
// that it is in the tab order relative to the currently focused row.
|
||||
|
||||
if (eventState === HoverContentState.ON || eventState === HoverContentState.FOCUSABLE) {
|
||||
if (!this.viewRef) {
|
||||
this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef, {});
|
||||
this.initElement(this.viewRef.rootNodes[0] as HTMLElement);
|
||||
} else if (this.viewContainerRef.indexOf(this.viewRef) === -1) {
|
||||
this.viewContainerRef.insert(this.viewRef!);
|
||||
}
|
||||
});
|
||||
|
||||
if (eventState === HoverContentState.ON) {
|
||||
this.makeElementVisible(this.viewRef.rootNodes[0] as HTMLElement);
|
||||
} else {
|
||||
this.makeElementHiddenButFocusable(this.viewRef.rootNodes[0] as HTMLElement);
|
||||
}
|
||||
} else if (this.viewRef) {
|
||||
this.viewContainerRef.detach(this.viewContainerRef.indexOf(this.viewRef));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 287 B |
-8
@@ -1,8 +1,6 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {MatIconRegistry} from '@angular/material/icon';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface Person {
|
||||
@@ -38,12 +36,6 @@ export class PopoverEditCellSpanMatTableExample {
|
||||
|
||||
readonly preservedValues = new WeakMap<Person, any>();
|
||||
|
||||
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
|
||||
iconRegistry.addSvgIcon(
|
||||
'edit',
|
||||
sanitizer.bypassSecurityTrustResourceUrl('assets/img/examples/edit-icon.svg'));
|
||||
}
|
||||
|
||||
onSubmit(person: Person, f: NgForm) {
|
||||
if (!f.valid) {
|
||||
return;
|
||||
|
||||
-8
@@ -1,8 +1,6 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {MatIconRegistry} from '@angular/material/icon';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface PeriodicElement {
|
||||
@@ -50,12 +48,6 @@ export class PopoverEditMatTableFlexExample {
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
|
||||
iconRegistry.addSvgIcon(
|
||||
'edit',
|
||||
sanitizer.bypassSecurityTrustResourceUrl('assets/img/examples/edit-icon.svg'));
|
||||
}
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
|
||||
@@ -22,7 +22,19 @@
|
||||
<!-- Position Column -->
|
||||
<ng-container matColumnDef="position">
|
||||
<th mat-header-cell *matHeaderCellDef> No. </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{element.position}}
|
||||
|
||||
<!-- Row hover content in a non-edit cell. -->
|
||||
<span *matRowHoverContent>
|
||||
<button mat-icon-button (click)="goodJob(element)">
|
||||
<mat-icon>thumb_up</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="badJob(element)">
|
||||
<mat-icon>thumb_down</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {MatIconRegistry} from '@angular/material/icon';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface PeriodicElement {
|
||||
@@ -50,11 +49,7 @@ export class PopoverEditMatTableExample {
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
|
||||
iconRegistry.addSvgIcon(
|
||||
'edit',
|
||||
sanitizer.bypassSecurityTrustResourceUrl('assets/img/examples/edit-icon.svg'));
|
||||
}
|
||||
constructor(private readonly _snackBar: MatSnackBar) {}
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
@@ -67,6 +62,15 @@ export class PopoverEditMatTableExample {
|
||||
|
||||
element.weight = f.value.weight;
|
||||
}
|
||||
|
||||
goodJob(element: PeriodicElement) {
|
||||
this._snackBar.open(`Way to go, ${element.name}!`, undefined, {duration: 2000});
|
||||
}
|
||||
|
||||
badJob(element: PeriodicElement) {
|
||||
this._snackBar.open(`You have failed me for the last time, #${element.position}.`, undefined,
|
||||
{duration: 2000});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-8
@@ -1,8 +1,6 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {DataSource} from '@angular/cdk/collections';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {NgForm} from '@angular/forms';
|
||||
import {MatIconRegistry} from '@angular/material/icon';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
export interface PeriodicElement {
|
||||
@@ -50,12 +48,6 @@ export class PopoverEditTabOutMatTableExample {
|
||||
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
|
||||
iconRegistry.addSvgIcon(
|
||||
'edit',
|
||||
sanitizer.bypassSecurityTrustResourceUrl('assets/img/examples/edit-icon.svg'));
|
||||
}
|
||||
|
||||
onSubmitName(element: PeriodicElement, f: NgForm) {
|
||||
if (!f.valid) { return; }
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
|
||||
$background-color: mat-color($background, 'card');
|
||||
|
||||
.mat-row-hover-content-host-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-row-hover-content {
|
||||
align-items: center;
|
||||
background: mat-edit-hover-content-background(90deg, $background-color);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {
|
||||
CdkPopoverEditColspan,
|
||||
HoverContentState,
|
||||
PopoverEditClickOutBehavior,
|
||||
} from '@angular/cdk-experimental/popover-edit';
|
||||
import {MatPopoverEditModule} from './index';
|
||||
@@ -78,13 +79,24 @@ abstract class BaseTestComponent {
|
||||
row.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
tick(31);
|
||||
// Wait for the mouse hover debounce in edit-event-dispatcher.
|
||||
tick(41);
|
||||
}
|
||||
|
||||
getRows() {
|
||||
return getRows(this.table.nativeElement);
|
||||
}
|
||||
|
||||
hoverContentStateForRow(rowIndex = 0) {
|
||||
const openButton = this.getOpenButton(rowIndex);
|
||||
|
||||
if (!openButton) {
|
||||
return HoverContentState.OFF;
|
||||
}
|
||||
return (openButton.parentNode as Element).classList.contains('mat-row-hover-content-visible') ?
|
||||
HoverContentState.ON : HoverContentState.FOCUSABLE;
|
||||
}
|
||||
|
||||
getEditCell(rowIndex = 0, cellIndex = 1) {
|
||||
const row = this.getRows()[rowIndex];
|
||||
return getCells(row)[cellIndex];
|
||||
@@ -266,7 +278,7 @@ describe('Material Popover Edit', () => {
|
||||
let fixture: ComponentFixture<BaseTestComponent>;
|
||||
let overlayContainer: OverlayContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatTableModule, MatPopoverEditModule, CommonModule, FormsModule],
|
||||
declarations: [componentClass],
|
||||
@@ -277,7 +289,8 @@ describe('Material Popover Edit', () => {
|
||||
fixture = TestBed.createComponent(componentClass);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
tick(10);
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
// The overlay container's `ngOnDestroy` won't be called between test runs so we need
|
||||
@@ -286,41 +299,107 @@ describe('Material Popover Edit', () => {
|
||||
overlayContainer.ngOnDestroy();
|
||||
});
|
||||
|
||||
describe('row hover content', () => {
|
||||
it('makes the first and last rows focusable but invisible', fakeAsync(() => {
|
||||
const rows = component.getRows();
|
||||
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(rows.length - 1))
|
||||
.toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('triggering edit', () => {
|
||||
it('shows and hides on-hover content only after a delay', fakeAsync(() => {
|
||||
const [row0, row1] = component.getRows();
|
||||
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
const [row0, row1] = component.getRows();
|
||||
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
tick(20);
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
tick(20);
|
||||
tick(20);
|
||||
row0.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
tick(20);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
tick(11);
|
||||
tick(31);
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.ON);
|
||||
|
||||
row1.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mouseover', {bubbles: true}));
|
||||
row1.dispatchEvent(new Event('mousemove', {bubbles: true}));
|
||||
|
||||
expect(component.getOpenButton(0)).toEqual(jasmine.any(HTMLElement));
|
||||
expect(component.getOpenButton(1)).toBe(null);
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
|
||||
tick(31);
|
||||
tick(41);
|
||||
|
||||
expect(component.getOpenButton(0)).toBe(null);
|
||||
expect(component.getOpenButton(1)).toEqual(jasmine.any(HTMLElement));
|
||||
}));
|
||||
expect(component.hoverContentStateForRow(0)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.ON);
|
||||
}));
|
||||
|
||||
it('shows hover content for the focused row and makes the rows above and below focusable',
|
||||
fakeAsync(() => {
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(2);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(4);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.getEditCell(4).blur();
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
|
||||
it('shows hover content for the editing row and makes the rows above and below ' +
|
||||
'focusable unless focus is in a different table row in which case it takes priority',
|
||||
fakeAsync(() => {
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.openLens(2);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(1)).toBe(HoverContentState.FOCUSABLE);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
|
||||
component.focusEditCell(4);
|
||||
tick(1);
|
||||
|
||||
expect(component.hoverContentStateForRow(2)).toBe(HoverContentState.OFF);
|
||||
expect(component.hoverContentStateForRow(4)).toBe(HoverContentState.ON);
|
||||
expect(component.hoverContentStateForRow(3)).toBe(HoverContentState.FOCUSABLE);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('triggering edit', () => {
|
||||
it('opens edit from on-hover button', fakeAsync(() => {
|
||||
component.triggerHoverState();
|
||||
component.clickOpenButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('opens edit from Enter on focued cell', fakeAsync(() => {
|
||||
@@ -328,6 +407,7 @@ describe('Material Popover Edit', () => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -416,6 +496,7 @@ describe('Material Popover Edit', () => {
|
||||
// Focus should have moved to the top of the lens.
|
||||
expect(document.activeElement).toBe(focusableElements[1]);
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('moves focus to the next cell when focus leaves end of lens with matPopoverEditTabOut',
|
||||
@@ -432,6 +513,7 @@ describe('Material Popover Edit', () => {
|
||||
// Focus should have moved to the next editable cell.
|
||||
expect(document.activeElement).toBe(component.getEditCell(1, 1));
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it(`moves focus to the previous cell when focus leaves end of lens with
|
||||
@@ -448,6 +530,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
// Focus should have moved to the next editable cell.
|
||||
expect(document.activeElement).toBe(component.getEditCell(0, 1));
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -457,6 +540,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('positions the lens at the top left corner and spans the full width of the cell',
|
||||
@@ -469,6 +553,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
expect(paneRect.width).toBe(cellRect.width);
|
||||
expect(paneRect.left).toBe(cellRect.left);
|
||||
expect(paneRect.top).toBe(cellRect.top);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('adjusts the positioning of the lens based on colspan', fakeAsync(() => {
|
||||
@@ -500,6 +585,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
expect(paneRect.top).toBe(cellRects[0].top);
|
||||
expect(paneRect.left).toBe(cellRects[0].left);
|
||||
expect(paneRect.right).toBe(cellRects[2].right);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('updates the form and submits, closing the lens', fakeAsync(() => {
|
||||
@@ -511,8 +597,9 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickSubmitButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not close the lens on submit when form is invalid', fakeAsync(() => {
|
||||
@@ -524,6 +611,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes lens on submit when form is invalid with ' +
|
||||
@@ -537,6 +625,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickSubmitButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on close', fakeAsync(() => {
|
||||
@@ -545,6 +634,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes and reopens a lens with modified value persisted', fakeAsync(() => {
|
||||
@@ -556,12 +646,13 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
|
||||
component.openLens();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('resets the lens to original value', fakeAsync(() => {
|
||||
@@ -573,6 +664,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('resets the lens to previously submitted value', fakeAsync(() => {
|
||||
@@ -592,6 +684,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickRevertButton();
|
||||
|
||||
expect(component.getInput()!.value).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on escape', fakeAsync(() => {
|
||||
@@ -601,6 +694,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
new KeyboardEvent('keyup', {bubbles: true, key: 'Escape'}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not close the lens on click within lens', fakeAsync(() => {
|
||||
@@ -609,6 +703,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.getInput()!.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('closes the lens on outside click', fakeAsync(() => {
|
||||
@@ -620,8 +715,8 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('submits the lens on outside click with ' +
|
||||
@@ -635,7 +730,8 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(false);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydragon');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydragon');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does nothing on outside click with ' +
|
||||
@@ -649,13 +745,15 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lensIsOpen()).toBe(true);
|
||||
expect(component.getEditCell().textContent!.trim()).toBe('Hydrogen');
|
||||
expect(component.getEditCell().firstChild!.textContent!.trim()).toBe('Hydrogen');
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('sets focus on the first input in the lens', fakeAsync(() => {
|
||||
component.openLens();
|
||||
|
||||
expect(document.activeElement).toBe(component.getInput());
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('returns focus to the edited cell after closing', fakeAsync(() => {
|
||||
@@ -664,6 +762,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.clickCloseButton();
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell());
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
|
||||
it('does not focus to the edited cell after closing if another element ' +
|
||||
@@ -674,6 +773,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
|
||||
component.getEditCell(1).dispatchEvent(new Event('click', {bubbles: true}));
|
||||
|
||||
expect(document.activeElement).toBe(component.getEditCell(1));
|
||||
clearLeftoverTimers();
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -705,3 +805,10 @@ function getCells(row: Element): HTMLElement[] {
|
||||
|
||||
return getElements(row, '.mat-cell, td');
|
||||
}
|
||||
|
||||
// Common actions like mouse events and focus/blur cause timers to be fired off.
|
||||
// When not testing this behavior directly, use this function to clear any timers that were
|
||||
// created in passing.
|
||||
function clearLeftoverTimers() {
|
||||
tick(100);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
import {Directive} from '@angular/core';
|
||||
import {
|
||||
_CELL_SELECTOR,
|
||||
_closest,
|
||||
CdkPopoverEdit,
|
||||
CdkPopoverEditTabOut,
|
||||
CdkRowHoverContent,
|
||||
@@ -27,6 +29,11 @@ const POPOVER_EDIT_INPUTS = [
|
||||
|
||||
const EDIT_PANE_CLASS = 'mat-edit-pane';
|
||||
|
||||
const MAT_ROW_HOVER_CLASS = 'mat-row-hover-content';
|
||||
const MAT_ROW_HOVER_RTL_CLASS = MAT_ROW_HOVER_CLASS + '-rtl';
|
||||
const MAT_ROW_HOVER_ANIMATE_CLASS = MAT_ROW_HOVER_CLASS + '-visible';
|
||||
const MAT_ROW_HOVER_CELL_CLASS = MAT_ROW_HOVER_CLASS + '-host-cell';
|
||||
|
||||
/**
|
||||
* Attaches an ng-template to a cell and shows it when instructed to by the
|
||||
* EditEventDispatcher service.
|
||||
@@ -61,16 +68,7 @@ export class MatPopoverEditTabOut<C> extends CdkPopoverEditTabOut<C> {
|
||||
|
||||
/**
|
||||
* A structural directive that shows its contents when the table row containing
|
||||
* it is hovered.
|
||||
*
|
||||
* Note that the contents of this directive are invisible to screen readers.
|
||||
* Typically this is used to show a button that launches the edit popup, which
|
||||
* is ok because screen reader users can trigger edit by pressing Enter on a focused
|
||||
* table cell.
|
||||
*
|
||||
* If this directive contains buttons for functionality other than opening edit then
|
||||
* care should be taken to make sure that this functionality is also exposed in
|
||||
* an accessible way.
|
||||
* it is hovered or when an element in the row has focus.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[matRowHoverContent]',
|
||||
@@ -78,24 +76,27 @@ export class MatPopoverEditTabOut<C> extends CdkPopoverEditTabOut<C> {
|
||||
export class MatRowHoverContent extends CdkRowHoverContent {
|
||||
protected initElement(element: HTMLElement) {
|
||||
super.initElement(element);
|
||||
element.classList.add('mat-row-hover-content');
|
||||
element.classList.add(MAT_ROW_HOVER_CLASS);
|
||||
}
|
||||
|
||||
protected prepareElement(element: HTMLElement) {
|
||||
super.prepareElement(element);
|
||||
protected makeElementHiddenButFocusable(element: HTMLElement): void {
|
||||
element.classList.remove(MAT_ROW_HOVER_ANIMATE_CLASS);
|
||||
}
|
||||
|
||||
protected makeElementVisible(element: HTMLElement): void {
|
||||
_closest(this.elementRef.nativeElement!, _CELL_SELECTOR)!
|
||||
.classList.add(MAT_ROW_HOVER_CELL_CLASS);
|
||||
|
||||
const RTL_CLASS = 'mat-row-hover-content-rtl';
|
||||
if (this.services.directionality.value === 'rtl') {
|
||||
element.classList.add(RTL_CLASS);
|
||||
element.classList.add(MAT_ROW_HOVER_RTL_CLASS);
|
||||
} else {
|
||||
element.classList.remove(RTL_CLASS);
|
||||
element.classList.remove(MAT_ROW_HOVER_RTL_CLASS);
|
||||
}
|
||||
|
||||
const ANIMATE_CLASS = 'mat-row-hover-content-visible';
|
||||
element.classList.remove(ANIMATE_CLASS);
|
||||
element.classList.remove(MAT_ROW_HOVER_ANIMATE_CLASS);
|
||||
this.services.ngZone.runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
element.classList.add(ANIMATE_CLASS);
|
||||
element.classList.add(MAT_ROW_HOVER_ANIMATE_CLASS);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user