Skip to content

feat(popover-edit): Ability to disable edit on specific cells #18273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/cdk-experimental/popover-edit/edit-event-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const enum HoverContentState {
*/
@Injectable()
export class EditEventDispatcher {
/** A subject that indicates which table cell is currently editing. */
/** A subject that indicates which table cell is currently editing (unless it is disabled). */
readonly editing = new Subject<Element|null>();

/** A subject that indicates which table row is currently hovered. */
Expand All @@ -62,6 +62,13 @@ export class EditEventDispatcher {
/** A subject that emits mouse move events from the table indicating the targeted row. */
readonly mouseMove = new Subject<Element|null>();

// TODO: Use WeakSet once IE11 support is dropped.
/**
* Tracks the currently disabled editable cells - edit calls will be ignored
* for these cells.
*/
readonly disabledCells = new WeakMap<Element, boolean>();

/** The EditRef for the currently active edit lens (if any). */
get editRef(): EditRef<any>|null {
return this._editRef;
Expand All @@ -81,9 +88,14 @@ export class EditEventDispatcher {
this._distinctUntilChanged as MonoTypeOperatorFunction<Element|null>,
);

readonly editingAndEnabled = this.editing.pipe(
filter(cell => cell == null || !this.disabledCells.has(cell)),
share(),
);

/** An observable that emits the row containing focus or an active edit. */
readonly editingOrFocused = combineLatest([
this.editing.pipe(
this.editingAndEnabled.pipe(
map(cell => closest(cell, ROW_SELECTOR)),
this._startWithNull,
),
Expand Down Expand Up @@ -126,7 +138,7 @@ export class EditEventDispatcher {
share(),
);

private readonly _editingDistinct = this.editing.pipe(
private readonly _editingAndEnabledDistinct = this.editingAndEnabled.pipe(
distinctUntilChanged(),
this._enterZone(),
share(),
Expand All @@ -138,7 +150,7 @@ export class EditEventDispatcher {
private _lastSeenRowHoverOrFocus: Observable<HoverContentState>|null = null;

constructor(private readonly _ngZone: NgZone) {
this._editingDistinct.subscribe(cell => {
this._editingAndEnabledDistinct.subscribe(cell => {
this._currentlyEditing = cell;
});
}
Expand All @@ -150,7 +162,7 @@ export class EditEventDispatcher {
editingCell(element: Element|EventTarget): Observable<boolean> {
let cell: Element|null = null;

return this._editingDistinct.pipe(
return this._editingAndEnabledDistinct.pipe(
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
this._distinctUntilChanged as MonoTypeOperatorFunction<boolean>,
);
Expand Down
33 changes: 30 additions & 3 deletions src/cdk-experimental/popover-edit/popover-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ const CELL_TEMPLATE = `
</span>
`;

const POPOVER_EDIT_DIRECTIVE_NAME = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`;
const POPOVER_EDIT_DIRECTIVE_NAME = `
[cdkPopoverEdit]="nameEdit"
[cdkPopoverEditColspan]="colspan"
[cdkPopoverEditDisabled]="nameEditDisabled"
`;

const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit" cdkPopoverEditTabOut`;

Expand All @@ -67,6 +71,7 @@ abstract class BaseTestComponent {

preservedValues = new FormValueContainer<PeriodicElement, {'name': string}>();

nameEditDisabled = false;
ignoreSubmitUnlessValid = true;
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
colspan: CdkPopoverEditColspan = {};
Expand Down Expand Up @@ -376,9 +381,7 @@ describe('CDK Popover Edit', () => {
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}));
Expand Down Expand Up @@ -478,11 +481,35 @@ describe('CDK Popover Edit', () => {
expect(component.lensIsOpen()).toBe(true);
clearLeftoverTimers();
}));

it('does not trigger edit when disabled', fakeAsync(() => {
component.nameEditDisabled = true;
fixture.detectChanges();

// Uses Enter to open the lens.
component.openLens();

expect(component.lensIsOpen()).toBe(false);
clearLeftoverTimers();
}));
});

describe('focus manipulation', () => {
const getRowCells = () => component.getRows().map(getCells);

describe('tabindex', () => {
it('sets tabindex to 0 on editable cells', () => {
expect(component.getEditCell().getAttribute('tabindex')).toBe('0');
});

it('unsets tabindex to 0 on disabled cells', () => {
component.nameEditDisabled = true;
fixture.detectChanges();

expect(component.getEditCell().hasAttribute('tabindex')).toBe(false);
});
});

describe('arrow keys', () => {
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
dispatchKeyboardEvent(cell, 'keydown', keyCode, undefined, cell);
Expand Down
21 changes: 19 additions & 2 deletions src/cdk-experimental/popover-edit/table-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,16 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
}

const POPOVER_EDIT_HOST_BINDINGS = {
'tabIndex': '0',
'[attr.tabindex]': 'disabled ? null : 0',
'class': 'cdk-popover-edit-cell',
'[attr.aria-haspopup]': 'true',
'[attr.aria-haspopup]': '!disabled',
};

const POPOVER_EDIT_INPUTS = [
'template: cdkPopoverEdit',
'context: cdkPopoverEditContext',
'colspan: cdkPopoverEditColspan',
'disabled: cdkPopoverEditDisabled',
];

/**
Expand Down Expand Up @@ -200,6 +201,22 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
}
private _colspan: CdkPopoverEditColspan = {};

/** Whether popover edit is disabled for this cell. */
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;

if (value) {
this.services.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!);
this.services.editEventDispatcher.disabledCells.set(this.elementRef.nativeElement!, true);
} else {
this.services.editEventDispatcher.disabledCells.delete(this.elementRef.nativeElement!);
}
}
private _disabled = false;

protected focusTrap?: FocusTrap;
protected overlayRef?: OverlayRef;
protected readonly destroyed = new Subject<void>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ng_module(
deps = [
"//src/material-experimental/popover-edit",
"//src/material/button",
"//src/material/checkbox",
"//src/material/icon",
"//src/material/input",
"//src/material/list",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {MatPopoverEditModule} from '@angular/material-experimental/popover-edit';
import {MatButtonModule} from '@angular/material/button';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatListModule} from '@angular/material/list';
Expand Down Expand Up @@ -37,6 +38,7 @@ const EXAMPLES = [
imports: [
CommonModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatInputModule,
MatListModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@

<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<th mat-header-cell *matHeaderCellDef>
Name
<mat-checkbox
[(ngModel)]="nameEditEnabled">Edit enabled</mat-checkbox>
</th>
<td mat-cell *matCellDef="let element"
[matPopoverEdit]="nameEdit">
[matPopoverEdit]="nameEdit"
[matPopoverEditDisabled]="!nameEditEnabled">
{{element.name}}

<!-- This edit is defined in the cell and can implicitly access element -->
Expand All @@ -65,9 +70,11 @@ <h2 mat-edit-title>Name</h2>
</div>
</ng-template>

<span *matRowHoverContent>
<button mat-icon-button matEditOpen><mat-icon>edit</mat-icon></button>
</span>
<ng-container *ngIf="nameEditEnabled">
<span *matRowHoverContent>
<button mat-icon-button matEditOpen><mat-icon>edit</mat-icon></button>
</span>
</ng-container>
</td>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export class PopoverEditMatTableExample {
['position', 'name', 'type', 'weight', 'symbol', 'fantasyCounterpart'];
dataSource = new ExampleDataSource();

nameEditEnabled = true;

readonly TYPES = TYPES;
readonly FANTASY_ELEMENTS = FANTASY_ELEMENTS;

Expand Down
33 changes: 30 additions & 3 deletions src/material-experimental/popover-edit/popover-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ const CELL_TEMPLATE = `
</span>
`;

const POPOVER_EDIT_DIRECTIVE_NAME = `[matPopoverEdit]="nameEdit" [matPopoverEditColspan]="colspan"`;
const POPOVER_EDIT_DIRECTIVE_NAME = `
[matPopoverEdit]="nameEdit"
[matPopoverEditColspan]="colspan"
[matPopoverEditDisabled]="nameEditDisabled"
`;

const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[matPopoverEdit]="weightEdit" matPopoverEditTabOut`;

Expand All @@ -65,6 +69,7 @@ abstract class BaseTestComponent {

preservedValues = new FormValueContainer<PeriodicElement, {'name': string}>();

nameEditDisabled = false;
ignoreSubmitUnlessValid = true;
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
colspan: CdkPopoverEditColspan = {};
Expand Down Expand Up @@ -308,9 +313,7 @@ describe('Material Popover Edit', () => {
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}));
Expand Down Expand Up @@ -410,11 +413,35 @@ describe('Material Popover Edit', () => {
expect(component.lensIsOpen()).toBe(true);
clearLeftoverTimers();
}));

it('does not trigger edit when disabled', fakeAsync(() => {
component.nameEditDisabled = true;
fixture.detectChanges();

// Uses Enter to open the lens.
component.openLens();

expect(component.lensIsOpen()).toBe(false);
clearLeftoverTimers();
}));
});

describe('focus manipulation', () => {
const getRowCells = () => component.getRows().map(getCells);

describe('tabindex', () => {
it('sets tabindex to 0 on editable cells', () => {
expect(component.getEditCell().getAttribute('tabindex')).toBe('0');
});

it('unsets tabindex to 0 on disabled cells', () => {
component.nameEditDisabled = true;
fixture.detectChanges();

expect(component.getEditCell().hasAttribute('tabindex')).toBe(false);
});
});

describe('arrow keys', () => {
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
dispatchKeyboardEvent(cell, 'keydown', keyCode, undefined, cell);
Expand Down
5 changes: 3 additions & 2 deletions src/material-experimental/popover-edit/table-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import {
} from '@angular/cdk-experimental/popover-edit';

const POPOVER_EDIT_HOST_BINDINGS = {
'tabIndex': '0',
'[attr.tabindex]': 'disabled ? null : 0',
'class': 'mat-popover-edit-cell',
'[attr.aria-haspopup]': 'true',
'[attr.aria-haspopup]': '!disabled',
};

const POPOVER_EDIT_INPUTS = [
'template: matPopoverEdit',
'context: matPopoverEditContext',
'colspan: matPopoverEditColspan',
'disabled: matPopoverEditDisabled',
];

const EDIT_PANE_CLASS = 'mat-edit-pane';
Expand Down