diff --git a/libs/components/core/src/index.ts b/libs/components/core/src/index.ts index 6ce3e553e6..3cc071e7a2 100644 --- a/libs/components/core/src/index.ts +++ b/libs/components/core/src/index.ts @@ -94,6 +94,9 @@ export { SkyResizeObserverService } from './lib/modules/resize-observer/resize-o export { SkyScreenReaderLabelDirective } from './lib/modules/screen-reader-label/screen-reader-label.directive'; +export { SkyScrollShadowDirective } from './lib/modules/scroll-shadow/scroll-shadow.directive'; +export { SkyScrollShadowEventArgs } from './lib/modules/scroll-shadow/scroll-shadow-event-args'; + export { SkyScrollableHostService } from './lib/modules/scrollable-host/scrollable-host.service'; export { SkyStackingContext } from './lib/modules/stacking-context/stacking-context'; diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html new file mode 100644 index 0000000000..0d6d7db432 --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html @@ -0,0 +1,26 @@ +
+
+
+ +
+ +
diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss new file mode 100644 index 0000000000..8556008a69 --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss @@ -0,0 +1,17 @@ +:is(.scroll-shadow-test-header, .scroll-shadow-test-footer) { + height: 100px; +} + +.scroll-shadow-test-body { + overflow-y: scroll; + display: block; + max-height: calc(100vh - 200px); +} + +.scroll-shadow-test-content { + display: block; +} + +.scroll-shadow-test-wrapper { + max-height: 100vh; +} diff --git a/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts new file mode 100644 index 0000000000..aac1149dff --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectorRef, Component, inject } from '@angular/core'; + +import { SkyScrollShadowEventArgs } from '../scroll-shadow-event-args'; +import { SkyScrollShadowDirective } from '../scroll-shadow.directive'; + +@Component({ + selector: 'sky-scroll-shadow-fixture', + styleUrls: ['./scroll-shadow.component.fixture.scss'], + templateUrl: './scroll-shadow.component.fixture.html', + imports: [CommonModule, SkyScrollShadowDirective], + standalone: true, +}) +export class ScrollShadowFixtureComponent { + public enabled = true; + public height = 400; + public scrollShadow: SkyScrollShadowEventArgs | undefined; + + #changeDetector = inject(ChangeDetectorRef); + + public scrollShadowChange(args: SkyScrollShadowEventArgs): void { + this.scrollShadow = args; + this.#changeDetector.markForCheck(); + } +} diff --git a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts similarity index 58% rename from libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts rename to libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts index 17b6a80d79..6a4df4ccb2 100644 --- a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow-event-args.ts +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow-event-args.ts @@ -1,7 +1,7 @@ /** * @internal */ -export interface SkyModalScrollShadowEventArgs { +export interface SkyScrollShadowEventArgs { bottomShadow: string; topShadow: string; diff --git a/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts new file mode 100644 index 0000000000..bd68d2e24e --- /dev/null +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts @@ -0,0 +1,180 @@ +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; + +import { ScrollShadowFixtureComponent } from './fixtures/scroll-shadow.component.fixture'; + +// Wait for the next change detection cycle. This avoids having nested setTimeout() calls +// and using the Jasmine done() function. +function waitForMutationObserver(): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve()); + }); +} + +describe('Scroll shadow directive', () => { + function getScrollBody(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-body'); + } + + function getScrollFooter(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-footer'); + } + + function getScrollHeader(): HTMLElement | null { + return document.querySelector('.scroll-shadow-test-header'); + } + + function scrollElement(element: HTMLElement | null, yDistance: number): void { + if (element) { + element.scrollTop = yDistance; + SkyAppTestUtility.fireDomEvent(element, 'scroll'); + fixture.detectChanges(); + } + } + + function validateShadow( + el: HTMLElement | null, + expectedAlpha?: number, + ): void { + if (!el) { + fail('Element not provided'); + return; + } + + const boxShadowStyle = getComputedStyle(el).boxShadow; + + if (expectedAlpha) { + const rgbaMatch = boxShadowStyle.match( + /rgba\(0,\s*0,\s*0,\s*([0-9.]*)\)/, + ); + + if (!(rgbaMatch && rgbaMatch[1])) { + fail('No shadow found'); + } else { + const alpha = parseFloat(rgbaMatch[1]); + + expect(expectedAlpha).toBeCloseTo(alpha, 2); + } + } else { + expect(boxShadowStyle).toBe('none'); + } + } + + let fixture: ComponentFixture; + let cmp: ScrollShadowFixtureComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ScrollShadowFixtureComponent], + }); + + fixture = TestBed.createComponent(ScrollShadowFixtureComponent); + cmp = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it('should not show a shadow when the body is not scrollable when disabled', async () => { + cmp.enabled = false; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should not show a shadow when the body is scrollable when disabled', async () => { + cmp.height = 800; + cmp.enabled = false; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should not show a shadow when the body is not scrollable', async () => { + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(getScrollFooter()); + validateShadow(getScrollHeader()); + }); + + it('should progressively show a drop shadow as the modal content scrolls', async () => { + const headerEl = getScrollHeader(); + const contentEl = getScrollBody(); + const footerEl = getScrollFooter(); + + if (!contentEl) { + fail('Content element not found'); + return; + } + + cmp.height = 800; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + scrollElement(contentEl, 0); + validateShadow(headerEl); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 15); + validateShadow(headerEl, 0.15); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 30); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.3); + + scrollElement(contentEl, 31); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.3); + + scrollElement( + contentEl, + contentEl.scrollHeight - 15 - contentEl.clientHeight, + ); + validateShadow(headerEl, 0.3); + validateShadow(footerEl, 0.15); + + scrollElement(contentEl, contentEl.scrollHeight - contentEl.clientHeight); + validateShadow(headerEl, 0.3); + validateShadow(footerEl); + }); + + it('should update the shadow on window resize', async () => { + const headerEl = getScrollHeader(); + const contentEl = getScrollBody(); + const footerEl = getScrollFooter(); + + if (!contentEl) { + fail('Content element not found'); + return; + } + + cmp.height = 800; + fixture.detectChanges(); + await waitForMutationObserver(); + fixture.detectChanges(); + + validateShadow(headerEl); + validateShadow(footerEl, 0.3); + + spyOnProperty(Element.prototype, 'scrollTop').and.returnValue(15); + SkyAppTestUtility.fireDomEvent(window, 'resize'); + fixture.detectChanges(); + + validateShadow(headerEl, 0.15); + validateShadow(footerEl, 0.3); + }); +}); diff --git a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts similarity index 64% rename from libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts rename to libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts index 62e5404192..51946b830e 100644 --- a/libs/components/modals/src/lib/modules/modal/modal-scroll-shadow.directive.ts +++ b/libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.ts @@ -3,37 +3,53 @@ import { ElementRef, EventEmitter, HostListener, + Input, NgZone, OnDestroy, - OnInit, Output, inject, } from '@angular/core'; -import { SkyMutationObserverService } from '@skyux/core'; -import { SkyTheme, SkyThemeService } from '@skyux/theme'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { SkyModalScrollShadowEventArgs } from './modal-scroll-shadow-event-args'; +import { SkyMutationObserverService } from '../mutation/mutation-observer-service'; + +import { SkyScrollShadowEventArgs } from './scroll-shadow-event-args'; /** - * Raises an event when the box shadow for the modal header or footer should be adjusted + * Raises an event when the box shadow for a component's header or footer should be adjusted * based on the scroll position of the host element. * @internal */ @Directive({ standalone: true, - selector: '[skyModalScrollShadow]', + selector: '[skyScrollShadow]', }) -export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { - @Output() - public skyModalScrollShadow = - new EventEmitter(); +export class SkyScrollShadowDirective implements OnDestroy { + @Input() + public set skyScrollShadowEnabled(value: boolean) { + this.#_enabled = value; + + if (value) { + this.#initMutationObserver(); + } else { + this.#emitShadow({ + bottomShadow: 'none', + topShadow: 'none', + }); + + this.#destroyMutationObserver(); + } + } + + public get skyScrollShadowEnabled(): boolean { + return this.#_enabled; + } - #currentShadow: SkyModalScrollShadowEventArgs | undefined; + @Output() + public skyScrollShadow = new EventEmitter(); - #currentTheme: SkyTheme | undefined; + #currentShadow: SkyScrollShadowEventArgs | undefined; #mutationObserver: MutationObserver | undefined; @@ -42,7 +58,8 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { readonly #elRef = inject(ElementRef); readonly #mutationObserverSvc = inject(SkyMutationObserverService); readonly #ngZone = inject(NgZone); - readonly #themeSvc = inject(SkyThemeService, { optional: true }); + + #_enabled = false; @HostListener('window:resize') public windowResize(): void { @@ -54,27 +71,6 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { this.#checkForShadow(); } - public ngOnInit(): void { - if (this.#themeSvc) { - this.#themeSvc.settingsChange - .pipe(takeUntil(this.#ngUnsubscribe)) - .subscribe((themeSettings) => { - this.#currentTheme = themeSettings.currentSettings.theme; - - if (this.#currentTheme === SkyTheme.presets.modern) { - this.#initMutationObserver(); - } else { - this.#emitShadow({ - bottomShadow: 'none', - topShadow: 'none', - }); - - this.#destroyMutationObserver(); - } - }); - } - } - public ngOnDestroy(): void { this.#ngUnsubscribe.next(); this.#ngUnsubscribe.complete(); @@ -112,8 +108,8 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { } #checkForShadow(): void { - if (this.#currentTheme === SkyTheme.presets.modern) { - const el = this.#elRef.nativeElement; + if (this.skyScrollShadowEnabled) { + const el: Element = this.#elRef.nativeElement; const topShadow = this.#buildShadowStyle(el.scrollTop); @@ -136,13 +132,13 @@ export class SkyModalScrollShadowDirective implements OnInit, OnDestroy { return opacity > 0 ? `0px 1px 8px 0px rgba(0, 0, 0, ${opacity})` : 'none'; } - #emitShadow(shadow: SkyModalScrollShadowEventArgs): void { + #emitShadow(shadow: SkyScrollShadowEventArgs): void { if ( !this.#currentShadow || this.#currentShadow.bottomShadow !== shadow.bottomShadow || this.#currentShadow.topShadow !== shadow.topShadow ) { - this.skyModalScrollShadow.emit(shadow); + this.skyScrollShadow.emit(shadow); this.#currentShadow = shadow; } } diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.html b/libs/components/modals/src/lib/modules/modal/modal.component.html index 58132ebfad..e20f7f56e8 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.html +++ b/libs/components/modals/src/lib/modules/modal/modal.component.html @@ -77,7 +77,8 @@ tabindex="0" skyId [attr.aria-labelledby]="headerId.id" - (skyModalScrollShadow)="scrollShadowChange($event)" + (skyScrollShadow)="scrollShadowChange($event)" + [skyScrollShadowEnabled]="scrollShadowEnabled" #modalContentId="skyId" #modalContentWrapper > diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts b/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts index e49efadfd3..f9f99e0afe 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.component.spec.ts @@ -1058,7 +1058,7 @@ describe('Modal component', () => { await expectAsync(getModalElement()).toBeAccessible(); }); - describe('when modern theme', () => { + describe('scroll shadow', () => { function scrollContent(contentEl: HTMLElement, top: number): void { contentEl.scrollTop = top; @@ -1113,129 +1113,221 @@ describe('Modal component', () => { } } - it('should progressively show a drop shadow as the modal content scrolls', fakeAsync(() => { - setModernTheme(); - - const modalInstance1 = openModal(ModalTestComponent); - - const modalHeaderEl = document.querySelector( - '.sky-modal-header', - ) as HTMLElement; - const modalContentEl = document.querySelector( - '.sky-modal-content', - ) as HTMLElement; - const modalFooterEl = document.querySelector( - '.sky-modal-footer', - ) as HTMLElement; - - const fixtureContentEl = document.querySelector( - '.modal-fixture-content', - ) as HTMLElement; - fixtureContentEl.style.height = `${window.innerHeight + 100}px`; - - scrollContent(modalContentEl, 0); - validateShadow(modalHeaderEl); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 15); - validateShadow(modalHeaderEl, 0.15); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 30); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.3); - - scrollContent(modalContentEl, 31); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.3); - - scrollContent( - modalContentEl, - modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, - ); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl, 0.15); + describe('when default theme', () => { + it('should not show a drop shadow as the modal content scrolls', fakeAsync(() => { + const modalInstance1 = openModal(ModalTestComponent); + + const modalHeaderEl = document.querySelector( + '.sky-modal-header', + ) as HTMLElement; + const modalContentEl = document.querySelector( + '.sky-modal-content', + ) as HTMLElement; + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + fixtureContentEl.style.height = `${window.innerHeight + 100}px`; + + scrollContent(modalContentEl, 0); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 15); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 30); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent(modalContentEl, 31); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); + + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); - scrollContent( - modalContentEl, - modalContentEl.scrollHeight - modalContentEl.clientHeight, - ); - validateShadow(modalHeaderEl, 0.3); - validateShadow(modalFooterEl); + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl); - closeModal(modalInstance1); - })); + closeModal(modalInstance1); + })); - it('should check for shadow when elements are added to the modal content', fakeAsync(() => { - let mutateCallback: MutationCallback | undefined; + it('should not check for shadow when elements are added to the modal content', fakeAsync(() => { + let mutateCallback: MutationCallback | undefined; - const fakeMutationObserver: MutationObserver = { - observe: jasmine.createSpy('observe'), - disconnect: jasmine.createSpy('disconnect'), - takeRecords: jasmine.createSpy('takeRecords'), - }; + const fakeMutationObserver: MutationObserver = { + observe: jasmine.createSpy('observe'), + disconnect: jasmine.createSpy('disconnect'), + takeRecords: jasmine.createSpy('takeRecords'), + }; - spyOn(TestBed.inject(SkyMutationObserverService), 'create').and.callFake( - (cb) => { + spyOn( + TestBed.inject(SkyMutationObserverService), + 'create', + ).and.callFake((cb) => { mutateCallback = cb; return fakeMutationObserver; - }, - ); + }); - setModernTheme(); + const modalInstance1 = openModal(ModalTestComponent); - const modalInstance1 = openModal(ModalTestComponent); + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; - const modalFooterEl = document.querySelector( - '.sky-modal-footer', - ) as HTMLElement; + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; - const fixtureContentEl = document.querySelector( - '.modal-fixture-content', - ) as HTMLElement; + const childEl = document.createElement('div'); + childEl.style.height = `${window.innerHeight + 100}px`; + childEl.style.backgroundColor = 'red'; - const childEl = document.createElement('div'); - childEl.style.height = `${window.innerHeight + 100}px`; - childEl.style.backgroundColor = 'red'; + fixtureContentEl.appendChild(childEl); - fixtureContentEl.appendChild(childEl); + triggerMutation(mutateCallback, fakeMutationObserver); - triggerMutation(mutateCallback, fakeMutationObserver); + tick(); + getApplicationRef().tick(); - tick(); - getApplicationRef().tick(); + validateShadow(modalFooterEl); - validateShadow(modalFooterEl, 0.3); + fixtureContentEl.removeChild(childEl); - fixtureContentEl.removeChild(childEl); + triggerMutation(mutateCallback, fakeMutationObserver); - triggerMutation(mutateCallback, fakeMutationObserver); + tick(); + getApplicationRef().tick(); - tick(); - getApplicationRef().tick(); + validateShadow(modalFooterEl); - validateShadow(modalFooterEl); + closeModal(modalInstance1); + })); + }); - closeModal(modalInstance1); - })); + describe('when modern theme', () => { + it('should progressively show a drop shadow as the modal content scrolls', fakeAsync(() => { + setModernTheme(); + + const modalInstance1 = openModal(ModalTestComponent); + + const modalHeaderEl = document.querySelector( + '.sky-modal-header', + ) as HTMLElement; + const modalContentEl = document.querySelector( + '.sky-modal-content', + ) as HTMLElement; + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + fixtureContentEl.style.height = `${window.innerHeight + 100}px`; + + scrollContent(modalContentEl, 0); + validateShadow(modalHeaderEl); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 15); + validateShadow(modalHeaderEl, 0.15); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 30); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.3); + + scrollContent(modalContentEl, 31); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.3); + + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - 15 - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl, 0.15); - it('should not create multiple mutation observers', fakeAsync(() => { - const modalInstance1 = openModal(ModalTestComponent); + scrollContent( + modalContentEl, + modalContentEl.scrollHeight - modalContentEl.clientHeight, + ); + validateShadow(modalHeaderEl, 0.3); + validateShadow(modalFooterEl); - const mutationObserverCreateSpy = spyOn( - TestBed.inject(SkyMutationObserverService), - 'create', - ).and.callThrough(); + closeModal(modalInstance1); + })); - setModernTheme(); - setModernTheme(); - setModernTheme(); + it('should check for shadow when elements are added to the modal content', fakeAsync(() => { + let mutateCallback: MutationCallback | undefined; - expect(mutationObserverCreateSpy.calls.count()).toBe(1); + const fakeMutationObserver: MutationObserver = { + observe: jasmine.createSpy('observe'), + disconnect: jasmine.createSpy('disconnect'), + takeRecords: jasmine.createSpy('takeRecords'), + }; - closeModal(modalInstance1); - })); + spyOn( + TestBed.inject(SkyMutationObserverService), + 'create', + ).and.callFake((cb) => { + mutateCallback = cb; + + return fakeMutationObserver; + }); + + setModernTheme(); + + const modalInstance1 = openModal(ModalTestComponent); + + const modalFooterEl = document.querySelector( + '.sky-modal-footer', + ) as HTMLElement; + + const fixtureContentEl = document.querySelector( + '.modal-fixture-content', + ) as HTMLElement; + + const childEl = document.createElement('div'); + childEl.style.height = `${window.innerHeight + 100}px`; + childEl.style.backgroundColor = 'red'; + + fixtureContentEl.appendChild(childEl); + + triggerMutation(mutateCallback, fakeMutationObserver); + + tick(); + getApplicationRef().tick(); + + validateShadow(modalFooterEl, 0.3); + + fixtureContentEl.removeChild(childEl); + + triggerMutation(mutateCallback, fakeMutationObserver); + + tick(); + getApplicationRef().tick(); + + validateShadow(modalFooterEl); + + closeModal(modalInstance1); + })); + }); }); it('should pass accessibility with scrolling content', async () => { diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.ts b/libs/components/modals/src/lib/modules/modal/modal.component.ts index ffc9c3e308..51af8d7004 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.component.ts @@ -21,9 +21,12 @@ import { SkyIdModule, SkyLiveAnnouncerService, SkyResizeObserverMediaQueryService, + SkyScrollShadowDirective, + SkyScrollShadowEventArgs, } from '@skyux/core'; import { SkyHelpInlineModule } from '@skyux/help-inline'; import { SkyIconModule } from '@skyux/icon'; +import { SkyTheme, SkyThemeService } from '@skyux/theme'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -36,8 +39,6 @@ import { SkyModalError } from './modal-error'; import { SkyModalErrorsService } from './modal-errors.service'; import { SkyModalHeaderComponent } from './modal-header.component'; import { SkyModalHostService } from './modal-host.service'; -import { SkyModalScrollShadowEventArgs } from './modal-scroll-shadow-event-args'; -import { SkyModalScrollShadowDirective } from './modal-scroll-shadow.directive'; const ARIA_ROLE_DEFAULT = 'dialog'; @@ -62,7 +63,7 @@ const ARIA_ROLE_DEFAULT = 'dialog'; SkyIconModule, SkyIdModule, SkyModalHeaderComponent, - SkyModalScrollShadowDirective, + SkyScrollShadowDirective, SkyModalsResourcesModule, ], }) @@ -163,13 +164,18 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { public modalZIndex: number | undefined; - public scrollShadow: SkyModalScrollShadowEventArgs | undefined; + public scrollShadow: SkyScrollShadowEventArgs = { + bottomShadow: 'none', + topShadow: 'none', + }; public size: string; @ViewChild('modalContentWrapper', { read: ElementRef }) public modalContentWrapperElement: ElementRef | undefined; + protected scrollShadowEnabled = false; + #ngUnsubscribe = new Subject(); #_ariaDescribedBy: string | undefined; @@ -197,6 +203,7 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { readonly #config = inject(SkyModalConfiguration, { optional: true }) ?? new SkyModalConfiguration(); + readonly #themeSvc = inject(SkyThemeService, { optional: true }); constructor() { this.ariaDescribedBy = this.#config.ariaDescribedBy; @@ -280,6 +287,15 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { this.#changeDetector.markForCheck(); } }); + + if (this.#themeSvc) { + this.#themeSvc.settingsChange + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((themeSettings) => { + this.scrollShadowEnabled = + themeSettings.currentSettings.theme === SkyTheme.presets.modern; + }); + } } public ngAfterViewInit(): void { @@ -331,7 +347,7 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { this.#componentAdapter.handleWindowChange(this.#elRef); } - public scrollShadowChange(args: SkyModalScrollShadowEventArgs): void { + public scrollShadowChange(args: SkyScrollShadowEventArgs): void { this.scrollShadow = args; }