-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(focus-trap,dialog): implement focus-trap and dialog directives
- Loading branch information
Showing
29 changed files
with
1,106 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
143
bundle/src/components/dialog/mdc.dialog.directive.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
})); | ||
}); |
Oops, something went wrong.