2323 lines
76 KiB
TypeScript
2323 lines
76 KiB
TypeScript
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
|
|
import {Directionality} from '@angular/cdk/bidi';
|
|
import {A, ESCAPE} from '@angular/cdk/keycodes';
|
|
import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
|
|
import {_supportsShadowDom} from '@angular/cdk/platform';
|
|
import {ScrollDispatcher} from '@angular/cdk/scrolling';
|
|
import {
|
|
createKeyboardEvent,
|
|
dispatchEvent,
|
|
dispatchKeyboardEvent,
|
|
dispatchMouseEvent,
|
|
patchElementFocus,
|
|
} from '@angular/cdk/testing/private';
|
|
import {Location} from '@angular/common';
|
|
import {SpyLocation} from '@angular/common/testing';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
createNgModuleRef,
|
|
Directive,
|
|
Injectable,
|
|
Injector,
|
|
NgModule,
|
|
TemplateRef,
|
|
ViewChild,
|
|
ViewContainerRef,
|
|
ViewEncapsulation,
|
|
forwardRef,
|
|
signal,
|
|
inject,
|
|
} from '@angular/core';
|
|
import {
|
|
ComponentFixture,
|
|
TestBed,
|
|
fakeAsync,
|
|
flush,
|
|
flushMicrotasks,
|
|
tick,
|
|
} from '@angular/core/testing';
|
|
import {By} from '@angular/platform-browser';
|
|
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
|
|
import {Subject} from 'rxjs';
|
|
import {CLOSE_ANIMATION_DURATION, OPEN_ANIMATION_DURATION} from './dialog-container';
|
|
import {
|
|
MAT_DIALOG_DATA,
|
|
MAT_DIALOG_DEFAULT_OPTIONS,
|
|
MatDialog,
|
|
MatDialogActions,
|
|
MatDialogClose,
|
|
MatDialogContent,
|
|
MatDialogModule,
|
|
MatDialogRef,
|
|
MatDialogState,
|
|
MatDialogTitle,
|
|
} from './index';
|
|
|
|
describe('MatDialog', () => {
|
|
let dialog: MatDialog;
|
|
let overlayContainerElement: HTMLElement;
|
|
let scrolledSubject = new Subject();
|
|
let focusMonitor: FocusMonitor;
|
|
|
|
let testViewContainerRef: ViewContainerRef;
|
|
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
|
|
let mockLocation: SpyLocation;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
MatDialogModule,
|
|
NoopAnimationsModule,
|
|
ComponentWithChildViewContainer,
|
|
ComponentWithTemplateRef,
|
|
PizzaMsg,
|
|
ContentElementDialog,
|
|
DialogWithInjectedData,
|
|
DialogWithoutFocusableElements,
|
|
DirectiveWithViewContainer,
|
|
ComponentWithContentElementTemplateRef,
|
|
],
|
|
providers: [
|
|
{provide: Location, useClass: SpyLocation},
|
|
{
|
|
provide: ScrollDispatcher,
|
|
useFactory: () => ({
|
|
scrolled: () => scrolledSubject,
|
|
register: () => {},
|
|
deregister: () => {},
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
dialog = TestBed.inject(MatDialog);
|
|
mockLocation = TestBed.inject(Location) as SpyLocation;
|
|
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
|
|
focusMonitor = TestBed.inject(FocusMonitor);
|
|
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
|
|
viewContainerFixture.detectChanges();
|
|
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
|
|
}));
|
|
|
|
it('should open a dialog with a component', () => {
|
|
let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.textContent).toContain('Pizza');
|
|
expect(dialogRef.componentInstance instanceof PizzaMsg).toBe(true);
|
|
expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
let dialogContainerElement = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
|
|
expect(dialogContainerElement.getAttribute('aria-modal')).toBe('true');
|
|
});
|
|
|
|
it('should open a dialog with a template', () => {
|
|
const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef);
|
|
templateRefFixture.componentInstance.localValue = 'Bees';
|
|
templateRefFixture.changeDetectorRef.markForCheck();
|
|
templateRefFixture.detectChanges();
|
|
|
|
const data = {value: 'Knees'};
|
|
|
|
let dialogRef = dialog.open(templateRefFixture.componentInstance.templateRef, {data});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.textContent).toContain('Cheese Bees Knees');
|
|
expect(templateRefFixture.componentInstance.dialogRef).toBe(dialogRef);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let dialogContainerElement = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
|
|
expect(dialogContainerElement.getAttribute('aria-modal')).toBe('true');
|
|
|
|
dialogRef.close();
|
|
});
|
|
|
|
it('should emit when dialog opening animation is complete', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
const spy = jasmine.createSpy('afterOpen spy');
|
|
|
|
dialogRef.afterOpened().subscribe(spy);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
// callback should not be called before animation is complete
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
flush();
|
|
expect(spy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should use injector from viewContainerRef for DialogInjector', () => {
|
|
let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let dialogInjector = dialogRef.componentInstance.dialogInjector;
|
|
|
|
expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef);
|
|
expect(dialogInjector.get<DirectiveWithViewContainer>(DirectiveWithViewContainer))
|
|
.withContext(
|
|
'Expected the dialog component to be created with the injector from ' +
|
|
'the viewContainerRef.',
|
|
)
|
|
.toBeTruthy();
|
|
});
|
|
|
|
it('should open a dialog with a component and no ViewContainerRef', () => {
|
|
let dialogRef = dialog.open(PizzaMsg);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.textContent).toContain('Pizza');
|
|
expect(dialogRef.componentInstance instanceof PizzaMsg).toBe(true);
|
|
expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
let dialogContainerElement = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
|
|
});
|
|
|
|
it('should apply the configured role to the dialog element', () => {
|
|
dialog.open(PizzaMsg, {role: 'alertdialog'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let dialogContainerElement = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(dialogContainerElement.getAttribute('role')).toBe('alertdialog');
|
|
});
|
|
|
|
it('should apply the specified `aria-describedby`', () => {
|
|
dialog.open(PizzaMsg, {ariaDescribedBy: 'description-element'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let dialogContainerElement = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(dialogContainerElement.getAttribute('aria-describedby')).toBe('description-element');
|
|
});
|
|
|
|
it('should close a dialog and get back a result', fakeAsync(() => {
|
|
let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
let afterCloseCallback = jasmine.createSpy('afterClose callback');
|
|
|
|
dialogRef.afterClosed().subscribe(afterCloseCallback);
|
|
dialogRef.close('Charmander');
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(afterCloseCallback).toHaveBeenCalledWith('Charmander');
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
|
|
}));
|
|
|
|
it('should dispose of dialog if view container is destroyed while animating', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
viewContainerFixture.destroy();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
|
|
}));
|
|
|
|
it('should dispatch the beforeClosed and afterClosed events when the overlay is detached externally', fakeAsync(() => {
|
|
const overlay = TestBed.inject(Overlay);
|
|
const dialogRef = dialog.open(PizzaMsg, {
|
|
viewContainerRef: testViewContainerRef,
|
|
scrollStrategy: overlay.scrollStrategies.close(),
|
|
});
|
|
const beforeClosedCallback = jasmine.createSpy('beforeClosed callback');
|
|
const afterCloseCallback = jasmine.createSpy('afterClosed callback');
|
|
|
|
dialogRef.beforeClosed().subscribe(beforeClosedCallback);
|
|
dialogRef.afterClosed().subscribe(afterCloseCallback);
|
|
|
|
scrolledSubject.next();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(beforeClosedCallback).toHaveBeenCalledTimes(1);
|
|
expect(afterCloseCallback).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should close a dialog and get back a result before it is closed', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
// beforeClose should emit before dialog container is destroyed
|
|
const beforeCloseHandler = jasmine.createSpy('beforeClose callback').and.callFake(() => {
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container'))
|
|
.not.withContext('dialog container exists when beforeClose is called')
|
|
.toBeNull();
|
|
});
|
|
|
|
dialogRef.beforeClosed().subscribe(beforeCloseHandler);
|
|
dialogRef.close('Bulbasaur');
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(beforeCloseHandler).toHaveBeenCalledWith('Bulbasaur');
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
|
|
}));
|
|
|
|
it('should close a dialog via the escape key', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
|
|
expect(event.defaultPrevented).toBe(true);
|
|
}));
|
|
|
|
it('should not close a dialog via the escape key with a modifier', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true});
|
|
dispatchEvent(document.body, event);
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
|
expect(event.defaultPrevented).toBe(false);
|
|
}));
|
|
|
|
it('should close from a ViewContainerRef with OnPush change detection', fakeAsync(() => {
|
|
const onPushFixture = TestBed.createComponent(ComponentWithOnPushViewContainer);
|
|
|
|
onPushFixture.detectChanges();
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {
|
|
viewContainerRef: onPushFixture.componentInstance.viewContainerRef,
|
|
});
|
|
|
|
flushMicrotasks();
|
|
onPushFixture.detectChanges();
|
|
flushMicrotasks();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length)
|
|
.withContext('Expected one open dialog.')
|
|
.toBe(1);
|
|
|
|
dialogRef.close();
|
|
flushMicrotasks();
|
|
onPushFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length)
|
|
.withContext('Expected no open dialogs.')
|
|
.toBe(0);
|
|
}));
|
|
|
|
it('should close when clicking on the overlay backdrop', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeFalsy();
|
|
}));
|
|
|
|
it('should emit the backdropClick stream when clicking on the overlay backdrop', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
const spy = jasmine.createSpy('backdropClick spy');
|
|
dialogRef.backdropClick().subscribe(spy);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
|
|
backdrop.click();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
// Additional clicks after the dialog has closed should not be emitted
|
|
backdrop.click();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should emit the keyboardEvent stream when key events target the overlay', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
const spy = jasmine.createSpy('keyboardEvent spy');
|
|
dialogRef.keydownEvents().subscribe(spy);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
let container = overlayContainerElement.querySelector('mat-dialog-container') as HTMLElement;
|
|
dispatchKeyboardEvent(document.body, 'keydown', A);
|
|
dispatchKeyboardEvent(backdrop, 'keydown', A);
|
|
dispatchKeyboardEvent(container, 'keydown', A);
|
|
|
|
expect(spy).toHaveBeenCalledTimes(3);
|
|
}));
|
|
|
|
it('should notify the observers if a dialog has been opened', () => {
|
|
dialog.afterOpened.subscribe(ref => {
|
|
expect(dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef})).toBe(ref);
|
|
});
|
|
});
|
|
|
|
it('should notify the observers if all open dialogs have finished closing', fakeAsync(() => {
|
|
const ref1 = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
const ref2 = dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
|
|
const spy = jasmine.createSpy('afterAllClosed spy');
|
|
|
|
dialog.afterAllClosed.subscribe(spy);
|
|
|
|
ref1.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
ref2.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
expect(spy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should emit the afterAllClosed stream on subscribe if there are no open dialogs', () => {
|
|
const spy = jasmine.createSpy('afterAllClosed spy');
|
|
|
|
dialog.afterAllClosed.subscribe(spy);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should override the width of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {width: '500px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.width).toBe('500px');
|
|
});
|
|
|
|
it('should override the height of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {height: '100px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.height).toBe('100px');
|
|
});
|
|
|
|
it('should override the min-width of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {minWidth: '500px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.minWidth).toBe('500px');
|
|
});
|
|
|
|
it('should override the max-width of the overlay pane', fakeAsync(() => {
|
|
let dialogRef = dialog.open(PizzaMsg);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.maxWidth).toBe('');
|
|
dialogRef.close();
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
|
|
dialogRef = dialog.open(PizzaMsg, {maxWidth: '100px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.maxWidth).toBe('100px');
|
|
}));
|
|
|
|
it('should override the min-height of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {minHeight: '300px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.minHeight).toBe('300px');
|
|
});
|
|
|
|
it('should override the max-height of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {maxHeight: '100px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.maxHeight).toBe('100px');
|
|
});
|
|
|
|
it('should override the top offset of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {position: {top: '100px'}});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.marginTop).toBe('100px');
|
|
});
|
|
|
|
it('should override the bottom offset of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {position: {bottom: '200px'}});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.marginBottom).toBe('200px');
|
|
});
|
|
|
|
it('should override the left offset of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {position: {left: '250px'}});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.marginLeft).toBe('250px');
|
|
});
|
|
|
|
it('should override the right offset of the overlay pane', () => {
|
|
dialog.open(PizzaMsg, {position: {right: '125px'}});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.marginRight).toBe('125px');
|
|
});
|
|
|
|
it('should allow for the position to be updated', () => {
|
|
let dialogRef = dialog.open(PizzaMsg, {position: {left: '250px'}});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.marginLeft).toBe('250px');
|
|
|
|
dialogRef.updatePosition({left: '500px'});
|
|
|
|
expect(overlayPane.style.marginLeft).toBe('500px');
|
|
});
|
|
|
|
it('should allow for the dimensions to be updated', () => {
|
|
let dialogRef = dialog.open(PizzaMsg, {width: '100px'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.width).toBe('100px');
|
|
|
|
dialogRef.updateSize('200px');
|
|
|
|
expect(overlayPane.style.width).toBe('200px');
|
|
});
|
|
|
|
it('should reset the overlay dimensions to their initial size', () => {
|
|
let dialogRef = dialog.open(PizzaMsg);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
|
|
expect(overlayPane.style.width).toBeFalsy();
|
|
expect(overlayPane.style.height).toBeFalsy();
|
|
|
|
dialogRef.updateSize('200px', '200px');
|
|
|
|
expect(overlayPane.style.width).toBe('200px');
|
|
expect(overlayPane.style.height).toBe('200px');
|
|
|
|
dialogRef.updateSize();
|
|
|
|
expect(overlayPane.style.width).toBeFalsy();
|
|
expect(overlayPane.style.height).toBeFalsy();
|
|
});
|
|
|
|
it('should allow setting the layout direction', () => {
|
|
dialog.open(PizzaMsg, {direction: 'rtl'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-global-overlay-wrapper')!;
|
|
|
|
expect(overlayPane.getAttribute('dir')).toBe('rtl');
|
|
});
|
|
|
|
it('should inject the correct layout direction in the component instance', () => {
|
|
const dialogRef = dialog.open(PizzaMsg, {direction: 'rtl'});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(dialogRef.componentInstance.directionality.value).toBe('rtl');
|
|
});
|
|
|
|
it('should fall back to injecting the global direction if none is passed by the config', () => {
|
|
const dialogRef = dialog.open(PizzaMsg, {});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(dialogRef.componentInstance.directionality.value).toBe('ltr');
|
|
});
|
|
|
|
it('should use the passed in ViewContainerRef from the config', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
// One view ref is for the container and one more for the component with the content.
|
|
expect(testViewContainerRef.length).toBe(2);
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(testViewContainerRef.length).toBe(0);
|
|
}));
|
|
|
|
it('should close all of the dialogs', fakeAsync(() => {
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
|
|
|
|
dialog.closeAll();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(0);
|
|
}));
|
|
|
|
it('should close all dialogs when the user goes forwards/backwards in history', fakeAsync(() => {
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
|
|
|
|
mockLocation.simulateUrlPop('');
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(0);
|
|
}));
|
|
|
|
it('should close all open dialogs when the location hash changes', fakeAsync(() => {
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
|
|
|
|
mockLocation.simulateHashChange('');
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(0);
|
|
}));
|
|
|
|
it('should close all of the dialogs when the injectable is destroyed', fakeAsync(() => {
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg);
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(3);
|
|
|
|
dialog.ngOnDestroy();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(0);
|
|
}));
|
|
|
|
it('should complete open and close streams when the injectable is destroyed', fakeAsync(() => {
|
|
const afterOpenedSpy = jasmine.createSpy('after opened spy');
|
|
const afterAllClosedSpy = jasmine.createSpy('after all closed spy');
|
|
const afterOpenedSubscription = dialog.afterOpened.subscribe({complete: afterOpenedSpy});
|
|
const afterAllClosedSubscription = dialog.afterAllClosed.subscribe({
|
|
complete: afterAllClosedSpy,
|
|
});
|
|
|
|
dialog.ngOnDestroy();
|
|
|
|
expect(afterOpenedSpy).toHaveBeenCalled();
|
|
expect(afterAllClosedSpy).toHaveBeenCalled();
|
|
|
|
afterOpenedSubscription.unsubscribe();
|
|
afterAllClosedSubscription.unsubscribe();
|
|
}));
|
|
|
|
it('should allow the consumer to disable closing a dialog on navigation', fakeAsync(() => {
|
|
dialog.open(PizzaMsg);
|
|
dialog.open(PizzaMsg, {closeOnNavigation: false});
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(2);
|
|
|
|
mockLocation.simulateUrlPop('');
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('mat-dialog-container').length).toBe(1);
|
|
}));
|
|
|
|
it('should have the componentInstance available in the afterClosed callback', fakeAsync(() => {
|
|
let dialogRef = dialog.open(PizzaMsg);
|
|
let spy = jasmine.createSpy('afterClosed spy');
|
|
|
|
flushMicrotasks();
|
|
viewContainerFixture.detectChanges();
|
|
flushMicrotasks();
|
|
|
|
dialogRef.afterClosed().subscribe(() => {
|
|
spy();
|
|
expect(dialogRef.componentInstance)
|
|
.withContext('Expected component instance to be defined.')
|
|
.toBeTruthy();
|
|
});
|
|
|
|
dialogRef.close();
|
|
|
|
flushMicrotasks();
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
// Ensure that the callback actually fires.
|
|
expect(spy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should be able to attach a custom scroll strategy', fakeAsync(() => {
|
|
const scrollStrategy: ScrollStrategy = {
|
|
attach: () => {},
|
|
enable: jasmine.createSpy('scroll strategy enable spy'),
|
|
disable: () => {},
|
|
};
|
|
|
|
dialog.open(PizzaMsg, {scrollStrategy});
|
|
flush();
|
|
expect(scrollStrategy.enable).toHaveBeenCalled();
|
|
}));
|
|
|
|
describe('passing in data', () => {
|
|
it('should be able to pass in data', () => {
|
|
let config = {data: {stringParam: 'hello', dateParam: new Date()}};
|
|
|
|
let instance = dialog.open(DialogWithInjectedData, config).componentInstance;
|
|
|
|
expect(instance.data.stringParam).toBe(config.data.stringParam);
|
|
expect(instance.data.dateParam).toBe(config.data.dateParam);
|
|
});
|
|
|
|
it('should default to null if no data is passed', () => {
|
|
expect(() => {
|
|
let dialogRef = dialog.open(DialogWithInjectedData);
|
|
expect(dialogRef.componentInstance.data).toBeNull();
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
it('should not keep a reference to the component after the dialog is closed', fakeAsync(() => {
|
|
let dialogRef = dialog.open(PizzaMsg);
|
|
|
|
expect(dialogRef.componentInstance).toBeTruthy();
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(dialogRef.componentInstance)
|
|
.withContext('Expected reference to have been cleared.')
|
|
.toBeFalsy();
|
|
}));
|
|
|
|
it('should assign a unique id to each dialog', fakeAsync(() => {
|
|
const one = dialog.open(PizzaMsg);
|
|
const two = dialog.open(PizzaMsg);
|
|
flush();
|
|
|
|
expect(one.id).toBeTruthy();
|
|
expect(two.id).toBeTruthy();
|
|
expect(one.id).not.toBe(two.id);
|
|
}));
|
|
|
|
it('should allow for the id to be overwritten', () => {
|
|
const dialogRef = dialog.open(PizzaMsg, {id: 'pizza'});
|
|
expect(dialogRef.id).toBe('pizza');
|
|
});
|
|
|
|
it('should throw when trying to open a dialog with the same id as another dialog', () => {
|
|
dialog.open(PizzaMsg, {id: 'pizza'});
|
|
expect(() => dialog.open(PizzaMsg, {id: 'pizza'})).toThrowError(/must be unique/g);
|
|
});
|
|
|
|
it('should be able to find a dialog by id', () => {
|
|
const dialogRef = dialog.open(PizzaMsg, {id: 'pizza'});
|
|
expect(dialog.getDialogById('pizza')).toBe(dialogRef);
|
|
});
|
|
|
|
it('should toggle `aria-hidden` on the overlay container siblings', fakeAsync(() => {
|
|
const sibling = document.createElement('div');
|
|
overlayContainerElement.parentNode!.appendChild(sibling);
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(sibling.getAttribute('aria-hidden'))
|
|
.withContext('Expected sibling to be hidden')
|
|
.toBe('true');
|
|
expect(overlayContainerElement.hasAttribute('aria-hidden'))
|
|
.withContext('Expected overlay container not to be hidden.')
|
|
.toBe(false);
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(sibling.hasAttribute('aria-hidden'))
|
|
.withContext('Expected sibling to no longer be hidden.')
|
|
.toBe(false);
|
|
sibling.remove();
|
|
}));
|
|
|
|
it('should restore `aria-hidden` to the overlay container siblings on close', fakeAsync(() => {
|
|
const sibling = document.createElement('div');
|
|
|
|
sibling.setAttribute('aria-hidden', 'true');
|
|
overlayContainerElement.parentNode!.appendChild(sibling);
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(sibling.getAttribute('aria-hidden'))
|
|
.withContext('Expected sibling to be hidden.')
|
|
.toBe('true');
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(sibling.getAttribute('aria-hidden'))
|
|
.withContext('Expected sibling to remain hidden.')
|
|
.toBe('true');
|
|
sibling.remove();
|
|
}));
|
|
|
|
it('should not set `aria-hidden` on `aria-live` elements', fakeAsync(() => {
|
|
const sibling = document.createElement('div');
|
|
|
|
sibling.setAttribute('aria-live', 'polite');
|
|
overlayContainerElement.parentNode!.appendChild(sibling);
|
|
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(sibling.hasAttribute('aria-hidden'))
|
|
.withContext('Expected live element not to be hidden.')
|
|
.toBe(false);
|
|
sibling.remove();
|
|
}));
|
|
|
|
it('should add and remove classes while open', () => {
|
|
let dialogRef = dialog.open(PizzaMsg, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
expect(pane.classList).not.toContain(
|
|
'custom-class-one',
|
|
'Expected class to be initially missing',
|
|
);
|
|
|
|
dialogRef.addPanelClass('custom-class-one');
|
|
expect(pane.classList).withContext('Expected class to be added').toContain('custom-class-one');
|
|
|
|
dialogRef.removePanelClass('custom-class-one');
|
|
expect(pane.classList).not.toContain('custom-class-one', 'Expected class to be removed');
|
|
});
|
|
|
|
describe('disableClose option', () => {
|
|
it('should prevent closing via clicks on the backdrop', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {disableClose: true, viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
|
}));
|
|
|
|
it('should prevent closing via the escape key', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {disableClose: true, viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
|
}));
|
|
|
|
it('should allow for the disableClose option to be updated while open', fakeAsync(() => {
|
|
let dialogRef = dialog.open(PizzaMsg, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
backdrop.click();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
|
|
|
dialogRef.disableClose = false;
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeFalsy();
|
|
}));
|
|
|
|
it('should recapture focus when clicking on the backdrop', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {disableClose: true, viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
let input = overlayContainerElement.querySelector('input') as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected input to be focused on open')
|
|
.toBe(input);
|
|
|
|
input.blur(); // Programmatic clicks might not move focus so we simulate it.
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected input to stay focused after click')
|
|
.toBe(input);
|
|
}));
|
|
|
|
it(
|
|
'should recapture focus to the first tabbable element when clicking on the backdrop with ' +
|
|
'autoFocus set to "first-tabbable" (the default)',
|
|
fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
let backdrop = overlayContainerElement.querySelector(
|
|
'.cdk-overlay-backdrop',
|
|
) as HTMLElement;
|
|
let input = overlayContainerElement.querySelector('input') as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected input to be focused on open')
|
|
.toBe(input);
|
|
|
|
input.blur(); // Programmatic clicks might not move focus so we simulate it.
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected input to stay focused after click')
|
|
.toBe(input);
|
|
}),
|
|
);
|
|
|
|
it(
|
|
'should recapture focus to the container when clicking on the backdrop with ' +
|
|
'autoFocus set to "dialog"',
|
|
fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'dialog',
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector(
|
|
'.cdk-overlay-backdrop',
|
|
) as HTMLElement;
|
|
let container = overlayContainerElement.querySelector(
|
|
'.mat-mdc-dialog-container',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected container to be focused on open')
|
|
.toBe(container);
|
|
|
|
container.blur(); // Programmatic clicks might not move focus so we simulate it.
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected container to stay focused after click')
|
|
.toBe(container);
|
|
}),
|
|
);
|
|
});
|
|
|
|
it(
|
|
'should recapture focus to the first header when clicking on the backdrop with ' +
|
|
'autoFocus set to "first-heading"',
|
|
fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'first-heading',
|
|
});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
let firstHeader = overlayContainerElement.querySelector(
|
|
'.mat-mdc-dialog-title[tabindex="-1"]',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first header to be focused on open')
|
|
.toBe(firstHeader);
|
|
|
|
firstHeader.blur(); // Programmatic clicks might not move focus so we simulate it.
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first header to stay focused after click')
|
|
.toBe(firstHeader);
|
|
}),
|
|
);
|
|
|
|
it(
|
|
'should recapture focus to the first element that matches the css selector when ' +
|
|
'clicking on the backdrop with autoFocus set to a css selector',
|
|
fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
disableClose: true,
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'button',
|
|
});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
|
let firstButton = overlayContainerElement.querySelector(
|
|
'[mat-dialog-close]',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first button to be focused on open')
|
|
.toBe(firstButton);
|
|
|
|
firstButton.blur(); // Programmatic clicks might not move focus so we simulate it.
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first button to stay focused after click')
|
|
.toBe(firstButton);
|
|
}),
|
|
);
|
|
|
|
describe('hasBackdrop option', () => {
|
|
it('should have a backdrop', () => {
|
|
dialog.open(PizzaMsg, {hasBackdrop: true, viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
|
|
});
|
|
|
|
it('should not have a backdrop', () => {
|
|
dialog.open(PizzaMsg, {hasBackdrop: false, viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('panelClass option', () => {
|
|
it('should have custom panel class', () => {
|
|
dialog.open(PizzaMsg, {
|
|
panelClass: 'custom-panel-class',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.custom-panel-class')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('backdropClass option', () => {
|
|
it('should have default backdrop class', () => {
|
|
dialog.open(PizzaMsg, {backdropClass: '', viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-dark-backdrop')).toBeTruthy();
|
|
});
|
|
|
|
it('should have custom backdrop class', () => {
|
|
dialog.open(PizzaMsg, {
|
|
backdropClass: 'custom-backdrop-class',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.custom-backdrop-class')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('focus management', () => {
|
|
// When testing focus, all of the elements must be in the DOM.
|
|
beforeEach(() => document.body.appendChild(overlayContainerElement));
|
|
afterEach(() => overlayContainerElement.remove());
|
|
|
|
it('should focus the first tabbable element of the dialog on open (the default)', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement!.tagName)
|
|
.withContext('Expected first tabbable element (input) in the dialog to be focused.')
|
|
.toBe('INPUT');
|
|
}));
|
|
|
|
it('should focus the dialog element on open', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'dialog',
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let container = overlayContainerElement.querySelector(
|
|
'.mat-mdc-dialog-container',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected container to be focused on open')
|
|
.toBe(container);
|
|
}));
|
|
|
|
it('should focus the first header element on open', fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'first-heading',
|
|
});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let firstHeader = overlayContainerElement.querySelector(
|
|
'h2[tabindex="-1"]',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first header to be focused on open')
|
|
.toBe(firstHeader);
|
|
}));
|
|
|
|
it('should focus the first element that matches the css selector from autoFocus on open', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'p',
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
let firstParagraph = overlayContainerElement.querySelector(
|
|
'p[tabindex="-1"]',
|
|
) as HTMLInputElement;
|
|
|
|
expect(document.activeElement)
|
|
.withContext('Expected first paragraph to be focused on open')
|
|
.toBe(firstParagraph);
|
|
}));
|
|
|
|
it('should attach the focus trap even if automatic focus is disabled', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
viewContainerRef: testViewContainerRef,
|
|
autoFocus: 'false',
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(
|
|
overlayContainerElement.querySelectorAll('.cdk-focus-trap-anchor').length,
|
|
).toBeGreaterThan(0);
|
|
}));
|
|
|
|
it('should re-focus trigger element when dialog closes', fakeAsync(() => {
|
|
// Create a element that has focus before the dialog is opened.
|
|
let button = document.createElement('button');
|
|
button.id = 'dialog-trigger';
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(document.activeElement!.id).not.toBe(
|
|
'dialog-trigger',
|
|
'Expected the focus to change when dialog was opened.',
|
|
);
|
|
|
|
dialogRef.close();
|
|
expect(document.activeElement!.id).not.toBe(
|
|
'dialog-trigger',
|
|
'Expected the focus not to have changed before the animation finishes.',
|
|
);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(document.activeElement!.id)
|
|
.withContext('Expected that the trigger was refocused after the dialog is closed.')
|
|
.toBe('dialog-trigger');
|
|
|
|
button.remove();
|
|
}));
|
|
|
|
it('should re-focus trigger element inside the shadow DOM when dialog closes', fakeAsync(() => {
|
|
if (!_supportsShadowDom()) {
|
|
return;
|
|
}
|
|
|
|
viewContainerFixture.destroy();
|
|
const fixture = TestBed.createComponent(ShadowDomComponent);
|
|
fixture.detectChanges();
|
|
flush();
|
|
const button = fixture.debugElement.query(By.css('button'))!.nativeElement;
|
|
|
|
button.focus();
|
|
|
|
const dialogRef = dialog.open(PizzaMsg);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
const spy = spyOn(button, 'focus').and.callThrough();
|
|
dialogRef.close();
|
|
fixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should re-focus the trigger via keyboard when closed via escape key', fakeAsync(() => {
|
|
const button = document.createElement('button');
|
|
let lastFocusOrigin: FocusOrigin = null;
|
|
|
|
focusMonitor.monitor(button, false).subscribe(focusOrigin => (lastFocusOrigin = focusOrigin));
|
|
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
// Patch the element focus after the initial and real focus, because otherwise the
|
|
// `activeElement` won't be set, and the dialog won't be able to restore focus to an
|
|
// element.
|
|
patchElementFocus(button);
|
|
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
flushMicrotasks();
|
|
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
|
|
|
|
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
|
|
flushMicrotasks();
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(lastFocusOrigin!)
|
|
.withContext('Expected the trigger button to be focused via keyboard')
|
|
.toBe('keyboard');
|
|
|
|
focusMonitor.stopMonitoring(button);
|
|
button.remove();
|
|
}));
|
|
|
|
it('should re-focus the trigger via mouse when backdrop has been clicked', fakeAsync(() => {
|
|
const button = document.createElement('button');
|
|
let lastFocusOrigin: FocusOrigin = null;
|
|
|
|
focusMonitor.monitor(button, false).subscribe(focusOrigin => (lastFocusOrigin = focusOrigin));
|
|
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
// Patch the element focus after the initial and real focus, because otherwise the
|
|
// `activeElement` won't be set, and the dialog won't be able to restore focus to an
|
|
// element.
|
|
patchElementFocus(button);
|
|
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
flushMicrotasks();
|
|
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
|
|
|
|
const backdrop = overlayContainerElement.querySelector(
|
|
'.cdk-overlay-backdrop',
|
|
) as HTMLElement;
|
|
|
|
backdrop.click();
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(lastFocusOrigin!)
|
|
.withContext('Expected the trigger button to be focused via mouse')
|
|
.toBe('mouse');
|
|
|
|
focusMonitor.stopMonitoring(button);
|
|
button.remove();
|
|
}));
|
|
|
|
it('should re-focus via keyboard if the close button has been triggered through keyboard', fakeAsync(() => {
|
|
const button = document.createElement('button');
|
|
let lastFocusOrigin: FocusOrigin = null;
|
|
|
|
focusMonitor.monitor(button, false).subscribe(focusOrigin => (lastFocusOrigin = focusOrigin));
|
|
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
// Patch the element focus after the initial and real focus, because otherwise the
|
|
// `activeElement` won't be set, and the dialog won't be able to restore focus to an
|
|
// element.
|
|
patchElementFocus(button);
|
|
|
|
dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
flushMicrotasks();
|
|
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
|
|
|
|
const closeButton = overlayContainerElement.querySelector(
|
|
'button[mat-dialog-close]',
|
|
) as HTMLElement;
|
|
|
|
// Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
|
|
// event with a MouseEvent, which has coordinates that are out of the element boundaries.
|
|
dispatchMouseEvent(closeButton, 'click', 0, 0);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(lastFocusOrigin!)
|
|
.withContext('Expected the trigger button to be focused via keyboard')
|
|
.toBe('keyboard');
|
|
|
|
focusMonitor.stopMonitoring(button);
|
|
button.remove();
|
|
}));
|
|
|
|
it('should re-focus via mouse if the close button has been clicked', fakeAsync(() => {
|
|
const button = document.createElement('button');
|
|
let lastFocusOrigin: FocusOrigin = null;
|
|
|
|
focusMonitor.monitor(button, false).subscribe(focusOrigin => (lastFocusOrigin = focusOrigin));
|
|
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
// Patch the element focus after the initial and real focus, because otherwise the
|
|
// `activeElement` won't be set, and the dialog won't be able to restore focus to an
|
|
// element.
|
|
patchElementFocus(button);
|
|
|
|
dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
flushMicrotasks();
|
|
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
|
|
|
|
const closeButton = overlayContainerElement.querySelector(
|
|
'button[mat-dialog-close]',
|
|
) as HTMLElement;
|
|
|
|
// The dialog close button detects the focus origin by inspecting the click event. If
|
|
// coordinates of the click are not present, it assumes that the click has been triggered
|
|
// by keyboard.
|
|
dispatchMouseEvent(closeButton, 'click', 10, 10);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(lastFocusOrigin!)
|
|
.withContext('Expected the trigger button to be focused via mouse')
|
|
.toBe('mouse');
|
|
|
|
focusMonitor.stopMonitoring(button);
|
|
button.remove();
|
|
}));
|
|
|
|
it('should allow the consumer to shift focus in afterClosed', fakeAsync(() => {
|
|
// Create a element that has focus before the dialog is opened.
|
|
let button = document.createElement('button');
|
|
let input = document.createElement('input');
|
|
|
|
button.id = 'dialog-trigger';
|
|
input.id = 'input-to-be-focused';
|
|
|
|
document.body.appendChild(button);
|
|
document.body.appendChild(input);
|
|
button.focus();
|
|
|
|
let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
|
|
dialogRef.afterClosed().subscribe(() => input.focus());
|
|
dialogRef.close();
|
|
|
|
tick(500);
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement!.id)
|
|
.withContext('Expected that the trigger was refocused after the dialog is closed.')
|
|
.toBe('input-to-be-focused');
|
|
|
|
button.remove();
|
|
input.remove();
|
|
flush();
|
|
}));
|
|
|
|
it('should move focus to the container if there are no focusable elements in the dialog', fakeAsync(() => {
|
|
dialog.open(DialogWithoutFocusableElements);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement!.tagName)
|
|
.withContext('Expected dialog container to be focused.')
|
|
.toBe('MAT-DIALOG-CONTAINER');
|
|
}));
|
|
|
|
it('should be able to disable focus restoration', fakeAsync(() => {
|
|
// Create a element that has focus before the dialog is opened.
|
|
const button = document.createElement('button');
|
|
button.id = 'dialog-trigger';
|
|
document.body.appendChild(button);
|
|
button.focus();
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {
|
|
viewContainerRef: testViewContainerRef,
|
|
restoreFocus: false,
|
|
});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(document.activeElement!.id).not.toBe(
|
|
'dialog-trigger',
|
|
'Expected the focus to change when dialog was opened.',
|
|
);
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
tick(500);
|
|
|
|
expect(document.activeElement!.id).not.toBe(
|
|
'dialog-trigger',
|
|
'Expected focus not to have been restored.',
|
|
);
|
|
|
|
button.remove();
|
|
}));
|
|
|
|
it('should not move focus if it was moved outside the dialog while animating', fakeAsync(() => {
|
|
// Create a element that has focus before the dialog is opened.
|
|
const button = document.createElement('button');
|
|
const otherButton = document.createElement('button');
|
|
const body = document.body;
|
|
button.id = 'dialog-trigger';
|
|
otherButton.id = 'other-button';
|
|
body.appendChild(button);
|
|
body.appendChild(otherButton);
|
|
button.focus();
|
|
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(document.activeElement!.id).not.toBe(
|
|
'dialog-trigger',
|
|
'Expected the focus to change when dialog was opened.',
|
|
);
|
|
|
|
// Start the closing sequence and move focus out of dialog.
|
|
dialogRef.close();
|
|
otherButton.focus();
|
|
|
|
expect(document.activeElement!.id)
|
|
.withContext('Expected focus to be on the alternate button.')
|
|
.toBe('other-button');
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(document.activeElement!.id)
|
|
.withContext('Expected focus to stay on the alternate button.')
|
|
.toBe('other-button');
|
|
|
|
button.remove();
|
|
otherButton.remove();
|
|
}));
|
|
});
|
|
|
|
describe('dialog content elements', () => {
|
|
let dialogRef: MatDialogRef<any>;
|
|
let hostInstance: ContentElementDialog | ComponentWithContentElementTemplateRef;
|
|
|
|
describe('inside component dialog', () => {
|
|
beforeEach(fakeAsync(() => {
|
|
dialogRef = dialog.open(ContentElementDialog, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
hostInstance = dialogRef.componentInstance;
|
|
}));
|
|
|
|
runContentElementTests();
|
|
});
|
|
|
|
describe('inside template portal', () => {
|
|
beforeEach(fakeAsync(() => {
|
|
const fixture = TestBed.createComponent(ComponentWithContentElementTemplateRef);
|
|
fixture.detectChanges();
|
|
|
|
dialogRef = dialog.open(fixture.componentInstance.templateRef, {
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
hostInstance = fixture.componentInstance;
|
|
}));
|
|
|
|
runContentElementTests();
|
|
});
|
|
|
|
it('should set the aria-labelledby attribute to the id of the title under OnPush host', fakeAsync(() => {
|
|
@Component({
|
|
standalone: true,
|
|
imports: [MatDialogTitle],
|
|
template: `@if (showTitle()) { <h2 mat-dialog-title>This is the first title</h2> }`,
|
|
})
|
|
class DialogCmp {
|
|
showTitle = signal(true);
|
|
}
|
|
|
|
@Component({
|
|
template: '',
|
|
selector: 'child',
|
|
standalone: true,
|
|
})
|
|
class Child {
|
|
readonly viewContainerRef = inject(ViewContainerRef);
|
|
readonly dialog = inject(MatDialog);
|
|
|
|
dialogRef?: MatDialogRef<DialogCmp>;
|
|
|
|
open() {
|
|
this.dialogRef = this.dialog.open(DialogCmp, {viewContainerRef: this.viewContainerRef});
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
imports: [Child],
|
|
template: `<child></child>`,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
class OnPushHost {
|
|
@ViewChild(Child, {static: true}) child: Child;
|
|
}
|
|
|
|
const hostFixture = TestBed.createComponent(OnPushHost);
|
|
hostFixture.componentInstance.child.open();
|
|
hostFixture.detectChanges();
|
|
flush();
|
|
hostFixture.detectChanges();
|
|
|
|
const overlayContainer = TestBed.inject(OverlayContainer);
|
|
const title = overlayContainer.getContainerElement().querySelector('[mat-dialog-title]')!;
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
|
|
expect(title.id).withContext('Expected title element to have an id.').toBeTruthy();
|
|
expect(container.getAttribute('aria-labelledby'))
|
|
.withContext('Expected the aria-labelledby to match the title id.')
|
|
.toBe(title.id);
|
|
|
|
hostFixture.componentInstance.child.dialogRef?.componentInstance.showTitle.set(false);
|
|
hostFixture.detectChanges();
|
|
flush();
|
|
hostFixture.detectChanges();
|
|
expect(container.getAttribute('aria-labelledby')).toBe(null);
|
|
}));
|
|
|
|
function runContentElementTests() {
|
|
it('should close the dialog when clicking on the close button', fakeAsync(() => {
|
|
expect(overlayContainerElement.querySelectorAll('.mat-mdc-dialog-container').length).toBe(
|
|
1,
|
|
);
|
|
|
|
(overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement).click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('.mat-mdc-dialog-container').length).toBe(
|
|
0,
|
|
);
|
|
}));
|
|
|
|
it('should not close if [mat-dialog-close] is applied on a non-button node', () => {
|
|
expect(overlayContainerElement.querySelectorAll('.mat-mdc-dialog-container').length).toBe(
|
|
1,
|
|
);
|
|
|
|
(overlayContainerElement.querySelector('div[mat-dialog-close]') as HTMLElement).click();
|
|
|
|
expect(overlayContainerElement.querySelectorAll('.mat-mdc-dialog-container').length).toBe(
|
|
1,
|
|
);
|
|
});
|
|
|
|
it('should allow for a user-specified aria-label on the close button', fakeAsync(() => {
|
|
let button = overlayContainerElement.querySelector('.close-with-aria-label')!;
|
|
expect(button.getAttribute('aria-label')).toBe('Best close button ever');
|
|
}));
|
|
|
|
it('should set the "type" attribute of the close button if not set manually', () => {
|
|
let button = overlayContainerElement.querySelector('button[mat-dialog-close]')!;
|
|
|
|
expect(button.getAttribute('type')).toBe('button');
|
|
});
|
|
|
|
it('should not override type attribute of the close button if set manually', () => {
|
|
let button = overlayContainerElement.querySelector('button.with-submit')!;
|
|
|
|
expect(button.getAttribute('type')).toBe('submit');
|
|
});
|
|
|
|
it('should return the [mat-dialog-close] result when clicking the close button', fakeAsync(() => {
|
|
let afterCloseCallback = jasmine.createSpy('afterClose callback');
|
|
dialogRef.afterClosed().subscribe(afterCloseCallback);
|
|
|
|
(overlayContainerElement.querySelector('button.close-with-true') as HTMLElement).click();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(afterCloseCallback).toHaveBeenCalledWith(true);
|
|
}));
|
|
|
|
it('should set the aria-labelledby attribute to the id of the title', fakeAsync(() => {
|
|
let title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
|
|
let container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(title.id).withContext('Expected title element to have an id.').toBeTruthy();
|
|
expect(container.getAttribute('aria-labelledby'))
|
|
.withContext('Expected the aria-labelledby to match the title id.')
|
|
.toBe(title.id);
|
|
}));
|
|
|
|
it('should update the aria-labelledby attribute if two titles are swapped', fakeAsync(() => {
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
let title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
|
|
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const previousId = title.id;
|
|
expect(title.id).toBeTruthy();
|
|
expect(container.getAttribute('aria-labelledby')).toBe(title.id);
|
|
|
|
hostInstance.shownTitle = 'second';
|
|
viewContainerFixture.changeDetectorRef.markForCheck();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
|
|
|
|
expect(title.id).toBeTruthy();
|
|
expect(title.id).not.toBe(previousId);
|
|
expect(container.getAttribute('aria-labelledby')).toBe(title.id);
|
|
}));
|
|
|
|
it('should update the aria-labelledby attribute if multiple titles are present and one is removed', fakeAsync(() => {
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
|
|
hostInstance.shownTitle = 'all';
|
|
viewContainerFixture.changeDetectorRef.markForCheck();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const titles = overlayContainerElement.querySelectorAll('[mat-dialog-title]');
|
|
|
|
expect(titles.length).toBe(3);
|
|
expect(container.getAttribute('aria-labelledby')).toBe(titles[0].id);
|
|
|
|
hostInstance.shownTitle = 'second';
|
|
viewContainerFixture.changeDetectorRef.markForCheck();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(container.getAttribute('aria-labelledby')).toBe(titles[1].id);
|
|
}));
|
|
|
|
it('should add correct css class according to given [align] input in [mat-dialog-actions]', () => {
|
|
let actions = overlayContainerElement.querySelector('mat-dialog-actions')!;
|
|
|
|
expect(actions)
|
|
.withContext('Expected action buttons to not have class align-center')
|
|
.not.toHaveClass('mat-mdc-dialog-actions-align-center');
|
|
expect(actions)
|
|
.withContext('Expected action buttons to have class align-end')
|
|
.toHaveClass('mat-mdc-dialog-actions-align-end');
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('aria-labelledby', () => {
|
|
it('should be able to set a custom aria-labelledby', () => {
|
|
dialog.open(PizzaMsg, {
|
|
ariaLabelledBy: 'Labelled By',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(container.getAttribute('aria-labelledby')).toBe('Labelled By');
|
|
});
|
|
|
|
it(
|
|
'should not set the aria-labelledby automatically if it has an aria-label ' +
|
|
'and an aria-labelledby',
|
|
fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
ariaLabel: 'Hello there',
|
|
ariaLabelledBy: 'Labelled By',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
viewContainerFixture.detectChanges();
|
|
tick();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(container.hasAttribute('aria-labelledby')).toBe(false);
|
|
}),
|
|
);
|
|
|
|
it(
|
|
'should set the aria-labelledby attribute to the config provided aria-labelledby ' +
|
|
'instead of the mat-dialog-title id',
|
|
fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
ariaLabelledBy: 'Labelled By',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
let title = overlayContainerElement.querySelector('[mat-dialog-title]')!;
|
|
let container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
flush();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(title.id).withContext('Expected title element to have an id.').toBeTruthy();
|
|
expect(container.getAttribute('aria-labelledby')).toBe('Labelled By');
|
|
}),
|
|
);
|
|
});
|
|
|
|
describe('aria-label', () => {
|
|
it('should be able to set a custom aria-label', () => {
|
|
dialog.open(PizzaMsg, {ariaLabel: 'Hello there', viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(container.getAttribute('aria-label')).toBe('Hello there');
|
|
});
|
|
|
|
it('should not set the aria-labelledby automatically if it has an aria-label', fakeAsync(() => {
|
|
dialog.open(ContentElementDialog, {
|
|
ariaLabel: 'Hello there',
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
viewContainerFixture.detectChanges();
|
|
tick();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
const container = overlayContainerElement.querySelector('mat-dialog-container')!;
|
|
expect(container.hasAttribute('aria-labelledby')).toBe(false);
|
|
}));
|
|
});
|
|
|
|
it('should dispose backdrop if containing dialog view is destroyed', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeDefined();
|
|
|
|
dialogRef.close();
|
|
viewContainerFixture.componentInstance.showChildView = false;
|
|
viewContainerFixture.changeDetectorRef.markForCheck();
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBe(null);
|
|
}));
|
|
});
|
|
|
|
describe('MatDialog with a parent MatDialog', () => {
|
|
let parentDialog: MatDialog;
|
|
let childDialog: MatDialog;
|
|
let overlayContainerElement: HTMLElement;
|
|
let fixture: ComponentFixture<ComponentThatProvidesMatDialog>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [MatDialogModule, NoopAnimationsModule, ComponentThatProvidesMatDialog],
|
|
providers: [
|
|
{
|
|
provide: OverlayContainer,
|
|
useFactory: () => {
|
|
overlayContainerElement = document.createElement('div');
|
|
return {getContainerElement: () => overlayContainerElement};
|
|
},
|
|
},
|
|
{provide: Location, useClass: SpyLocation},
|
|
],
|
|
});
|
|
|
|
parentDialog = TestBed.inject(MatDialog);
|
|
fixture = TestBed.createComponent(ComponentThatProvidesMatDialog);
|
|
childDialog = fixture.componentInstance.dialog;
|
|
fixture.detectChanges();
|
|
}));
|
|
|
|
afterEach(() => {
|
|
overlayContainerElement.innerHTML = '';
|
|
});
|
|
|
|
it('should close dialogs opened by a parent when calling closeAll on a child MatDialog', fakeAsync(() => {
|
|
parentDialog.open(PizzaMsg);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.textContent)
|
|
.withContext('Expected a dialog to be opened')
|
|
.toContain('Pizza');
|
|
|
|
childDialog.closeAll();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.textContent!.trim())
|
|
.withContext('Expected closeAll on child MatDialog to close dialog opened by parent')
|
|
.toBe('');
|
|
}));
|
|
|
|
it('should close dialogs opened by a child when calling closeAll on a parent MatDialog', fakeAsync(() => {
|
|
childDialog.open(PizzaMsg);
|
|
fixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.textContent)
|
|
.withContext('Expected a dialog to be opened')
|
|
.toContain('Pizza');
|
|
|
|
parentDialog.closeAll();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.textContent!.trim())
|
|
.withContext('Expected closeAll on parent MatDialog to close dialog opened by child')
|
|
.toBe('');
|
|
}));
|
|
|
|
it('should close the top dialog via the escape key', fakeAsync(() => {
|
|
childDialog.open(PizzaMsg);
|
|
|
|
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
|
|
}));
|
|
|
|
it('should not close the parent dialogs when a child is destroyed', fakeAsync(() => {
|
|
parentDialog.open(PizzaMsg);
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.textContent)
|
|
.withContext('Expected a dialog to be opened')
|
|
.toContain('Pizza');
|
|
|
|
childDialog.ngOnDestroy();
|
|
fixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.textContent)
|
|
.withContext('Expected a dialog to be opened')
|
|
.toContain('Pizza');
|
|
}));
|
|
});
|
|
|
|
describe('MatDialog with default options', () => {
|
|
let dialog: MatDialog;
|
|
let overlayContainerElement: HTMLElement;
|
|
|
|
let testViewContainerRef: ViewContainerRef;
|
|
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
const defaultConfig = {
|
|
hasBackdrop: false,
|
|
disableClose: true,
|
|
width: '100px',
|
|
height: '100px',
|
|
minWidth: '50px',
|
|
minHeight: '50px',
|
|
maxWidth: '150px',
|
|
maxHeight: '150px',
|
|
autoFocus: 'dialog',
|
|
};
|
|
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
MatDialogModule,
|
|
NoopAnimationsModule,
|
|
ComponentWithChildViewContainer,
|
|
DirectiveWithViewContainer,
|
|
],
|
|
providers: [{provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: defaultConfig}],
|
|
});
|
|
|
|
dialog = TestBed.inject(MatDialog);
|
|
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
|
|
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
|
|
viewContainerFixture.detectChanges();
|
|
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
|
|
}));
|
|
|
|
it('should use the provided defaults', () => {
|
|
dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
|
|
|
|
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
|
|
|
expect(document.activeElement!.tagName).not.toBe('INPUT');
|
|
|
|
let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
expect(overlayPane.style.width).toBe('100px');
|
|
expect(overlayPane.style.height).toBe('100px');
|
|
expect(overlayPane.style.minWidth).toBe('50px');
|
|
expect(overlayPane.style.minHeight).toBe('50px');
|
|
expect(overlayPane.style.maxWidth).toBe('150px');
|
|
expect(overlayPane.style.maxHeight).toBe('150px');
|
|
});
|
|
|
|
it('should be overridable by open() options', fakeAsync(() => {
|
|
dialog.open(PizzaMsg, {
|
|
hasBackdrop: true,
|
|
disableClose: false,
|
|
viewContainerRef: testViewContainerRef,
|
|
});
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
|
|
|
|
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
|
viewContainerFixture.detectChanges();
|
|
flush();
|
|
|
|
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeFalsy();
|
|
}));
|
|
});
|
|
|
|
describe('MatDialog with animations enabled', () => {
|
|
let dialog: MatDialog;
|
|
|
|
let testViewContainerRef: ViewContainerRef;
|
|
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [
|
|
MatDialogModule,
|
|
BrowserAnimationsModule,
|
|
ComponentWithChildViewContainer,
|
|
DirectiveWithViewContainer,
|
|
],
|
|
});
|
|
|
|
dialog = TestBed.inject(MatDialog);
|
|
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
|
|
viewContainerFixture.detectChanges();
|
|
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
|
|
}));
|
|
|
|
it('should emit when dialog opening animation is complete', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
const spy = jasmine.createSpy('afterOpen spy');
|
|
|
|
dialogRef.afterOpened().subscribe(spy);
|
|
|
|
viewContainerFixture.detectChanges();
|
|
|
|
// callback should not be called before animation is complete
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
tick(OPEN_ANIMATION_DURATION);
|
|
flush();
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should return the current state of the dialog', fakeAsync(() => {
|
|
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
|
|
|
expect(dialogRef.getState()).toBe(MatDialogState.OPEN);
|
|
dialogRef.close();
|
|
viewContainerFixture.detectChanges();
|
|
|
|
expect(dialogRef.getState()).toBe(MatDialogState.CLOSING);
|
|
|
|
// Ensure that the closing state is still set if half of the animation has
|
|
// passed by. The dialog state should be only set to `closed` when the dialog
|
|
// finished the close animation.
|
|
tick(CLOSE_ANIMATION_DURATION / 2);
|
|
expect(dialogRef.getState()).toBe(MatDialogState.CLOSING);
|
|
|
|
// Flush the remaining duration of the closing animation. We flush all other remaining
|
|
// tasks (e.g. the fallback close timeout) to avoid fakeAsync pending timer failures.
|
|
flush();
|
|
expect(dialogRef.getState()).toBe(MatDialogState.CLOSED);
|
|
}));
|
|
});
|
|
|
|
describe('MatDialog with explicit injector provided', () => {
|
|
let overlayContainerElement: HTMLElement;
|
|
let fixture: ComponentFixture<ModuleBoundDialogParentComponent>;
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [MatDialogModule, BrowserAnimationsModule, ModuleBoundDialogParentComponent],
|
|
});
|
|
|
|
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
|
|
fixture = TestBed.createComponent(ModuleBoundDialogParentComponent);
|
|
}));
|
|
|
|
it('should use the standalone injector and render the dialog successfully', () => {
|
|
fixture.componentInstance.openDialog();
|
|
fixture.detectChanges();
|
|
|
|
expect(
|
|
overlayContainerElement.querySelector('module-bound-dialog-child-component')!.innerHTML,
|
|
).toEqual('<p>Pasta</p>');
|
|
});
|
|
});
|
|
|
|
@Directive({
|
|
selector: 'dir-with-view-container',
|
|
standalone: true,
|
|
})
|
|
class DirectiveWithViewContainer {
|
|
viewContainerRef = inject(ViewContainerRef);
|
|
}
|
|
|
|
@Component({
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: 'hello',
|
|
standalone: false,
|
|
})
|
|
class ComponentWithOnPushViewContainer {
|
|
viewContainerRef = inject(ViewContainerRef);
|
|
}
|
|
|
|
@Component({
|
|
selector: 'arbitrary-component',
|
|
template: `@if (showChildView) {<dir-with-view-container></dir-with-view-container>}`,
|
|
standalone: true,
|
|
imports: [DirectiveWithViewContainer],
|
|
})
|
|
class ComponentWithChildViewContainer {
|
|
showChildView = true;
|
|
|
|
@ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer;
|
|
|
|
get childViewContainer() {
|
|
return this.childWithViewContainer.viewContainerRef;
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
selector: 'arbitrary-component-with-template-ref',
|
|
template: `<ng-template let-data let-dialogRef="dialogRef">
|
|
Cheese {{localValue}} {{data?.value}}{{setDialogRef(dialogRef)}}</ng-template>`,
|
|
standalone: true,
|
|
})
|
|
class ComponentWithTemplateRef {
|
|
localValue: string;
|
|
dialogRef: MatDialogRef<any>;
|
|
|
|
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
|
|
|
setDialogRef(dialogRef: MatDialogRef<any>): string {
|
|
this.dialogRef = dialogRef;
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/** Simple component for testing ComponentPortal. */
|
|
@Component({
|
|
template: '<p>Pizza</p> <input> <button>Close</button>',
|
|
standalone: true,
|
|
})
|
|
class PizzaMsg {
|
|
dialogRef = inject<MatDialogRef<PizzaMsg>>(MatDialogRef);
|
|
dialogInjector = inject(Injector);
|
|
directionality = inject(Directionality);
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
@if (shouldShowTitle('first')) {
|
|
<h2 mat-dialog-title>This is the first title</h2>
|
|
}
|
|
@if (shouldShowTitle('second')) {
|
|
<h2 mat-dialog-title>This is the second title</h2>
|
|
}
|
|
@if (shouldShowTitle('third')) {
|
|
<h2 mat-dialog-title>This is the third title</h2>
|
|
}
|
|
<mat-dialog-content>Lorem ipsum dolor sit amet.</mat-dialog-content>
|
|
<mat-dialog-actions align="end">
|
|
<button mat-dialog-close>Close</button>
|
|
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
|
|
<button
|
|
class="close-with-aria-label"
|
|
aria-label="Best close button ever"
|
|
[mat-dialog-close]="true"></button>
|
|
<div mat-dialog-close>Should not close</div>
|
|
<button class="with-submit" type="submit" mat-dialog-close>Should have submit</button>
|
|
</mat-dialog-actions>
|
|
`,
|
|
standalone: true,
|
|
imports: [MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose],
|
|
})
|
|
class ContentElementDialog {
|
|
shownTitle: 'first' | 'second' | 'third' | 'all' = 'first';
|
|
|
|
shouldShowTitle(name: string) {
|
|
return this.shownTitle === 'all' || this.shownTitle === name;
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<ng-template>
|
|
@if (shouldShowTitle('first')) {
|
|
<h2 mat-dialog-title>This is the first title</h2>
|
|
}
|
|
@if (shouldShowTitle('second')) {
|
|
<h2 mat-dialog-title>This is the second title</h2>
|
|
}
|
|
@if (shouldShowTitle('third')) {
|
|
<h2 mat-dialog-title>This is the third title</h2>
|
|
}
|
|
<mat-dialog-content>Lorem ipsum dolor sit amet.</mat-dialog-content>
|
|
<mat-dialog-actions align="end">
|
|
<button mat-dialog-close>Close</button>
|
|
<button class="close-with-true" [mat-dialog-close]="true">Close and return true</button>
|
|
<button
|
|
class="close-with-aria-label"
|
|
aria-label="Best close button ever"
|
|
[mat-dialog-close]="true"></button>
|
|
<div mat-dialog-close>Should not close</div>
|
|
<button class="with-submit" type="submit" mat-dialog-close>Should have submit</button>
|
|
</mat-dialog-actions>
|
|
</ng-template>
|
|
`,
|
|
standalone: true,
|
|
imports: [MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose],
|
|
})
|
|
class ComponentWithContentElementTemplateRef {
|
|
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
|
|
|
shownTitle: 'first' | 'second' | 'third' | 'all' = 'first';
|
|
|
|
shouldShowTitle(name: string) {
|
|
return this.shownTitle === 'all' || this.shownTitle === name;
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
template: '',
|
|
providers: [MatDialog],
|
|
standalone: true,
|
|
})
|
|
class ComponentThatProvidesMatDialog {
|
|
dialog = inject(MatDialog);
|
|
}
|
|
|
|
/** Simple component for testing ComponentPortal. */
|
|
@Component({
|
|
template: '',
|
|
standalone: true,
|
|
})
|
|
class DialogWithInjectedData {
|
|
data = inject(MAT_DIALOG_DATA);
|
|
}
|
|
|
|
@Component({
|
|
template: '<p>Pasta</p>',
|
|
standalone: true,
|
|
})
|
|
class DialogWithoutFocusableElements {}
|
|
|
|
@Component({
|
|
template: `<button>I'm a button</button>`,
|
|
encapsulation: ViewEncapsulation.ShadowDom,
|
|
standalone: false,
|
|
})
|
|
class ShadowDomComponent {}
|
|
|
|
@Component({
|
|
template: '',
|
|
standalone: true,
|
|
})
|
|
class ModuleBoundDialogParentComponent {
|
|
private _injector = inject(Injector);
|
|
private _dialog = inject(MatDialog);
|
|
|
|
openDialog(): void {
|
|
const ngModuleRef = createNgModuleRef(
|
|
ModuleBoundDialogModule,
|
|
/* parentInjector */ this._injector,
|
|
);
|
|
|
|
this._dialog.open(ModuleBoundDialogComponent, {injector: ngModuleRef.injector});
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
class ModuleBoundDialogService {
|
|
name = 'Pasta';
|
|
}
|
|
|
|
@Component({
|
|
template: '<module-bound-dialog-child-component></module-bound-dialog-child-component>',
|
|
standalone: true,
|
|
imports: [forwardRef(() => ModuleBoundDialogChildComponent)],
|
|
})
|
|
class ModuleBoundDialogComponent {}
|
|
|
|
@Component({
|
|
selector: 'module-bound-dialog-child-component',
|
|
template: '<p>{{service.name}}</p>',
|
|
standalone: true,
|
|
})
|
|
class ModuleBoundDialogChildComponent {
|
|
service = inject(ModuleBoundDialogService);
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [ModuleBoundDialogComponent, ModuleBoundDialogChildComponent],
|
|
providers: [ModuleBoundDialogService],
|
|
})
|
|
class ModuleBoundDialogModule {}
|