-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components/core): add scroll shadow directive
- Loading branch information
1 parent
d25e8cb
commit f9b3fff
Showing
9 changed files
with
346 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...mponents/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<div class="scroll-shadow-test-wrapper"> | ||
<div | ||
class="scroll-shadow-test-header" | ||
[ngStyle]="{ | ||
'box-shadow': scrollShadow?.topShadow ?? 'none' | ||
}" | ||
></div> | ||
<div | ||
class="scroll-shadow-test-body" | ||
(skyScrollShadow)="scrollShadowChange($event)" | ||
> | ||
<span | ||
class="scroll-shadow-test-content" | ||
[ngStyle]="{ | ||
'height.px': height | ||
}" | ||
></span> | ||
</div> | ||
<div | ||
class="scroll-shadow-test-footer" | ||
[ngStyle]="{ | ||
'box-shadow': scrollShadow?.bottomShadow ?? 'none' | ||
}" | ||
></div> | ||
</div> |
17 changes: 17 additions & 0 deletions
17
...mponents/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
24 changes: 24 additions & 0 deletions
24
...components/core/src/lib/modules/scroll-shadow/fixtures/scroll-shadow.component.fixture.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
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 height = 400; | ||
public scrollShadow: SkyScrollShadowEventArgs | undefined; | ||
|
||
#changeDetector = inject(ChangeDetectorRef); | ||
|
||
public scrollShadowChange(args: SkyScrollShadowEventArgs): void { | ||
this.scrollShadow = args; | ||
this.#changeDetector.markForCheck(); | ||
} | ||
} |
2 changes: 1 addition & 1 deletion
2
...s/modal/modal-scroll-shadow-event-args.ts → ...scroll-shadow/scroll-shadow-event-args.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
259 changes: 259 additions & 0 deletions
259
libs/components/core/src/lib/modules/scroll-shadow/scroll-shadow.directive.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
import { | ||
ComponentFixture, | ||
TestBed, | ||
fakeAsync, | ||
tick, | ||
} from '@angular/core/testing'; | ||
import { SkyAppTestUtility } from '@skyux-sdk/testing'; | ||
import { | ||
SkyTheme, | ||
SkyThemeMode, | ||
SkyThemeService, | ||
SkyThemeSettings, | ||
SkyThemeSettingsChange, | ||
} from '@skyux/theme'; | ||
|
||
import { BehaviorSubject } from 'rxjs'; | ||
|
||
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<void> { | ||
return new Promise<void>((resolve) => { | ||
setTimeout(() => resolve()); | ||
}); | ||
} | ||
|
||
describe('Scroll shadow directive', () => { | ||
function getScrollBody(): HTMLElement | null { | ||
return document.querySelector<HTMLElement>('.scroll-shadow-test-body'); | ||
} | ||
|
||
function getScrollFooter(): HTMLElement | null { | ||
return document.querySelector<HTMLElement>('.scroll-shadow-test-footer'); | ||
} | ||
|
||
function getScrollHeader(): HTMLElement | null { | ||
return document.querySelector<HTMLElement>('.scroll-shadow-test-header'); | ||
} | ||
|
||
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<ScrollShadowFixtureComponent>; | ||
let cmp: ScrollShadowFixtureComponent; | ||
|
||
describe('no theme service', () => { | ||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [ScrollShadowFixtureComponent], | ||
}); | ||
|
||
fixture = TestBed.createComponent(ScrollShadowFixtureComponent); | ||
cmp = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should not show a shadow when the body is not scrollable', async () => { | ||
validateShadow(getScrollFooter()); | ||
validateShadow(getScrollHeader()); | ||
}); | ||
|
||
it('should not show a shadow when the body is scrollable', async () => { | ||
cmp.height = 800; | ||
fixture.detectChanges(); | ||
await waitForMutationObserver(); | ||
fixture.detectChanges(); | ||
|
||
validateShadow(getScrollFooter()); | ||
validateShadow(getScrollHeader()); | ||
}); | ||
}); | ||
|
||
describe('default theme', () => { | ||
const mockThemeSvc = { | ||
settingsChange: new BehaviorSubject<SkyThemeSettingsChange>({ | ||
currentSettings: new SkyThemeSettings( | ||
SkyTheme.presets.default, | ||
SkyThemeMode.presets.light, | ||
), | ||
previousSettings: undefined, | ||
}), | ||
}; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [ScrollShadowFixtureComponent], | ||
providers: [ | ||
{ | ||
provide: SkyThemeService, | ||
useValue: mockThemeSvc, | ||
}, | ||
], | ||
}); | ||
|
||
fixture = TestBed.createComponent(ScrollShadowFixtureComponent); | ||
cmp = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should not show a shadow when the body is not scrollable', async () => { | ||
validateShadow(getScrollFooter()); | ||
validateShadow(getScrollHeader()); | ||
}); | ||
|
||
it('should not show a shadow when the body is scrollable', async () => { | ||
cmp.height = 800; | ||
fixture.detectChanges(); | ||
await waitForMutationObserver(); | ||
fixture.detectChanges(); | ||
|
||
validateShadow(getScrollFooter()); | ||
validateShadow(getScrollHeader()); | ||
}); | ||
}); | ||
|
||
describe('modern theme', () => { | ||
function scrollElement( | ||
element: HTMLElement | null, | ||
yDistance: number, | ||
): void { | ||
if (element) { | ||
element.scrollTop = yDistance; | ||
SkyAppTestUtility.fireDomEvent(element, 'scroll'); | ||
fixture.detectChanges(); | ||
} | ||
} | ||
|
||
const mockThemeSvc = { | ||
settingsChange: new BehaviorSubject<SkyThemeSettingsChange>({ | ||
currentSettings: new SkyThemeSettings( | ||
SkyTheme.presets.modern, | ||
SkyThemeMode.presets.light, | ||
), | ||
previousSettings: undefined, | ||
}), | ||
}; | ||
|
||
beforeEach(fakeAsync(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [ScrollShadowFixtureComponent], | ||
providers: [ | ||
{ | ||
provide: SkyThemeService, | ||
useValue: mockThemeSvc, | ||
}, | ||
], | ||
}); | ||
|
||
fixture = TestBed.createComponent(ScrollShadowFixtureComponent); | ||
cmp = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
tick(); | ||
fixture.detectChanges(); | ||
})); | ||
|
||
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.