Skip to content

Commit b163b64

Browse files
kseamonjelbourn
authored andcommitted
feat(popover-edit): Ability to disable edit on specific cells (#18273)
1 parent efa8263 commit b163b64

File tree

9 files changed

+116
-20
lines changed

9 files changed

+116
-20
lines changed

src/cdk-experimental/popover-edit/edit-event-dispatcher.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const enum HoverContentState {
4747
*/
4848
@Injectable()
4949
export class EditEventDispatcher {
50-
/** A subject that indicates which table cell is currently editing. */
50+
/** A subject that indicates which table cell is currently editing (unless it is disabled). */
5151
readonly editing = new Subject<Element|null>();
5252

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

65+
// TODO: Use WeakSet once IE11 support is dropped.
66+
/**
67+
* Tracks the currently disabled editable cells - edit calls will be ignored
68+
* for these cells.
69+
*/
70+
readonly disabledCells = new WeakMap<Element, boolean>();
71+
6572
/** The EditRef for the currently active edit lens (if any). */
6673
get editRef(): EditRef<any>|null {
6774
return this._editRef;
@@ -81,9 +88,14 @@ export class EditEventDispatcher {
8188
this._distinctUntilChanged as MonoTypeOperatorFunction<Element|null>,
8289
);
8390

91+
readonly editingAndEnabled = this.editing.pipe(
92+
filter(cell => cell == null || !this.disabledCells.has(cell)),
93+
share(),
94+
);
95+
8496
/** An observable that emits the row containing focus or an active edit. */
8597
readonly editingOrFocused = combineLatest([
86-
this.editing.pipe(
98+
this.editingAndEnabled.pipe(
8799
map(cell => closest(cell, ROW_SELECTOR)),
88100
this._startWithNull,
89101
),
@@ -126,7 +138,7 @@ export class EditEventDispatcher {
126138
share(),
127139
);
128140

129-
private readonly _editingDistinct = this.editing.pipe(
141+
private readonly _editingAndEnabledDistinct = this.editingAndEnabled.pipe(
130142
distinctUntilChanged(),
131143
this._enterZone(),
132144
share(),
@@ -138,7 +150,7 @@ export class EditEventDispatcher {
138150
private _lastSeenRowHoverOrFocus: Observable<HoverContentState>|null = null;
139151

140152
constructor(private readonly _ngZone: NgZone) {
141-
this._editingDistinct.subscribe(cell => {
153+
this._editingAndEnabledDistinct.subscribe(cell => {
142154
this._currentlyEditing = cell;
143155
});
144156
}
@@ -150,7 +162,7 @@ export class EditEventDispatcher {
150162
editingCell(element: Element|EventTarget): Observable<boolean> {
151163
let cell: Element|null = null;
152164

153-
return this._editingDistinct.pipe(
165+
return this._editingAndEnabledDistinct.pipe(
154166
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
155167
this._distinctUntilChanged as MonoTypeOperatorFunction<boolean>,
156168
);

src/cdk-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ const CELL_TEMPLATE = `
5252
</span>
5353
`;
5454

55-
const POPOVER_EDIT_DIRECTIVE_NAME = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`;
55+
const POPOVER_EDIT_DIRECTIVE_NAME = `
56+
[cdkPopoverEdit]="nameEdit"
57+
[cdkPopoverEditColspan]="colspan"
58+
[cdkPopoverEditDisabled]="nameEditDisabled"
59+
`;
5660

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

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

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

74+
nameEditDisabled = false;
7075
ignoreSubmitUnlessValid = true;
7176
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
7277
colspan: CdkPopoverEditColspan = {};
@@ -376,9 +381,7 @@ describe('CDK Popover Edit', () => {
376381
expect(component.hoverContentStateForRow(rows.length - 1))
377382
.toBe(HoverContentState.FOCUSABLE);
378383
}));
379-
});
380384

381-
describe('triggering edit', () => {
382385
it('shows and hides on-hover content only after a delay', fakeAsync(() => {
383386
const [row0, row1] = component.getRows();
384387
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
@@ -478,11 +481,35 @@ describe('CDK Popover Edit', () => {
478481
expect(component.lensIsOpen()).toBe(true);
479482
clearLeftoverTimers();
480483
}));
484+
485+
it('does not trigger edit when disabled', fakeAsync(() => {
486+
component.nameEditDisabled = true;
487+
fixture.detectChanges();
488+
489+
// Uses Enter to open the lens.
490+
component.openLens();
491+
492+
expect(component.lensIsOpen()).toBe(false);
493+
clearLeftoverTimers();
494+
}));
481495
});
482496

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

500+
describe('tabindex', () => {
501+
it('sets tabindex to 0 on editable cells', () => {
502+
expect(component.getEditCell().getAttribute('tabindex')).toBe('0');
503+
});
504+
505+
it('unsets tabindex to 0 on disabled cells', () => {
506+
component.nameEditDisabled = true;
507+
fixture.detectChanges();
508+
509+
expect(component.getEditCell().hasAttribute('tabindex')).toBe(false);
510+
});
511+
});
512+
486513
describe('arrow keys', () => {
487514
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
488515
dispatchKeyboardEvent(cell, 'keydown', keyCode, undefined, cell);

src/cdk-experimental/popover-edit/table-directives.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,16 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
148148
}
149149

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

156156
const POPOVER_EDIT_INPUTS = [
157157
'template: cdkPopoverEdit',
158158
'context: cdkPopoverEditContext',
159159
'colspan: cdkPopoverEditColspan',
160+
'disabled: cdkPopoverEditDisabled',
160161
];
161162

162163
/**
@@ -200,6 +201,22 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
200201
}
201202
private _colspan: CdkPopoverEditColspan = {};
202203

204+
/** Whether popover edit is disabled for this cell. */
205+
get disabled(): boolean {
206+
return this._disabled;
207+
}
208+
set disabled(value: boolean) {
209+
this._disabled = value;
210+
211+
if (value) {
212+
this.services.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!);
213+
this.services.editEventDispatcher.disabledCells.set(this.elementRef.nativeElement!, true);
214+
} else {
215+
this.services.editEventDispatcher.disabledCells.delete(this.elementRef.nativeElement!);
216+
}
217+
}
218+
private _disabled = false;
219+
203220
protected focusTrap?: FocusTrap;
204221
protected overlayRef?: OverlayRef;
205222
protected readonly destroyed = new Subject<void>();

src/components-examples/material-experimental/popover-edit/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ng_module(
1313
deps = [
1414
"//src/material-experimental/popover-edit",
1515
"//src/material/button",
16+
"//src/material/checkbox",
1617
"//src/material/icon",
1718
"//src/material/input",
1819
"//src/material/list",

src/components-examples/material-experimental/popover-edit/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {CommonModule} from '@angular/common';
33
import {FormsModule} from '@angular/forms';
44
import {MatPopoverEditModule} from '@angular/material-experimental/popover-edit';
55
import {MatButtonModule} from '@angular/material/button';
6+
import {MatCheckboxModule} from '@angular/material/checkbox';
67
import {MatIconModule} from '@angular/material/icon';
78
import {MatInputModule} from '@angular/material/input';
89
import {MatListModule} from '@angular/material/list';
@@ -37,6 +38,7 @@ const EXAMPLES = [
3738
imports: [
3839
CommonModule,
3940
MatButtonModule,
41+
MatCheckboxModule,
4042
MatIconModule,
4143
MatInputModule,
4244
MatListModule,

src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.html

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,14 @@
3838

3939
<!-- Name Column -->
4040
<ng-container matColumnDef="name">
41-
<th mat-header-cell *matHeaderCellDef> Name </th>
41+
<th mat-header-cell *matHeaderCellDef>
42+
Name
43+
<mat-checkbox
44+
[(ngModel)]="nameEditEnabled">Edit enabled</mat-checkbox>
45+
</th>
4246
<td mat-cell *matCellDef="let element"
43-
[matPopoverEdit]="nameEdit">
47+
[matPopoverEdit]="nameEdit"
48+
[matPopoverEditDisabled]="!nameEditEnabled">
4449
{{element.name}}
4550

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

68-
<span *matRowHoverContent>
69-
<button mat-icon-button matEditOpen><mat-icon>edit</mat-icon></button>
70-
</span>
73+
<ng-container *ngIf="nameEditEnabled">
74+
<span *matRowHoverContent>
75+
<button mat-icon-button matEditOpen><mat-icon>edit</mat-icon></button>
76+
</span>
77+
</ng-container>
7178
</td>
7279
</ng-container>
7380

src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export class PopoverEditMatTableExample {
7878
['position', 'name', 'type', 'weight', 'symbol', 'fantasyCounterpart'];
7979
dataSource = new ExampleDataSource();
8080

81+
nameEditEnabled = true;
82+
8183
readonly TYPES = TYPES;
8284
readonly FANTASY_ELEMENTS = FANTASY_ELEMENTS;
8385

src/material-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ const CELL_TEMPLATE = `
5050
</span>
5151
`;
5252

53-
const POPOVER_EDIT_DIRECTIVE_NAME = `[matPopoverEdit]="nameEdit" [matPopoverEditColspan]="colspan"`;
53+
const POPOVER_EDIT_DIRECTIVE_NAME = `
54+
[matPopoverEdit]="nameEdit"
55+
[matPopoverEditColspan]="colspan"
56+
[matPopoverEditDisabled]="nameEditDisabled"
57+
`;
5458

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

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

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

72+
nameEditDisabled = false;
6873
ignoreSubmitUnlessValid = true;
6974
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
7075
colspan: CdkPopoverEditColspan = {};
@@ -308,9 +313,7 @@ describe('Material Popover Edit', () => {
308313
expect(component.hoverContentStateForRow(rows.length - 1))
309314
.toBe(HoverContentState.FOCUSABLE);
310315
}));
311-
});
312316

313-
describe('triggering edit', () => {
314317
it('shows and hides on-hover content only after a delay', fakeAsync(() => {
315318
const [row0, row1] = component.getRows();
316319
row0.dispatchEvent(new Event('mouseover', {bubbles: true}));
@@ -410,11 +413,35 @@ describe('Material Popover Edit', () => {
410413
expect(component.lensIsOpen()).toBe(true);
411414
clearLeftoverTimers();
412415
}));
416+
417+
it('does not trigger edit when disabled', fakeAsync(() => {
418+
component.nameEditDisabled = true;
419+
fixture.detectChanges();
420+
421+
// Uses Enter to open the lens.
422+
component.openLens();
423+
424+
expect(component.lensIsOpen()).toBe(false);
425+
clearLeftoverTimers();
426+
}));
413427
});
414428

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

432+
describe('tabindex', () => {
433+
it('sets tabindex to 0 on editable cells', () => {
434+
expect(component.getEditCell().getAttribute('tabindex')).toBe('0');
435+
});
436+
437+
it('unsets tabindex to 0 on disabled cells', () => {
438+
component.nameEditDisabled = true;
439+
fixture.detectChanges();
440+
441+
expect(component.getEditCell().hasAttribute('tabindex')).toBe(false);
442+
});
443+
});
444+
418445
describe('arrow keys', () => {
419446
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
420447
dispatchKeyboardEvent(cell, 'keydown', keyCode, undefined, cell);

src/material-experimental/popover-edit/table-directives.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import {
1616
} from '@angular/cdk-experimental/popover-edit';
1717

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

2424
const POPOVER_EDIT_INPUTS = [
2525
'template: matPopoverEdit',
2626
'context: matPopoverEditContext',
2727
'colspan: matPopoverEditColspan',
28+
'disabled: matPopoverEditDisabled',
2829
];
2930

3031
const EDIT_PANE_CLASS = 'mat-edit-pane';

0 commit comments

Comments
 (0)