Skip to content

Commit

Permalink
fix: scrolling to basket validation messages (#1615)
Browse files Browse the repository at this point in the history
* the scroll behavior of the basket validation results component has been simplified
* the scroll duration has been set to 0 because in case of a delay the scroll target element might not be available any more if validation results lead to (automatical) route changes
  • Loading branch information
SGrueber authored and shauke committed Mar 28, 2024
1 parent a4646fe commit 25c2c3f
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 40 deletions.
21 changes: 12 additions & 9 deletions src/app/core/directives/scroll.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export class ScrollDirective implements OnChanges {

/**
* Sets the scroll container
* This values uses the elements parent node by default.
* The scroll container must have a scroll bar.
*
* @usageNotes
* Set it to "root" to use the window, "parent" to use the elements parent (default)
* Set it to "root" to use the window documentElement (default), "parent" to use the element's parent,
* or pass in any HTMLElement that is a parent to the element.
*/
@Input() scrollContainer: 'parent' | 'root' | HTMLElement = 'root';
Expand All @@ -60,7 +60,7 @@ export class ScrollDirective implements OnChanges {

private scroll() {
const target: HTMLElement = this.el.nativeElement;
const scrollContainer =
const container =
this.scrollContainer === 'parent'
? target.parentElement
: this.scrollContainer === 'root'
Expand All @@ -75,21 +75,24 @@ export class ScrollDirective implements OnChanges {
// calculate the offset from target to scrollContainer
let offset = target.offsetTop;
let tempTarget = target.offsetParent as HTMLElement;
while (!tempTarget.isSameNode(scrollContainer) && !tempTarget.isSameNode(this.document.body)) {
while (!tempTarget.isSameNode(container) && !tempTarget.isSameNode(this.document.body)) {
offset += tempTarget.offsetTop;
tempTarget = tempTarget.offsetParent as HTMLElement;
}
offset -= this.scrollSpacing;
if (offset < 0) {
offset = 0;
}

// scroll instantly if no duration is set
if (!this.scrollDuration) {
scrollContainer.scrollTop = offset;
container.scrollTop = offset;
return;
}

// set values for animation
let startTime: number;
const initialScrollPosition = scrollContainer.scrollTop;
const initialScrollPosition = container.scrollTop;
const scrollDifference = offset - initialScrollPosition;

const step = (timestamp: number) => {
Expand All @@ -102,8 +105,8 @@ export class ScrollDirective implements OnChanges {
const passed = timestamp - startTime;

// exit animation when duration is reached or calculated difference is smaller than 0.5 pixel
if (passed >= this.scrollDuration || Math.abs(scrollContainer.scrollTop - offset) <= 0.5) {
scrollContainer.scrollTop = offset;
if (passed >= this.scrollDuration || Math.abs(container.scrollTop - offset) <= 0.5) {
container.scrollTop = offset;
return;
}

Expand All @@ -114,7 +117,7 @@ export class ScrollDirective implements OnChanges {
const ease = (x: number) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);

// set new scroll value based on current time progression
scrollContainer.scrollTop = initialScrollPosition + scrollDifference * ease(progress);
container.scrollTop = initialScrollPosition + scrollDifference * ease(progress);

// request next animation frame
requestAnimationFrame(step);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
<!-- target element to scroll to the messages -->
<div [ishScroll]="scrollToMessage$ | async" [scrollSpacing]="scrollSpacing"></div>
<!-- error messages -->
<div
*ngFor="let message of errorMessages$ | async; let first = first"
*ngFor="let message of errorMessages$ | async"
class="alert alert-danger"
role="alert"
[ishScroll]="first"
[scrollDuration]="scrollDuration"
[scrollSpacing]="scrollSpacing"
data-testing-id="validation-error-message"
>
{{ message }}
</div>

<!-- info messages -->
<div
*ngFor="let message of infoMessages$ | async; let first = first"
*ngFor="let message of infoMessages$ | async"
class="alert alert-info"
role="alert"
[ishScroll]="first"
[scrollDuration]="scrollDuration"
[scrollSpacing]="scrollSpacing"
data-testing-id="validation-info-message"
>
{{ message }}
</div>

<div
*ngIf="hasGeneralBasketError$ | async"
class="alert alert-danger"
[ishScroll]="true"
[scrollDuration]="scrollDuration"
[scrollSpacing]="scrollSpacing"
data-testing-id="general-validation-message"
>
<div *ngIf="hasGeneralBasketError$ | async" class="alert alert-danger" data-testing-id="general-validation-message">
{{ 'basket.validation.general.error' | translate }}
</div>

Expand All @@ -41,9 +30,6 @@
*ngIf="undeliverableItems.length"
class="alert alert-box"
role="alert"
[ishScroll]="true"
[scrollDuration]="scrollDuration"
[scrollSpacing]="scrollSpacing"
data-testing-id="undeliverable-items-message"
>
<div class="alert-box-header">
Expand All @@ -62,14 +48,7 @@

<!-- removed items messages -->
<ng-container *ngIf="removedItems$ | async as removedItems">
<div
*ngIf="removedItems.length"
class="alert alert-danger"
[ishScroll]="true"
[scrollDuration]="scrollDuration"
[scrollSpacing]="scrollSpacing"
data-testing-id="validation-removed-message"
>
<div *ngIf="removedItems.length" class="alert alert-danger" data-testing-id="validation-removed-message">
{{ 'shopping_cart.adjusted_items.warnung' | translate }}
</div>
<ish-basket-validation-products [items]="removedItems" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, OnInit, O
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { uniq } from 'lodash-es';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, shareReplay } from 'rxjs/operators';

import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { BasketFeedback } from 'ish-core/models/basket-feedback/basket-feedback.model';
Expand All @@ -29,11 +29,11 @@ export class BasketValidationResultsComponent implements OnInit {
infoMessages$: Observable<string[]>;
undeliverableItems$: Observable<LineItemView[]>;
removedItems$: Observable<{ message: string; productSKU: string; price: PriceItem }[]>;
scrollToMessage$: Observable<boolean>;

itemHasBeenRemoved = false;

// default values to control scrolling behavior
scrollDuration = 500;
scrollSpacing = 64;

private destroyRef = inject(DestroyRef);
Expand All @@ -43,9 +43,8 @@ export class BasketValidationResultsComponent implements OnInit {
@Output() continueCheckout = new EventEmitter<void>();

ngOnInit() {
this.validationResults$ = this.checkoutFacade.basketValidationResults$;

// update emitted to display spinning animation
this.validationResults$ = this.checkoutFacade.basketValidationResults$.pipe(shareReplay(1));
this.validationResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
if (this.itemHasBeenRemoved) {
this.continueCheckout.emit();
Expand Down Expand Up @@ -99,6 +98,10 @@ export class BasketValidationResultsComponent implements OnInit {
this.infoMessages$ = this.validationResults$.pipe(
map(results => uniq(results?.infos?.map(info => info.message)).filter(message => !!message))
);

this.scrollToMessage$ = this.validationResults$.pipe(
map(results => !!(results?.errors?.length || results?.infos?.length))
);
}

isLineItemMessage(error: BasketFeedback): boolean {
Expand Down

0 comments on commit 25c2c3f

Please sign in to comment.