Skip to content

Commit

Permalink
feat(focus-trap,dialog): implement focus-trap and dialog directives
Browse files Browse the repository at this point in the history
  • Loading branch information
gjdev committed May 22, 2018
1 parent 87ab60e commit fdfa357
Show file tree
Hide file tree
Showing 29 changed files with 1,106 additions and 37 deletions.
68 changes: 39 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,42 @@ It integrates [Google's Material Components for the Web](https://github.com/mate

## Status

Component | Directives | Comments
------------------------ | --------- | --
button | [See demo](https://blox.src.zone/material/directives/button) |
card | [See demo](https://blox.src.zone/material/directives/card) |
checkbox | [See demo](https://blox.src.zone/material/directives/checkbox) |
dialog | |
drawer | [See demo](https://blox.src.zone/material/directives/drawer) |
elevation | [See demo](https://blox.src.zone/material/directives/elevation) |
fab | [See demo](https://blox.src.zone/material/directives/fab) |
form-field | Available | See demos for e.g. radio, and checkbox.
grid-list | |
icon-toggle | [See demo](https://blox.src.zone/material/directives/icon-toggle) |
linear-progress | [See demo](https://blox.src.zone/material/directives/linear-progress) |
list | [See demo](https://blox.src.zone/material/directives/list) |
menu | [See demo](https://blox.src.zone/material/directives/menu) |
radio | [See demo](https://blox.src.zone/material/directives/radio) |
ripple | [See demo](https://blox.src.zone/material/directives/ripple) |
select | [See demo](https://blox.src.zone/material/directives/select) |
slider | [See demo](https://blox.src.zone/material/directives/slider) |
snackbar | [See demo](https://blox.src.zone/material/directives/snackbar) |
switch | [See demo](https://blox.src.zone/material/directives/switch) |
tabs | [See demo](https://blox.src.zone/material/directives/tab) |
text-field | [See demo](https://blox.src.zone/material/directives/text-field) |
toolbar | [See demo](https://blox.src.zone/material/directives/toolbar) |

Note: the `@material` packages `layout-grid`, `theme`, and `typography` provide styling
(scss, css) only. As such they can be consumed directly from your Angular app, and we see
no reason to wrap their functionality in Angular components or directives.

Component | Documentation |
-------------------- | --------- |
button | [button docs & demo](https://blox.src.zone/material/directives/button) |
card | [card docs & demo](https://blox.src.zone/material/directives/card) |
checkbox | [checkbox docs & demo](https://blox.src.zone/material/directives/checkbox) |
chips | in tracker |
dialog | [dialog docs & demo](https://blox.src.zone/material/directives/drawer) |
drawer | [drawer docs & demo](https://blox.src.zone/material/directives/drawer) |
elevation | [elevation docs & demo](https://blox.src.zone/material/directives/elevation) |
fab | [fab docs & demo](https://blox.src.zone/material/directives/fab) |
form-field | see e.g. [radio docs & demo](https://blox.src.zone/material/directives/radio), and [checkbox docs & demo](https://blox.src.zone/material/directives/checkbox) |
~~grid-list~~ | deprecated by the Material Components Web team |
icon-toggle | [icon-toggle docs & demo](https://blox.src.zone/material/directives/icon-toggle) |
linear-progress | [linear-progress docs & demo](https://blox.src.zone/material/directives/linear-progress) |
list | [list docs & demo](https://blox.src.zone/material/directives/list) |
menu | [menu docs & demo](https://blox.src.zone/material/directives/menu) |
radio | [radio docs & demo](https://blox.src.zone/material/directives/radio) |
ripple | [ripple docs & demo](https://blox.src.zone/material/directives/ripple) |
select | [select docs & demo](https://blox.src.zone/material/directives/select) |
slider | [slider docs & demo](https://blox.src.zone/material/directives/slider) |
snackbar | [snackbar docs & demo](https://blox.src.zone/material/directives/snackbar) |
switch | [switch docs & demo](https://blox.src.zone/material/directives/switch) |
tabs | [tabs docs & demo](https://blox.src.zone/material/directives/tab) |
text-field | [text-field docs & demo](https://blox.src.zone/material/directives/text-field) |
toolbar | [toolbar docs & demo](https://blox.src.zone/material/directives/toolbar) |
top-app-bar | in tracker |

The following material-components-web packages provide styling (scss, css) only. As such they
can be consumed directly from your Angular app, and we see no reason to wrap their functionality
in Angular components or directives. Just use the styles and sass mixins as documented by the
material-components-web team:

Package | Documentation |
---------------------| --------- |
image-list | [image-list documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
layout-grid | [layout-grid documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
shape | [shape documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
theme | [theme documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
typography | [typography documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
6 changes: 3 additions & 3 deletions bundle/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test:watch": "karma start karma.conf.ts --single-run false"
},
"dependencies": {
"focus-trap": "^2.4.5",
"karma-junit-reporter": "^1.2.0",
"material-components-web": "^0.35.1"
},
Expand Down
21 changes: 21 additions & 0 deletions bundle/src/components/dialog/mdc.dialog.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @docs-private */
export interface MdcDialogAdapter {
addClass: (className: string) => void,
removeClass: (className: string) => void,
addBodyClass: (className: string) => void,
removeBodyClass: (className: string) => void,
eventTargetHasClass: (target: EventTarget, className: string) => boolean,
registerInteractionHandler: (evt: string, handler: EventListener) => void,
deregisterInteractionHandler: (evt: string, handler: EventListener) => void,
registerSurfaceInteractionHandler: (evt: string, handler: EventListener) => void,
deregisterSurfaceInteractionHandler: (evt: string, handler: EventListener) => void,
registerDocumentKeydownHandler: (handler: EventListener) => void,
deregisterDocumentKeydownHandler: (handler: EventListener) => void,
registerTransitionEndHandler: (handler: EventListener) => void,
deregisterTransitionEndHandler: (handler: EventListener) => void,
notifyAccept: () => void,
notifyCancel: () => void,
trapFocusOnSurface: () => void,
untrapFocusOnSurface: () => void,
isDialog: (el: Element) => boolean
}
143 changes: 143 additions & 0 deletions bundle/src/components/dialog/mdc.dialog.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { FOCUS_TRAP_DIRECTIVES } from '../focus-trap/mdc.focus-trap.directive';
import { MDC_EVENT_REGISTRY_PROVIDER } from '../../utils/mdc.event.registry';
import { DIALOG_DIRECTIVES, MdcDialogDirective, MdcDialogBodyDirective } from './mdc.dialog.directive';
import { MdcButtonDirective } from '../button/mdc.button.directive';
import { cancelledClick, booleanAttributeStyleTest } from '../../testutils/page.test';

const templateWithDialog = `
<button id="open" mdcButton (click)="dialog.open()">Open Dialog</button>
<aside id="dialog" #dialog="mdcDialog" mdcDialog mdcFocusTrap>
<div id="surface" mdcDialogSurface>
<header mdcDialogHeader>
<h2 mdcDialogHeaderTitle>Modal Dialog</h2>
</header>
<section mdcDialogBody [scrollable]="scrollable">
Dialog Body
</section>
<footer mdcDialogFooter>
<button *ngIf="cancelButton" id="cancel" mdcButton mdcDialogCancel>Decline</button>
<button *ngIf="acceptButton" id="accept" mdcButton mdcDialogAccept>Accept</button>
</footer>
</div>
<div mdcDialogBackdrop></div>
</aside>
`;

describe('MdcDialogDirective', () => {
@Component({
template: templateWithDialog
})
class TestComponent {
scrollable = false;
cancelButton = true;
acceptButton = true;
}

function setup() {
const fixture = TestBed.configureTestingModule({
providers: [MDC_EVENT_REGISTRY_PROVIDER],
declarations: [...DIALOG_DIRECTIVES, ...FOCUS_TRAP_DIRECTIVES, MdcButtonDirective, TestComponent]
}).createComponent(TestComponent);
fixture.detectChanges();
return { fixture };
}

it('should only display the dialog when opened', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const dialog = fixture.nativeElement.querySelector('#dialog');
const cancel = fixture.nativeElement.querySelector('#cancel');
const accept = fixture.nativeElement.querySelector('#accept');

expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
button.click();
expect(dialog.classList.contains('mdc-dialog--open')).toBe(true, 'dialog must be in opened state');
cancel.click();
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
button.click();
expect(dialog.classList.contains('mdc-dialog--open')).toBe(true, 'dialog must be in opened state');
accept.click();
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
}));

it('should trap focus to the dialog when opened', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const dialog = fixture.nativeElement.querySelector('#dialog');
const accept = fixture.nativeElement.querySelector('#accept');
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
button.click();
// focusTrap is activated on animation 'transitionend', so simulate that event
// (as tick() and friends won't wait for it):
fixture.nativeElement.querySelector('#surface').dispatchEvent(new TransitionEvent('transitionend', {}));
// clicks on the button should now be cancelled:
expect(cancelledClick(button)).toBe(true);
// clicks on buttons inside the dialog should not be cancelled:
expect(cancelledClick(accept)).toBe(false);
}));

it('should apply dialog button styling to buttons dynamically added', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const dialog = fixture.nativeElement.querySelector('#dialog');
const testComponent = fixture.debugElement.injector.get(TestComponent);
testComponent.cancelButton = false;
testComponent.acceptButton = false;
fixture.detectChanges();

button.click();
expect(fixture.nativeElement.querySelector('#cancel')).toBeNull();
testComponent.cancelButton = true;
testComponent.acceptButton = true;
fixture.detectChanges();
const cancel = fixture.nativeElement.querySelector('#cancel');
expect(cancel.classList).toContain('mdc-dialog__footer__button');
const accept = fixture.nativeElement.querySelector('#accept');
expect(accept.classList).toContain('mdc-dialog__footer__button');
expect(accept.classList).toContain('mdc-dialog__footer__button--accept');
}));

it('should emit the accept event', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const mdcDialog = fixture.debugElement.query(By.directive(MdcDialogDirective)).injector.get(MdcDialogDirective);
const accept = fixture.nativeElement.querySelector('#accept');
button.click();
let accepted = false;
mdcDialog.accept.subscribe(() => { accepted = true; });
accept.click();
expect(accepted).toBe(true);
}));

it('should emit the cancel event', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const mdcDialog = fixture.debugElement.query(By.directive(MdcDialogDirective)).injector.get(MdcDialogDirective);
const cancel = fixture.nativeElement.querySelector('#cancel');
button.click();
let canceled = false;
mdcDialog.cancel.subscribe(() => { canceled = true; });
cancel.click();
expect(canceled).toBe(true);
}));

it('should style the body according to the scrollable property', (() => {
const { fixture } = setup();
const button = fixture.nativeElement.querySelector('#open');
const dialog = fixture.nativeElement.querySelector('#dialog');
const testComponent = fixture.debugElement.injector.get(TestComponent);
const mdcDialogBody = fixture.debugElement.query(By.directive(MdcDialogBodyDirective)).injector.get(MdcDialogBodyDirective);

button.click();
booleanAttributeStyleTest(
fixture,
testComponent,
mdcDialogBody,
'scrollable',
'scrollable',
'mdc-dialog__body--scrollable');
}));
});
Loading

0 comments on commit fdfa357

Please sign in to comment.