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;
}