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; 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)).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 = 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); 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); 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; 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; 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: ``, standalone: true, imports: [DirectiveWithViewContainer], }) class ComponentWithChildViewContainer { @ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer; get childViewContainer() { return this.childWithViewContainer.viewContainerRef; } } @Component({ selector: 'arbitrary-component-with-template-ref', template: ` Cheese {{localValue}} {{data?.value}}{{setRef(bottomSheetRef)}}`, standalone: true, }) class ComponentWithTemplateRef { localValue: string; bottomSheetRef: MatBottomSheetRef; @ViewChild(TemplateRef) templateRef: TemplateRef; setRef(bottomSheetRef: MatBottomSheetRef): string { this.bottomSheetRef = bottomSheetRef; return ''; } } @Component({ template: '

Pizza

', standalone: true, }) class PizzaMsg { bottomSheetRef = inject>(MatBottomSheetRef); injector = inject(Injector); directionality = inject(Directionality); } @Component({ template: '

Taco

', standalone: true, }) class TacoMsg {} @Component({ template: `

This is the title

This is the paragraph

`, 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: ``, encapsulation: ViewEncapsulation.ShadowDom, standalone: true, }) class ShadowDomComponent {}