Skip to content

Commit fdfa357

Browse files
committed
feat(focus-trap,dialog): implement focus-trap and dialog directives
1 parent 87ab60e commit fdfa357

29 files changed

+1106
-37
lines changed

README.md

+39-29
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,42 @@ It integrates [Google's Material Components for the Web](https://github.com./mate
1919

2020
## Status
2121

22-
Component | Directives | Comments
23-
------------------------ | --------- | --
24-
button | [See demo](https://blox.src.zone/material/directives/button) |
25-
card | [See demo](https://blox.src.zone/material/directives/card) |
26-
checkbox | [See demo](https://blox.src.zone/material/directives/checkbox) |
27-
dialog | |
28-
drawer | [See demo](https://blox.src.zone/material/directives/drawer) |
29-
elevation | [See demo](https://blox.src.zone/material/directives/elevation) |
30-
fab | [See demo](https://blox.src.zone/material/directives/fab) |
31-
form-field | Available | See demos for e.g. radio, and checkbox.
32-
grid-list | |
33-
icon-toggle | [See demo](https://blox.src.zone/material/directives/icon-toggle) |
34-
linear-progress | [See demo](https://blox.src.zone/material/directives/linear-progress) |
35-
list | [See demo](https://blox.src.zone/material/directives/list) |
36-
menu | [See demo](https://blox.src.zone/material/directives/menu) |
37-
radio | [See demo](https://blox.src.zone/material/directives/radio) |
38-
ripple | [See demo](https://blox.src.zone/material/directives/ripple) |
39-
select | [See demo](https://blox.src.zone/material/directives/select) |
40-
slider | [See demo](https://blox.src.zone/material/directives/slider) |
41-
snackbar | [See demo](https://blox.src.zone/material/directives/snackbar) |
42-
switch | [See demo](https://blox.src.zone/material/directives/switch) |
43-
tabs | [See demo](https://blox.src.zone/material/directives/tab) |
44-
text-field | [See demo](https://blox.src.zone/material/directives/text-field) |
45-
toolbar | [See demo](https://blox.src.zone/material/directives/toolbar) |
46-
47-
Note: the `@material` packages `layout-grid`, `theme`, and `typography` provide styling
48-
(scss, css) only. As such they can be consumed directly from your Angular app, and we see
49-
no reason to wrap their functionality in Angular components or directives.
50-
22+
Component | Documentation |
23+
-------------------- | --------- |
24+
button | [button docs & demo](https://blox.src.zone/material/directives/button) |
25+
card | [card docs & demo](https://blox.src.zone/material/directives/card) |
26+
checkbox | [checkbox docs & demo](https://blox.src.zone/material/directives/checkbox) |
27+
chips | in tracker |
28+
dialog | [dialog docs & demo](https://blox.src.zone/material/directives/drawer) |
29+
drawer | [drawer docs & demo](https://blox.src.zone/material/directives/drawer) |
30+
elevation | [elevation docs & demo](https://blox.src.zone/material/directives/elevation) |
31+
fab | [fab docs & demo](https://blox.src.zone/material/directives/fab) |
32+
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) |
33+
~~grid-list~~ | deprecated by the Material Components Web team |
34+
icon-toggle | [icon-toggle docs & demo](https://blox.src.zone/material/directives/icon-toggle) |
35+
linear-progress | [linear-progress docs & demo](https://blox.src.zone/material/directives/linear-progress) |
36+
list | [list docs & demo](https://blox.src.zone/material/directives/list) |
37+
menu | [menu docs & demo](https://blox.src.zone/material/directives/menu) |
38+
radio | [radio docs & demo](https://blox.src.zone/material/directives/radio) |
39+
ripple | [ripple docs & demo](https://blox.src.zone/material/directives/ripple) |
40+
select | [select docs & demo](https://blox.src.zone/material/directives/select) |
41+
slider | [slider docs & demo](https://blox.src.zone/material/directives/slider) |
42+
snackbar | [snackbar docs & demo](https://blox.src.zone/material/directives/snackbar) |
43+
switch | [switch docs & demo](https://blox.src.zone/material/directives/switch) |
44+
tabs | [tabs docs & demo](https://blox.src.zone/material/directives/tab) |
45+
text-field | [text-field docs & demo](https://blox.src.zone/material/directives/text-field) |
46+
toolbar | [toolbar docs & demo](https://blox.src.zone/material/directives/toolbar) |
47+
top-app-bar | in tracker |
48+
49+
The following material-components-web packages provide styling (scss, css) only. As such they
50+
can be consumed directly from your Angular app, and we see no reason to wrap their functionality
51+
in Angular components or directives. Just use the styles and sass mixins as documented by the
52+
material-components-web team:
53+
54+
Package | Documentation |
55+
---------------------| --------- |
56+
image-list | [image-list documentation](https://github.com./material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
57+
layout-grid | [layout-grid documentation](https://github.com./material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
58+
shape | [shape documentation](https://github.com./material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
59+
theme | [theme documentation](https://github.com./material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |
60+
typography | [typography documentation](https://github.com./material-components/material-components-web/blob/master/packages/mdc-image-list/README.md) |

bundle/package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundle/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"test:watch": "karma start karma.conf.ts --single-run false"
3030
},
3131
"dependencies": {
32+
"focus-trap": "^2.4.5",
3233
"karma-junit-reporter": "^1.2.0",
3334
"material-components-web": "^0.35.1"
3435
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** @docs-private */
2+
export interface MdcDialogAdapter {
3+
addClass: (className: string) => void,
4+
removeClass: (className: string) => void,
5+
addBodyClass: (className: string) => void,
6+
removeBodyClass: (className: string) => void,
7+
eventTargetHasClass: (target: EventTarget, className: string) => boolean,
8+
registerInteractionHandler: (evt: string, handler: EventListener) => void,
9+
deregisterInteractionHandler: (evt: string, handler: EventListener) => void,
10+
registerSurfaceInteractionHandler: (evt: string, handler: EventListener) => void,
11+
deregisterSurfaceInteractionHandler: (evt: string, handler: EventListener) => void,
12+
registerDocumentKeydownHandler: (handler: EventListener) => void,
13+
deregisterDocumentKeydownHandler: (handler: EventListener) => void,
14+
registerTransitionEndHandler: (handler: EventListener) => void,
15+
deregisterTransitionEndHandler: (handler: EventListener) => void,
16+
notifyAccept: () => void,
17+
notifyCancel: () => void,
18+
trapFocusOnSurface: () => void,
19+
untrapFocusOnSurface: () => void,
20+
isDialog: (el: Element) => boolean
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
2+
import { By } from '@angular/platform-browser';
3+
import { Component } from '@angular/core';
4+
import { FOCUS_TRAP_DIRECTIVES } from '../focus-trap/mdc.focus-trap.directive';
5+
import { MDC_EVENT_REGISTRY_PROVIDER } from '../../utils/mdc.event.registry';
6+
import { DIALOG_DIRECTIVES, MdcDialogDirective, MdcDialogBodyDirective } from './mdc.dialog.directive';
7+
import { MdcButtonDirective } from '../button/mdc.button.directive';
8+
import { cancelledClick, booleanAttributeStyleTest } from '../../testutils/page.test';
9+
10+
const templateWithDialog = `
11+
<button id="open" mdcButton (click)="dialog.open()">Open Dialog</button>
12+
<aside id="dialog" #dialog="mdcDialog" mdcDialog mdcFocusTrap>
13+
<div id="surface" mdcDialogSurface>
14+
<header mdcDialogHeader>
15+
<h2 mdcDialogHeaderTitle>Modal Dialog</h2>
16+
</header>
17+
<section mdcDialogBody [scrollable]="scrollable">
18+
Dialog Body
19+
</section>
20+
<footer mdcDialogFooter>
21+
<button *ngIf="cancelButton" id="cancel" mdcButton mdcDialogCancel>Decline</button>
22+
<button *ngIf="acceptButton" id="accept" mdcButton mdcDialogAccept>Accept</button>
23+
</footer>
24+
</div>
25+
<div mdcDialogBackdrop></div>
26+
</aside>
27+
`;
28+
29+
describe('MdcDialogDirective', () => {
30+
@Component({
31+
template: templateWithDialog
32+
})
33+
class TestComponent {
34+
scrollable = false;
35+
cancelButton = true;
36+
acceptButton = true;
37+
}
38+
39+
function setup() {
40+
const fixture = TestBed.configureTestingModule({
41+
providers: [MDC_EVENT_REGISTRY_PROVIDER],
42+
declarations: [...DIALOG_DIRECTIVES, ...FOCUS_TRAP_DIRECTIVES, MdcButtonDirective, TestComponent]
43+
}).createComponent(TestComponent);
44+
fixture.detectChanges();
45+
return { fixture };
46+
}
47+
48+
it('should only display the dialog when opened', (() => {
49+
const { fixture } = setup();
50+
const button = fixture.nativeElement.querySelector('#open');
51+
const dialog = fixture.nativeElement.querySelector('#dialog');
52+
const cancel = fixture.nativeElement.querySelector('#cancel');
53+
const accept = fixture.nativeElement.querySelector('#accept');
54+
55+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
56+
button.click();
57+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(true, 'dialog must be in opened state');
58+
cancel.click();
59+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
60+
button.click();
61+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(true, 'dialog must be in opened state');
62+
accept.click();
63+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
64+
}));
65+
66+
it('should trap focus to the dialog when opened', (() => {
67+
const { fixture } = setup();
68+
const button = fixture.nativeElement.querySelector('#open');
69+
const dialog = fixture.nativeElement.querySelector('#dialog');
70+
const accept = fixture.nativeElement.querySelector('#accept');
71+
expect(dialog.classList.contains('mdc-dialog--open')).toBe(false, 'dialog must be in closed state');
72+
button.click();
73+
// focusTrap is activated on animation 'transitionend', so simulate that event
74+
// (as tick() and friends won't wait for it):
75+
fixture.nativeElement.querySelector('#surface').dispatchEvent(new TransitionEvent('transitionend', {}));
76+
// clicks on the button should now be cancelled:
77+
expect(cancelledClick(button)).toBe(true);
78+
// clicks on buttons inside the dialog should not be cancelled:
79+
expect(cancelledClick(accept)).toBe(false);
80+
}));
81+
82+
it('should apply dialog button styling to buttons dynamically added', (() => {
83+
const { fixture } = setup();
84+
const button = fixture.nativeElement.querySelector('#open');
85+
const dialog = fixture.nativeElement.querySelector('#dialog');
86+
const testComponent = fixture.debugElement.injector.get(TestComponent);
87+
testComponent.cancelButton = false;
88+
testComponent.acceptButton = false;
89+
fixture.detectChanges();
90+
91+
button.click();
92+
expect(fixture.nativeElement.querySelector('#cancel')).toBeNull();
93+
testComponent.cancelButton = true;
94+
testComponent.acceptButton = true;
95+
fixture.detectChanges();
96+
const cancel = fixture.nativeElement.querySelector('#cancel');
97+
expect(cancel.classList).toContain('mdc-dialog__footer__button');
98+
const accept = fixture.nativeElement.querySelector('#accept');
99+
expect(accept.classList).toContain('mdc-dialog__footer__button');
100+
expect(accept.classList).toContain('mdc-dialog__footer__button--accept');
101+
}));
102+
103+
it('should emit the accept event', (() => {
104+
const { fixture } = setup();
105+
const button = fixture.nativeElement.querySelector('#open');
106+
const mdcDialog = fixture.debugElement.query(By.directive(MdcDialogDirective)).injector.get(MdcDialogDirective);
107+
const accept = fixture.nativeElement.querySelector('#accept');
108+
button.click();
109+
let accepted = false;
110+
mdcDialog.accept.subscribe(() => { accepted = true; });
111+
accept.click();
112+
expect(accepted).toBe(true);
113+
}));
114+
115+
it('should emit the cancel event', (() => {
116+
const { fixture } = setup();
117+
const button = fixture.nativeElement.querySelector('#open');
118+
const mdcDialog = fixture.debugElement.query(By.directive(MdcDialogDirective)).injector.get(MdcDialogDirective);
119+
const cancel = fixture.nativeElement.querySelector('#cancel');
120+
button.click();
121+
let canceled = false;
122+
mdcDialog.cancel.subscribe(() => { canceled = true; });
123+
cancel.click();
124+
expect(canceled).toBe(true);
125+
}));
126+
127+
it('should style the body according to the scrollable property', (() => {
128+
const { fixture } = setup();
129+
const button = fixture.nativeElement.querySelector('#open');
130+
const dialog = fixture.nativeElement.querySelector('#dialog');
131+
const testComponent = fixture.debugElement.injector.get(TestComponent);
132+
const mdcDialogBody = fixture.debugElement.query(By.directive(MdcDialogBodyDirective)).injector.get(MdcDialogBodyDirective);
133+
134+
button.click();
135+
booleanAttributeStyleTest(
136+
fixture,
137+
testComponent,
138+
mdcDialogBody,
139+
'scrollable',
140+
'scrollable',
141+
'mdc-dialog__body--scrollable');
142+
}));
143+
});

0 commit comments

Comments
 (0)