Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
refactor(dialog): Split dialog Foundation#handleInteraction into #han…
Browse files Browse the repository at this point in the history
…dleClick/#handleKeydown. (#4655)

BREAKING CHANGE: Dialog `Foundation#handleInteraction` has been split into two methods: `#handleClick` and `#handleKeydown`.
  • Loading branch information
joyzhong authored and Matt Goo committed May 3, 2019
1 parent fac44b1 commit 7400b63
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 52 deletions.
8 changes: 5 additions & 3 deletions packages/mdc-dialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,16 +400,18 @@ Method Signature | Description
`setScrimClickAction(action: string)` | Sets the action reflected when the scrim is clicked. Setting to `''` disables closing the dialog via scrim click.
`getAutoStackButtons() => boolean` | Returns whether stacked/unstacked action button layout is automatically handled during layout logic.
`setAutoStackButtons(autoStack: boolean) => void` | Sets whether stacked/unstacked action button layout is automatically handled during layout logic.
`handleInteraction(event: Event)` | Handles `click` and `keydown` events on or within the dialog's root element
`handleDocumentKeydown(event: Event)` | Handles `keydown` events on or within the document while the dialog is open
`handleClick(event: MouseEvent)` | Handles `click` events on or within the dialog's root element.
`handleKeydown(event: KeyboardEvent)` | Handles `keydown` events on or within the dialog's root element.
`handleDocumentKeydown(event: Event)` | Handles `keydown` events on or within the document while the dialog is open.

#### Event Handlers

When wrapping the Dialog foundation, the following events must be bound to the indicated foundation methods:

Event | Target | Foundation Handler | Register | Deregister
--- | --- | --- | --- | ---
`click` | `.mdc-dialog` (root) | `handleInteraction` | During initialization | During destruction
`click` | `.mdc-dialog` (root) | `handleClick` | During initialization | During destruction
`keydown` | `.mdc-dialog` (root) | `handleKeydown` | During initialization | During destruction
`keydown` | `document` | `handleDocumentKeydown` | On `MDCDialog:opening` | On `MDCDialog:closing`
`resize` | `window` | `layout` | On `MDCDialog:opening` | On `MDCDialog:closing`
`orientationchange` | `window` | `layout` | On `MDCDialog:opening` | On `MDCDialog:closing`
Expand Down
14 changes: 8 additions & 6 deletions packages/mdc-dialog/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export class MDCDialog extends MDCComponent<MDCDialogFoundation> {
private focusTrap_!: FocusTrap; // assigned in initialSyncWithDOM()
private focusTrapFactory_?: MDCDialogFocusTrapFactory; // assigned in initialize()

private handleInteraction_!: SpecificEventListener<'click' | 'keydown'>; // assigned in initialSyncWithDOM()
private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM()
private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()
private handleDocumentKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()
private handleLayout_!: EventListener; // assigned in initialSyncWithDOM()
private handleOpening_!: EventListener; // assigned in initialSyncWithDOM()
Expand Down Expand Up @@ -107,7 +108,8 @@ export class MDCDialog extends MDCComponent<MDCDialogFoundation> {
initialSyncWithDOM() {
this.focusTrap_ = util.createFocusTrapInstance(this.container_, this.focusTrapFactory_, this.initialFocusEl_);

this.handleInteraction_ = this.foundation_.handleInteraction.bind(this.foundation_);
this.handleClick_ = this.foundation_.handleClick.bind(this.foundation_);
this.handleKeydown_ = this.foundation_.handleKeydown.bind(this.foundation_);
this.handleDocumentKeydown_ = this.foundation_.handleDocumentKeydown.bind(this.foundation_);
this.handleLayout_ = this.layout.bind(this);

Expand All @@ -121,15 +123,15 @@ export class MDCDialog extends MDCComponent<MDCDialogFoundation> {
document.removeEventListener('keydown', this.handleDocumentKeydown_);
};

this.listen('click', this.handleInteraction_);
this.listen('keydown', this.handleInteraction_);
this.listen('click', this.handleClick_);
this.listen('keydown', this.handleKeydown_);
this.listen(strings.OPENING_EVENT, this.handleOpening_);
this.listen(strings.CLOSING_EVENT, this.handleClosing_);
}

destroy() {
this.unlisten('click', this.handleInteraction_);
this.unlisten('keydown', this.handleInteraction_);
this.unlisten('click', this.handleClick_);
this.unlisten('keydown', this.handleKeydown_);
this.unlisten(strings.OPENING_EVENT, this.handleOpening_);
this.unlisten(strings.CLOSING_EVENT, this.handleClosing_);
this.handleClosing_();
Expand Down
35 changes: 24 additions & 11 deletions packages/mdc-dialog/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,26 +176,39 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
});
}

