mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'master' into develop
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './lib/inject-restore-scroll-position';
|
||||
export * from './lib/provide-scroll-position-restoration';
|
||||
export * from './lib/store-scroll-position';
|
||||
export * from './lib/scroll-top-button.component';
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture } from '@angular/core/testing';
|
||||
import { ScrollTopButtonComponent } from './scroll-top-button.component';
|
||||
|
||||
describe('ScrollTopButtonComponent (happy path)', () => {
|
||||
let fixture: ComponentFixture<ScrollTopButtonComponent>;
|
||||
let component: ScrollTopButtonComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ScrollTopButtonComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ScrollTopButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// Polyfill / Reset matchMedia für jedes Test-Setup
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query: string) => ({
|
||||
matches: false, // Default: keine reduzierte Animation
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated, aber Angular / libs könnten darauf zugreifen
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call scrollTo with smooth when prefers-reduced-motion is false', () => {
|
||||
// Arrange
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
// matchMedia default (set in beforeEach) returns matches: false
|
||||
|
||||
// Act
|
||||
component.scrollTop();
|
||||
|
||||
// Assert
|
||||
expect((targetEl as any).scrollTo).toHaveBeenCalledWith({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call scrollTo with auto when prefers-reduced-motion is true', () => {
|
||||
// Arrange
|
||||
(window.matchMedia as jest.Mock).mockImplementationOnce(
|
||||
(query: string) => ({
|
||||
matches: true, // reduzierte Bewegungen bevorzugt
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
component.scrollTop();
|
||||
|
||||
// Assert
|
||||
expect((targetEl as any).scrollTo).toHaveBeenCalledWith({
|
||||
top: 0,
|
||||
behavior: 'auto',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render button when target element scrolled down', () => {
|
||||
// Arrange
|
||||
jest.useFakeTimers();
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
targetEl.scrollTop = 150; // > 0 so truthy
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
targetEl.dispatchEvent(new Event('scroll'));
|
||||
jest.advanceTimersByTime(20); // allow debounceTime(10) to elapse
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const button = fixture.nativeElement.querySelector(
|
||||
'[data-what="scroll-top-button"]',
|
||||
);
|
||||
expect(button).not.toBeNull();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not render button when target element at top (scrollTop = 0)', () => {
|
||||
// Arrange
|
||||
jest.useFakeTimers();
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
targetEl.scrollTop = 0; // top position
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
targetEl.dispatchEvent(new Event('scroll'));
|
||||
jest.advanceTimersByTime(20);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const button = fixture.nativeElement.querySelector(
|
||||
'[data-what="scroll-top-button"]',
|
||||
);
|
||||
expect(button).toBeNull();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaSortByUpMedium } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { debounceTime, fromEvent, switchMap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'utils-scroll-top-button',
|
||||
imports: [IconButtonComponent],
|
||||
providers: [provideIcons({ isaSortByUpMedium })],
|
||||
template: `
|
||||
@if (display()) {
|
||||
<button
|
||||
uiIconButton
|
||||
aria-label="Scroll to top"
|
||||
type="button"
|
||||
color="tertiary"
|
||||
size="large"
|
||||
data-what="scroll-top-button"
|
||||
name="isaSortByUpMedium"
|
||||
(click)="scrollTop()"
|
||||
></button>
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
'[class]': '["utils-scroll-top-button"]',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ScrollTopButtonComponent {
|
||||
/** The scroll target, either `window` or a specific element. */
|
||||
target = input<Window | HTMLElement>(window);
|
||||
|
||||
/** Whether the target is an `HTMLElement`. */
|
||||
isTargetElement = computed(() => this.target() instanceof HTMLElement);
|
||||
|
||||
/** The scroll event signal. */
|
||||
scrollEvent = toSignal(
|
||||
toObservable(this.target).pipe(
|
||||
switchMap((target) => fromEvent(target, 'scroll').pipe(debounceTime(16))),
|
||||
),
|
||||
);
|
||||
|
||||
/** Whether to display the button. */
|
||||
display = computed(() => {
|
||||
this.scrollEvent();
|
||||
const target = this.target();
|
||||
|
||||
if (target instanceof HTMLElement) {
|
||||
return target.scrollTop;
|
||||
}
|
||||
|
||||
return target.scrollY;
|
||||
});
|
||||
|
||||
/** Scrolls to the top of the page. */
|
||||
scrollTop() {
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
).matches; // Anforderung im Ticket
|
||||
|
||||
this.target().scrollTo({
|
||||
top: 0,
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user