sass-references/angular-material/material/bottom-sheet/bottom-sheet.spec.ts

1098 lines
35 KiB
TypeScript

import {Directionality} from '@angular/cdk/bidi';
import {A, ESCAPE} from '@angular/cdk/keycodes';
import {OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
dispatchEvent,
dispatchKeyboardEvent,
} from '@angular/cdk/testing/private';
import {Location} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {
Component,
ComponentRef,
Directive,
Injector,
TemplateRef,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
inject,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
fakeAsync,
flush,
flushMicrotasks,
tick,
} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MAT_BOTTOM_SHEET_DEFAULT_OPTIONS, MatBottomSheet} from './bottom-sheet';
import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetConfig} from './bottom-sheet-config';
import {MatBottomSheetModule} from './bottom-sheet-module';
import {MatBottomSheetRef} from './bottom-sheet-ref';
describe('MatBottomSheet', () => {
let bottomSheet: MatBottomSheet;
let overlayContainerElement: HTMLElement;
let viewportRuler: ViewportRuler;
let testViewContainerRef: ViewContainerRef;
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
let mockLocation: SpyLocation;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [
MatBottomSheetModule,
NoopAnimationsModule,
ComponentWithChildViewContainer,
ComponentWithTemplateRef,
ContentElementDialog,
PizzaMsg,
TacoMsg,
DirectiveWithViewContainer,
BottomSheetWithInjectedData,
ShadowDomComponent,
],
providers: [{provide: Location, useClass: SpyLocation}],
});
bottomSheet = TestBed.inject(MatBottomSheet);
viewportRuler = TestBed.inject(ViewportRuler);
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
mockLocation = TestBed.inject(Location) as SpyLocation;
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
viewContainerFixture.detectChanges();
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
}));
it('should open a bottom sheet with a component', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(bottomSheetRef.instance instanceof PizzaMsg).toBe(true);
expect(bottomSheetRef.componentRef instanceof ComponentRef).toBe(true);
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should open a bottom sheet with a template', () => {
const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef);
templateRefFixture.componentInstance.localValue = 'Bees';
templateRefFixture.detectChanges();
const bottomSheetRef = bottomSheet.open(templateRefFixture.componentInstance.templateRef, {
data: {value: 'Knees'},
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Cheese Bees Knees');
expect(templateRefFixture.componentInstance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should position the bottom sheet at the bottom center of the screen', () => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
const containerElement = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
const containerRect = containerElement.getBoundingClientRect();
const viewportSize = viewportRuler.getViewportSize();
expect(Math.floor(containerRect.bottom)).toBe(Math.floor(viewportSize.height));
expect(Math.floor(containerRect.left + containerRect.width / 2)).toBe(
Math.floor(viewportSize.width / 2),
);
});
it('should emit when the bottom sheet opening animation is complete', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('afterOpened spy');
bottomSheetRef.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 the correct injector', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
const injector = bottomSheetRef.instance.injector;
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
expect(injector.get<DirectiveWithViewContainer>(DirectiveWithViewContainer)).toBeTruthy();
});
it('should open a bottom sheet with a component and no ViewContainerRef', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
expect(overlayContainerElement.textContent).toContain('Pizza');
expect(bottomSheetRef.instance instanceof PizzaMsg).toBe(true);
expect(bottomSheetRef.instance.bottomSheetRef).toBe(bottomSheetRef);
});
it('should apply the correct role to the container element', () => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
const containerElement = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
expect(containerElement.getAttribute('role')).toBe('dialog');
expect(containerElement.getAttribute('aria-modal')).toBe('true');
});
it('should close a bottom sheet via the escape key', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeNull();
expect(event.defaultPrevented).toBe(true);
}));
it('should not close a bottom sheet via the escape key with a modifier', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true});
dispatchEvent(document.body, event);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
expect(event.defaultPrevented).toBe(false);
}));
it('should close when clicking on the overlay backdrop', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeFalsy();
}));
it('should dispose of bottom sheet if view container is destroyed while animating', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
bottomSheetRef.dismiss();
viewContainerFixture.detectChanges();
viewContainerFixture.destroy();
flush();
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull();
}));
it('should emit the backdropClick stream when clicking on the overlay backdrop', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('backdropClick spy');
bottomSheetRef.backdropClick().subscribe(spy);
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
backdrop.click();
expect(spy).toHaveBeenCalledTimes(1);
viewContainerFixture.detectChanges();
flush();
// Additional clicks after the bottom sheet was 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 bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
const spy = jasmine.createSpy('keyboardEvent spy');
bottomSheetRef.keydownEvents().subscribe(spy);
viewContainerFixture.detectChanges();
flush();
const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
const container = overlayContainerElement.querySelector(
'mat-bottom-sheet-container',
) as HTMLElement;
dispatchKeyboardEvent(document.body, 'keydown', A);
dispatchKeyboardEvent(backdrop, 'keydown', A);
dispatchKeyboardEvent(container, 'keydown', A);
expect(spy).toHaveBeenCalledTimes(3);
}));
it('should allow setting the layout direction', () => {
bottomSheet.open(PizzaMsg, {direction: 'rtl'});
viewContainerFixture.detectChanges();
const overlayPane = overlayContainerElement.querySelector('.cdk-global-overlay-wrapper')!;
expect(overlayPane.getAttribute('dir')).toBe('rtl');
});
it('should inject the correct direction in the instantiated component', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {direction: 'rtl'});
viewContainerFixture.detectChanges();
expect(bottomSheetRef.instance.directionality.value).toBe('rtl');
});
it('should fall back to injecting the global direction if none is passed by the config', () => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {});
viewContainerFixture.detectChanges();
expect(bottomSheetRef.instance.directionality.value).toBe('ltr');
});
it('should be able to set a custom panel class', () => {
bottomSheet.open(PizzaMsg, {
panelClass: 'custom-panel-class',
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.custom-panel-class')).toBeTruthy();
});
it('should be able to set a custom aria-label', () => {
bottomSheet.open(PizzaMsg, {
ariaLabel: 'Hello there',
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
const container = overlayContainerElement.querySelector('mat-bottom-sheet-container')!;
expect(container.getAttribute('aria-label')).toBe('Hello there');
});
it('should be able to get dismissed through the service', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
bottomSheet.dismiss();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.childElementCount).toBe(0);
}));
it('should dismiss the bottom sheet when the service is destroyed', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
bottomSheet.ngOnDestroy();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.childElementCount).toBe(0);
}));
it('should open a new bottom sheet after dismissing a previous sheet', fakeAsync(() => {
const config: MatBottomSheetConfig = {viewContainerRef: testViewContainerRef};
let bottomSheetRef: MatBottomSheetRef<any> = bottomSheet.open(PizzaMsg, config);
viewContainerFixture.detectChanges();
bottomSheetRef.dismiss();
viewContainerFixture.detectChanges();
// Wait for the dismiss animation to finish.
flush();
bottomSheetRef = bottomSheet.open(TacoMsg, config);
viewContainerFixture.detectChanges();
// Wait for the open animation to finish.
flush();
expect(bottomSheetRef.containerInstance._animationState)
.withContext(`Expected the animation state would be 'visible'.`)
.toBe('visible');
}));
it('should remove past bottom sheets when opening new ones', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
bottomSheet.open(TacoMsg);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.textContent).toContain('Taco');
}));
it('should not throw when opening multiple bottom sheet in quick succession', fakeAsync(() => {
expect(() => {
for (let i = 0; i < 3; i++) {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
}
flush();
}).not.toThrow();
}));
it('should remove bottom sheet if another is shown while its still animating open', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
viewContainerFixture.detectChanges();
bottomSheet.open(TacoMsg);
viewContainerFixture.detectChanges();
tick();
expect(overlayContainerElement.textContent).toContain('Taco');
tick(500);
}));
it('should emit after being dismissed', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg);
const spy = jasmine.createSpy('afterDismissed spy');
bottomSheetRef.afterDismissed().subscribe(spy);
viewContainerFixture.detectChanges();
bottomSheetRef.dismiss();
viewContainerFixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledTimes(1);
}));
it('should be able to pass a result back to the dismissed stream', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open<PizzaMsg, any, number>(PizzaMsg);
const spy = jasmine.createSpy('afterDismissed spy');
bottomSheetRef.afterDismissed().subscribe(spy);
viewContainerFixture.detectChanges();
bottomSheetRef.dismiss(1337);
viewContainerFixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledWith(1337);
}));
it('should be able to pass data when dismissing through the service', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open<PizzaMsg, any, number>(PizzaMsg);
const spy = jasmine.createSpy('afterDismissed spy');
bottomSheetRef.afterDismissed().subscribe(spy);
viewContainerFixture.detectChanges();
bottomSheet.dismiss(1337);
viewContainerFixture.detectChanges();
flush();
expect(spy).toHaveBeenCalledWith(1337);
}));
it('should close the bottom sheet when going forwards/backwards in history', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
mockLocation.simulateUrlPop('');
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeFalsy();
}));
it('should close the bottom sheet when the location hash changes', fakeAsync(() => {
bottomSheet.open(PizzaMsg);
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
mockLocation.simulateHashChange('');
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeFalsy();
}));
it('should allow the consumer to disable closing a bottom sheet on navigation', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {closeOnNavigation: false});
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
mockLocation.simulateUrlPop('');
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
}));
it('should be able to attach a custom scroll strategy', () => {
const scrollStrategy: ScrollStrategy = {
attach: () => {},
enable: jasmine.createSpy('scroll strategy enable spy'),
disable: () => {},
};
bottomSheet.open(PizzaMsg, {scrollStrategy});
expect(scrollStrategy.enable).toHaveBeenCalled();
});
it('should contain the height style properties on overlay pane', () => {
bottomSheet.open(PizzaMsg, {
panelClass: 'height--pane',
height: '300px',
maxHeight: 400, // this is converted into pixels
minHeight: 200, // this is converted into pixels
});
viewContainerFixture.detectChanges();
const paneElement = overlayContainerElement.querySelector('.height--pane') as HTMLElement;
expect(paneElement).toBeTruthy();
expect(paneElement.style.height).toBe('300px');
expect(paneElement.style.maxHeight).toBe('400px');
expect(paneElement.style.minHeight).toBe('200px');
});
describe('passing in data', () => {
it('should be able to pass in data', () => {
const config = {
data: {
stringParam: 'hello',
dateParam: new Date(),
},
};
const instance = bottomSheet.open(BottomSheetWithInjectedData, config).instance;
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(() => {
const bottomSheetRef = bottomSheet.open(BottomSheetWithInjectedData);
expect(bottomSheetRef.instance.data).toBeNull();
}).not.toThrow();
});
});
describe('disableClose option', () => {
it('should prevent closing via clicks on the backdrop', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
disableClose: true,
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
) as HTMLElement;
backdrop.click();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
}));
it('should prevent closing via the escape key', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
disableClose: true,
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
}));
it('should allow for the disableClose option to be updated while open', fakeAsync(() => {
const bottomSheetRef = bottomSheet.open(PizzaMsg, {
disableClose: true,
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
const backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
) as HTMLElement;
backdrop.click();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
bottomSheetRef.disableClose = false;
backdrop.click();
viewContainerFixture.detectChanges();
flush();
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeFalsy();
}));
});
describe('hasBackdrop option', () => {
it('should have a backdrop', () => {
bottomSheet.open(PizzaMsg, {
hasBackdrop: true,
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeTruthy();
});
it('should not have a backdrop', () => {
bottomSheet.open(PizzaMsg, {
hasBackdrop: false,
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
});
});
describe('backdropClass option', () => {
it('should have default backdrop class', () => {
bottomSheet.open(PizzaMsg, {
backdropClass: '',
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-dark-backdrop')).toBeTruthy();
});
it('should have custom backdrop class', () => {
bottomSheet.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 bottom sheet container by default', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
expect(document.activeElement!.tagName)
.withContext('Expected bottom sheet container to be focused.')
.toBe('MAT-BOTTOM-SHEET-CONTAINER');
}));
it('should create a focus trap if autoFocus is disabled', fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
autoFocus: false,
});
viewContainerFixture.detectChanges();
flush();
const focusTrapAnchors = overlayContainerElement.querySelectorAll('.cdk-focus-trap-anchor');
expect(focusTrapAnchors.length).toBeGreaterThan(0);
}));
it(
'should focus the first tabbable element of the bottom sheet on open when' +
'autoFocus is set to "first-tabbable"',
async () => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
autoFocus: 'first-tabbable',
});
viewContainerFixture.detectChanges();
await viewContainerFixture.whenStable();
viewContainerFixture.detectChanges();
expect(document.activeElement!.tagName)
.withContext('Expected first tabbable element (input) in the dialog to be focused.')
.toBe('INPUT');
},
);
it(
'should focus the bottom sheet element on open when autoFocus is set to ' +
'"dialog" (the default)',
fakeAsync(() => {
bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
});
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
let container = overlayContainerElement.querySelector(
'.mat-bottom-sheet-container',
) as HTMLInputElement;
expect(document.activeElement)
.withContext('Expected container to be focused on open')
.toBe(container);
}),
);
it(
'should focus the bottom sheet element on open when autoFocus is set to ' + '"first-heading"',
fakeAsync(() => {
bottomSheet.open(ContentElementDialog, {
viewContainerRef: testViewContainerRef,
autoFocus: 'first-heading',
});
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
let firstHeader = overlayContainerElement.querySelector(
'h1[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 on open when ' +
'autoFocus is set to a css selector',
fakeAsync(() => {
bottomSheet.open(ContentElementDialog, {
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 re-focus trigger element when bottom sheet closes', fakeAsync(() => {
const button = document.createElement('button');
button.id = 'bottom-sheet-trigger';
document.body.appendChild(button);
button.focus();
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
flush();
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expected the focus to change when sheet was opened.',
);
bottomSheetRef.dismiss();
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expcted the focus not to have changed before the animation finishes.',
);
flush();
viewContainerFixture.detectChanges();
tick(500);
expect(document.activeElement!.id)
.withContext('Expected that the trigger was refocused after the sheet is closed.')
.toBe('bottom-sheet-trigger');
button.remove();
}));
it('should be able to disable focus restoration', fakeAsync(() => {
const button = document.createElement('button');
button.id = 'bottom-sheet-trigger';
document.body.appendChild(button);
button.focus();
const bottomSheetRef = bottomSheet.open(PizzaMsg, {
viewContainerRef: testViewContainerRef,
restoreFocus: false,
});
flush();
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expected the focus to change when sheet was opened.',
);
bottomSheetRef.dismiss();
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expcted the focus not to have changed before the animation finishes.',
);
flush();
viewContainerFixture.detectChanges();
tick(500);
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expected the trigger not to be refocused on close.',
);
button.remove();
}));
it('should not move focus if it was moved outside the sheet while animating', fakeAsync(() => {
// Create a element that has focus before the bottom sheet is opened.
const button = document.createElement('button');
const otherButton = document.createElement('button');
const body = document.body;
button.id = 'bottom-sheet-trigger';
otherButton.id = 'other-button';
body.appendChild(button);
body.appendChild(otherButton);
button.focus();
const bottomSheetRef = bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
flush();
viewContainerFixture.detectChanges();
flush();
viewContainerFixture.detectChanges();
expect(document.activeElement!.id).not.toBe(
'bottom-sheet-trigger',
'Expected the focus to change when the bottom sheet was opened.',
);
// Start the closing sequence and move focus out of bottom sheet.
bottomSheetRef.dismiss();
otherButton.focus();
expect(document.activeElement!.id)
.withContext('Expected focus to be on the alternate button.')
.toBe('other-button');
flushMicrotasks();
viewContainerFixture.detectChanges();
flush();
expect(document.activeElement!.id)
.withContext('Expected focus to stay on the alternate button.')
.toBe('other-button');
button.remove();
otherButton.remove();
}));
it('should re-focus trigger element inside the shadow DOM when the bottom sheet is dismissed', fakeAsync(() => {
if (!_supportsShadowDom()) {
return;
}
viewContainerFixture.destroy();
const fixture = TestBed.createComponent(ShadowDomComponent);
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'))!.nativeElement;
button.focus();
const ref = bottomSheet.open(PizzaMsg);
flushMicrotasks();
fixture.detectChanges();
flushMicrotasks();
const spy = spyOn(button, 'focus').and.callThrough();
ref.dismiss();
flushMicrotasks();
fixture.detectChanges();
tick(500);
expect(spy).toHaveBeenCalled();
}));
});
});
describe('MatBottomSheet with parent MatBottomSheet', () => {
let parentBottomSheet: MatBottomSheet;
let childBottomSheet: MatBottomSheet;
let overlayContainerElement: HTMLElement;
let fixture: ComponentFixture<ComponentThatProvidesMatBottomSheet>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatBottomSheetModule, NoopAnimationsModule, ComponentThatProvidesMatBottomSheet],
});
parentBottomSheet = TestBed.inject(MatBottomSheet);
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
fixture = TestBed.createComponent(ComponentThatProvidesMatBottomSheet);
childBottomSheet = fixture.componentInstance.bottomSheet;
fixture.detectChanges();
}));
it('should close bottom sheets opened by parent when opening from child', fakeAsync(() => {
parentBottomSheet.open(PizzaMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected a bottom sheet to be opened')
.toContain('Pizza');
childBottomSheet.open(TacoMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected parent bottom sheet to be dismissed by opening from child')
.toContain('Taco');
}));
it('should close bottom sheets opened by child when opening from parent', fakeAsync(() => {
childBottomSheet.open(PizzaMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected a bottom sheet to be opened')
.toContain('Pizza');
parentBottomSheet.open(TacoMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected child bottom sheet to be dismissed by opening from parent')
.toContain('Taco');
}));
it('should not close parent bottom sheet when child is destroyed', fakeAsync(() => {
parentBottomSheet.open(PizzaMsg);
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected a bottom sheet to be opened')
.toContain('Pizza');
childBottomSheet.ngOnDestroy();
fixture.detectChanges();
tick(1000);
expect(overlayContainerElement.textContent)
.withContext('Expected a bottom sheet to stay open')
.toContain('Pizza');
}));
});
describe('MatBottomSheet with default options', () => {
let bottomSheet: MatBottomSheet;
let overlayContainerElement: HTMLElement;
let testViewContainerRef: ViewContainerRef;
let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;
beforeEach(fakeAsync(() => {
const defaultConfig: MatBottomSheetConfig = {
hasBackdrop: false,
disableClose: true,
autoFocus: 'dialog',
};
TestBed.configureTestingModule({
imports: [
MatBottomSheetModule,
NoopAnimationsModule,
ComponentWithChildViewContainer,
DirectiveWithViewContainer,
],
providers: [{provide: MAT_BOTTOM_SHEET_DEFAULT_OPTIONS, useValue: defaultConfig}],
});
bottomSheet = TestBed.inject(MatBottomSheet);
overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
viewContainerFixture.detectChanges();
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
}));
it('should use the provided defaults', () => {
bottomSheet.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
viewContainerFixture.detectChanges();
expect(overlayContainerElement.querySelector('.cdk-overlay-backdrop')).toBeFalsy();
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
expect(document.activeElement!.tagName).not.toBe('INPUT');
});
it('should be overridable by open() options', fakeAsync(() => {
bottomSheet.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-bottom-sheet-container')).toBeFalsy();
}));
});
@Directive({
selector: 'dir-with-view-container',
standalone: true,
})
class DirectiveWithViewContainer {
viewContainerRef = inject(ViewContainerRef);
}
@Component({
template: `<dir-with-view-container></dir-with-view-container>`,
standalone: true,
imports: [DirectiveWithViewContainer],
})
class ComponentWithChildViewContainer {
@ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer;
get childViewContainer() {
return this.childWithViewContainer.viewContainerRef;
}
}
@Component({
selector: 'arbitrary-component-with-template-ref',
template: `<ng-template let-data let-bottomSheetRef="bottomSheetRef">
Cheese {{localValue}} {{data?.value}}{{setRef(bottomSheetRef)}}</ng-template>`,
standalone: true,
})
class ComponentWithTemplateRef {
localValue: string;
bottomSheetRef: MatBottomSheetRef<any>;
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
setRef(bottomSheetRef: MatBottomSheetRef<any>): string {
this.bottomSheetRef = bottomSheetRef;
return '';
}
}
@Component({
template: '<p>Pizza</p> <input> <button>Close</button>',
standalone: true,
})
class PizzaMsg {
bottomSheetRef = inject<MatBottomSheetRef<PizzaMsg>>(MatBottomSheetRef);
injector = inject(Injector);
directionality = inject(Directionality);
}
@Component({
template: '<p>Taco</p>',
standalone: true,
})
class TacoMsg {}
@Component({
template: `
<h1>This is the title</h1>
<p>This is the paragraph</p>
`,
standalone: true,
})
class ContentElementDialog {}
@Component({
template: '',
providers: [MatBottomSheet],
standalone: true,
imports: [MatBottomSheetModule],
})
class ComponentThatProvidesMatBottomSheet {
bottomSheet = inject(MatBottomSheet);
}
@Component({
template: '',
standalone: true,
})
class BottomSheetWithInjectedData {
data = inject(MAT_BOTTOM_SHEET_DATA);
}
@Component({
template: `<button>I'm a button</button>`,
encapsulation: ViewEncapsulation.ShadowDom,
standalone: true,
})
class ShadowDomComponent {}