handleInteraction(evt: MouseEvent | KeyboardEvent) {
const isClick = evt.type === 'click';
const isEnter = (evt as KeyboardEvent).key === 'Enter' || (evt as KeyboardEvent).keyCode === 13;
const isSpace = (evt as KeyboardEvent).key === 'Space' || (evt as KeyboardEvent).keyCode === 32;
/** Closes the dialog if scrim or action button click. */
handleClick(evt: MouseEvent) {
const isScrim = this.adapter_.eventTargetMatches(evt.target, strings.SCRIM_SELECTOR);
const isDefault = !this.adapter_.eventTargetMatches(evt.target, strings.SUPPRESS_DEFAULT_PRESS_SELECTOR);

// Check for scrim click first since it doesn't require querying ancestors
if (isClick && isScrim && this.scrimClickAction_ !== '') {
// Check for scrim click first since it doesn't require querying ancestors.
if (isScrim && this.scrimClickAction_ !== '') {
this.close(this.scrimClickAction_);
} else if (isClick || isSpace || isEnter) {
} else {
const action = this.adapter_.getActionFromEvent(evt);
if (action) {
this.close(action);
} else if (isEnter && isDefault) {
this.adapter_.clickDefaultButton();
}
}
}

handleKeydown(evt: KeyboardEvent) {
const isEnter = evt.key === 'Enter' || evt.keyCode === 13;
if (!isEnter) {
return;
}
const action = this.adapter_.getActionFromEvent(evt);
if (action) {
// Action button callback is handled in `handleClick`,
// since space/enter keydowns on buttons trigger click events.
return;
}

const isDefault = !this.adapter_.eventTargetMatches(
evt.target, strings.SUPPRESS_DEFAULT_PRESS_SELECTOR);
if (isEnter && isDefault) {
this.adapter_.clickDefaultButton();
}
}

handleDocumentKeydown(evt: KeyboardEvent) {
const isEscape = evt.key === 'Escape' || evt.keyCode === 27;
if (isEscape && this.escapeKeyAction_ !== '') {
Expand Down
61 changes: 33 additions & 28 deletions test/unit/mdc-dialog/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ const ENTER_EVENTS = [
{type: 'keydown', keyCode: 13, target: {}},
];

const INTERACTION_EVENTS = [
{type: 'click', target: {}},
{type: 'keydown', key: 'Space', target: {}},
{type: 'keydown', keyCode: 32, target: {}},
].concat(ENTER_EVENTS);

suite('MDCDialogFoundation');

test('exports cssClasses', () => {
Expand Down Expand Up @@ -359,90 +353,101 @@ test('#layout removes scrollable class when content is not scrollable', () => {
td.verify(mockAdapter.removeClass(cssClasses.SCROLLABLE));
});

test(`interaction closes dialog when ${strings.ACTION_ATTRIBUTE} attribute is present`, () => {
test(`#handleClick: Click closes dialog when ${strings.ACTION_ATTRIBUTE} attribute is present`, () => {
const {foundation, mockAdapter} = setupTest();
const action = 'action';
foundation.close = td.func('close');

INTERACTION_EVENTS.forEach((event) => {
td.when(mockAdapter.getActionFromEvent(event)).thenReturn(action);
foundation.open();
foundation.handleInteraction(event);
const event = {target: {}};
td.when(mockAdapter.getActionFromEvent(event)).thenReturn(action);
foundation.open();
foundation.handleClick(event);

td.verify(foundation.close(action));
td.reset();
});
td.verify(foundation.close(action));
});

test('interaction does not close dialog with action for non-activation keys', () => {
test('#handleKeydown: Keydown does not close dialog with action for non-activation keys', () => {
const {foundation, mockAdapter} = setupTest();
const action = 'action';
const event = {type: 'keydown', key: 'Escape', target: {}};
const event = {type: 'keydown', key: 'Shift', target: {}};
foundation.close = td.func('close');
td.when(mockAdapter.getActionFromEvent(event)).thenReturn(action);

foundation.open();
foundation.handleInteraction(event);
foundation.handleKeydown(event);

td.verify(foundation.close(action), {times: 0});
});

test(`interaction does nothing when ${strings.ACTION_ATTRIBUTE} attribute is not present`, () => {
test(`#handleClick: Click does nothing when ${strings.ACTION_ATTRIBUTE} attribute is not present`, () => {
const {foundation, mockAdapter} = setupTest();
foundation.close = td.func('close');

INTERACTION_EVENTS.forEach((event) => {
const event = {target: {}};
td.when(mockAdapter.getActionFromEvent(event)).thenReturn('');
foundation.open();
foundation.handleClick(event);

td.verify(foundation.close(td.matchers.isA(String)), {times: 0});
});

test(`#handleKeydown: Keydown does nothing when ${strings.ACTION_ATTRIBUTE} attribute is not present`, () => {
const {foundation, mockAdapter} = setupTest();
foundation.close = td.func('close');

ENTER_EVENTS.forEach((event) => {
td.when(mockAdapter.getActionFromEvent(event)).thenReturn('');
foundation.open();
foundation.handleInteraction(event);
foundation.handleKeydown(event);

td.verify(foundation.close(td.matchers.isA(String)), {times: 0});
td.reset();
});
});

test('enter keydown calls adapter.clickDefaultButton', () => {
test('#handleKeydown: Enter keydown calls adapter.clickDefaultButton', () => {
const {foundation, mockAdapter} = setupTest();

ENTER_EVENTS.forEach((event) => {
foundation.handleInteraction(event);
foundation.handleKeydown(event);
td.verify(mockAdapter.clickDefaultButton());
td.reset();
});
});

test('enter keydown does not call adapter.clickDefaultButton when it should be suppressed', () => {
test('#handleKeydown: Enter keydown does not call adapter.clickDefaultButton when it should be suppressed', () => {
const {foundation, mockAdapter} = setupTest();

ENTER_EVENTS.forEach((event) => {
td.when(mockAdapter.eventTargetMatches(event.target, strings.SUPPRESS_DEFAULT_PRESS_SELECTOR)).thenReturn(true);
foundation.handleInteraction(event);
foundation.handleKeydown(event);
td.verify(mockAdapter.clickDefaultButton(), {times: 0});
td.reset();
});
});

test(`click closes dialog when ${strings.SCRIM_SELECTOR} selector matches`, () => {
test(`#handleClick: Click closes dialog when ${strings.SCRIM_SELECTOR} selector matches`, () => {
const {foundation, mockAdapter} = setupTest();
const evt = {type: 'click', target: {}};
foundation.close = td.func('close');
td.when(mockAdapter.eventTargetMatches(evt.target, strings.SCRIM_SELECTOR)).thenReturn(true);

foundation.open();
foundation.handleInteraction(evt);
foundation.handleClick(evt);

td.verify(foundation.close(foundation.getScrimClickAction()));
});

test(`click does nothing when ${strings.SCRIM_SELECTOR} class is present but scrimClickAction is empty string`, () => {
test(`#handleClick: Click does nothing when ${strings.SCRIM_SELECTOR} class is present but scrimClickAction is
empty string`, () => {
const {foundation, mockAdapter} = setupTest();
const evt = {type: 'click', target: {}};
foundation.close = td.func('close');
td.when(mockAdapter.eventTargetMatches(evt.target, strings.SCRIM_SELECTOR)).thenReturn(true);

foundation.setScrimClickAction('');
foundation.open();
foundation.handleInteraction(evt);
foundation.handleClick(evt);

td.verify(foundation.close(td.matchers.isA(String)), {times: 0});
});
Expand Down
8 changes: 4 additions & 4 deletions test/unit/mdc-dialog/mdc-dialog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,29 +108,29 @@ test('attachTo throws an error when container element is missing', () => {
test('#initialSyncWithDOM registers click handler on the root element', () => {
const {root, component, mockFoundation} = setupTestWithMocks();
domEvents.emit(root, 'click');
td.verify(mockFoundation.handleInteraction(td.matchers.isA(Event)), {times: 1});
td.verify(mockFoundation.handleClick(td.matchers.isA(Event)), {times: 1});
component.destroy();
});

test('#initialSyncWithDOM registers keydown handler on the root element', () => {
const {root, component, mockFoundation} = setupTestWithMocks();
domEvents.emit(root, 'keydown');
td.verify(mockFoundation.handleInteraction(td.matchers.isA(Event)), {times: 1});
td.verify(mockFoundation.handleKeydown(td.matchers.isA(Event)), {times: 1});
component.destroy();
});

test('#destroy deregisters click handler on the root element', () => {
const {root, component, mockFoundation} = setupTestWithMocks();
component.destroy();
domEvents.emit(root, 'click');
td.verify(mockFoundation.handleInteraction(td.matchers.isA(Event)), {times: 0});
td.verify(mockFoundation.handleClick(td.matchers.isA(Event)), {times: 0});
});

test('#destroy deregisters keydown handler on the root element', () => {
const {root, component, mockFoundation} = setupTestWithMocks();
component.destroy();
domEvents.emit(root, 'keydown');
td.verify(mockFoundation.handleInteraction(td.matchers.isA(Event)), {times: 0});
td.verify(mockFoundation.handleKeydown(td.matchers.isA(Event)), {times: 0});
});

test(`${strings.OPENING_EVENT} registers document keydown handler and ${strings.CLOSING_EVENT} deregisters it`, () => {
Expand Down

0 comments on commit 7400b63

Please sign in to comment.