diff --git a/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts b/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts index 70c96ba20a5e..ff373b3a0094 100644 --- a/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts +++ b/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts @@ -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(); /** A subject that indicates which table row is currently hovered. */ @@ -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(); + // 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(); + /** The EditRef for the currently active edit lens (if any). */ get editRef(): EditRef|null { return this._editRef; @@ -81,9 +88,14 @@ export class EditEventDispatcher { this._distinctUntilChanged as MonoTypeOperatorFunction, ); + 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, ), @@ -126,7 +138,7 @@ export class EditEventDispatcher { share(), ); - private readonly _editingDistinct = this.editing.pipe( + private readonly _editingAndEnabledDistinct = this.editingAndEnabled.pipe( distinctUntilChanged(), this._enterZone(), share(), @@ -138,7 +150,7 @@ export class EditEventDispatcher { private _lastSeenRowHoverOrFocus: Observable|null = null; constructor(private readonly _ngZone: NgZone) { - this._editingDistinct.subscribe(cell => { + this._editingAndEnabledDistinct.subscribe(cell => { this._currentlyEditing = cell; }); } @@ -150,7 +162,7 @@ export class EditEventDispatcher { editingCell(element: Element|EventTarget): Observable { 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, ); diff --git a/src/cdk-experimental/popover-edit/popover-edit.spec.ts b/src/cdk-experimental/popover-edit/popover-edit.spec.ts index 955ccf11719c..fe7edee66585 100644 --- a/src/cdk-experimental/popover-edit/popover-edit.spec.ts +++ b/src/cdk-experimental/popover-edit/popover-edit.spec.ts @@ -52,7 +52,11 @@ const CELL_TEMPLATE = ` `; -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`; @@ -67,6 +71,7 @@ abstract class BaseTestComponent { preservedValues = new FormValueContainer(); + nameEditDisabled = false; ignoreSubmitUnlessValid = true; clickOutBehavior: PopoverEditClickOutBehavior = 'close'; colspan: CdkPopoverEditColspan = {}; @@ -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})); @@ -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); diff --git a/src/cdk-experimental/popover-edit/table-directives.ts b/src/cdk-experimental/popover-edit/table-directives.ts index e591ff379068..52f81fad219c 100644 --- a/src/cdk-experimental/popover-edit/table-directives.ts +++ b/src/cdk-experimental/popover-edit/table-directives.ts @@ -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', ]; /** @@ -200,6 +201,22 @@ export class CdkPopoverEdit 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(); diff --git a/src/components-examples/material-experimental/popover-edit/BUILD.bazel b/src/components-examples/material-experimental/popover-edit/BUILD.bazel index b54879aba6d0..a114662e9697 100644 --- a/src/components-examples/material-experimental/popover-edit/BUILD.bazel +++ b/src/components-examples/material-experimental/popover-edit/BUILD.bazel @@ -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", diff --git a/src/components-examples/material-experimental/popover-edit/index.ts b/src/components-examples/material-experimental/popover-edit/index.ts index 2911c1f66223..8f50c05f5b6d 100644 --- a/src/components-examples/material-experimental/popover-edit/index.ts +++ b/src/components-examples/material-experimental/popover-edit/index.ts @@ -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'; @@ -37,6 +38,7 @@ const EXAMPLES = [ imports: [ CommonModule, MatButtonModule, + MatCheckboxModule, MatIconModule, MatInputModule, MatListModule, diff --git a/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.html b/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.html index 19172c9d45e2..f622ae4fb336 100644 --- a/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.html +++ b/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.html @@ -38,9 +38,14 @@ - Name + + Name + Edit enabled + + [matPopoverEdit]="nameEdit" + [matPopoverEditDisabled]="!nameEditEnabled"> {{element.name}} @@ -65,9 +70,11 @@

Name

- - - + + + + +
diff --git a/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.ts b/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.ts index a3072170ef01..c222868d548a 100644 --- a/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.ts +++ b/src/components-examples/material-experimental/popover-edit/popover-edit-mat-table/popover-edit-mat-table-example.ts @@ -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; diff --git a/src/material-experimental/popover-edit/popover-edit.spec.ts b/src/material-experimental/popover-edit/popover-edit.spec.ts index f0b93d326ac9..268cdbc0489f 100644 --- a/src/material-experimental/popover-edit/popover-edit.spec.ts +++ b/src/material-experimental/popover-edit/popover-edit.spec.ts @@ -50,7 +50,11 @@ const CELL_TEMPLATE = ` `; -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`; @@ -65,6 +69,7 @@ abstract class BaseTestComponent { preservedValues = new FormValueContainer(); + nameEditDisabled = false; ignoreSubmitUnlessValid = true; clickOutBehavior: PopoverEditClickOutBehavior = 'close'; colspan: CdkPopoverEditColspan = {}; @@ -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})); @@ -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); diff --git a/src/material-experimental/popover-edit/table-directives.ts b/src/material-experimental/popover-edit/table-directives.ts index ec7e7df3f298..0d402075247f 100644 --- a/src/material-experimental/popover-edit/table-directives.ts +++ b/src/material-experimental/popover-edit/table-directives.ts @@ -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';