Merged PR 2066: fix(ui-layout, input-controls-dropdown, oms-return-details): prevent stale sc...

fix(ui-layout, input-controls-dropdown, oms-return-details): prevent stale scroll events from closing dropdown on open

Delay scroll listener registration using requestAnimationFrame when
activating CloseOnScrollDirective. This prevents stale scroll events
still in the event queue from immediately triggering closeOnScroll
when opening the dropdown after scrolling.

Also adds conditional rendering for product format and publication date
in return-details-order-group-item component.

Refs: #5513
This commit is contained in:
Nino Righi
2025-12-02 14:02:32 +00:00
committed by Lorenz Hilpert
parent ee9f030a99
commit aee63711e4
3 changed files with 45 additions and 46 deletions

View File

@@ -44,21 +44,25 @@
</div>
<div class="text-isa-neutral-900 flex flex-col gap-2" uiItemRowProdcutInfo>
<div class="flex flex-row gap-2 items-center justify-start">
<ng-icon [name]="i.product.format | lowercase"></ng-icon>
@if (!!i?.product?.format) {
<ng-icon [name]="i.product.format | lowercase"></ng-icon>
}
<span class="isa-text-body-2-bold truncate">
{{ i.product.formatDetail }}
{{ i?.product?.formatDetail }}
</span>
</div>
<div
class="text-isa-neutral-600 isa-text-body-2-regular"
data-what="product-info"
[attr.data-which]="i.product.ean"
[attr.data-which]="i?.product?.ean"
>
{{ i.product.manufacturer }} | {{ i.product.ean }}
</div>
<div class="text-isa-neutral-600 isa-text-body-2-regular">
{{ i.product.publicationDate | date: "dd. MMM yyyy" }}
{{ i?.product?.manufacturer }} | {{ i?.product?.ean }}
</div>
@if (!!i?.product?.publicationDate) {
<div class="text-isa-neutral-600 isa-text-body-2-regular">
{{ i.product.publicationDate | date: 'dd. MMM yyyy' }}
</div>
}
</div>
<oms-feature-return-details-order-group-item-controls [item]="i">
</oms-feature-return-details-order-group-item-controls>

View File

@@ -124,8 +124,8 @@ export class DropdownOptionComponent<T> implements Highlightable {
'(keydown.enter)': 'select(keyManger.activeItem); close()',
'(keydown.escape)': 'close()',
'(click)':
'disabled() ? $event.stopImmediatePropagation() : (isOpen() ? close() : open())',
'(closeOnScroll)': 'close()',
'disabled() ? $event.stopPropagation() : (isOpen() ? close() : open())',
'(closeOnScroll)': '(isOpen() ? close() : "")',
},
})
export class DropdownButtonComponent<T>

View File

@@ -47,6 +47,7 @@ export class CloseOnScrollDirective implements OnDestroy {
#document = inject(DOCUMENT);
#scrollListener?: (event: Event) => void;
#isActive = false;
#pendingActivation?: number;
/**
* When true, the directive listens for scroll events.
@@ -81,50 +82,44 @@ export class CloseOnScrollDirective implements OnDestroy {
if (this.#isActive) {
return;
}
this.#scrollListener = (event: Event) => {
const target = event.target as HTMLElement;
const excludeElement = this.closeOnScrollExclude();
// Check if scroll happened within the excluded element
if (excludeElement?.contains(target)) {
return;
}
// Emit close event - scroll happened outside excluded element
this.#logger.debug('Scroll detected outside panel, emitting close');
this.closeOnScroll.emit();
};
// Use capture: true to catch scroll events from ALL elements (scroll events don't bubble)
// Use passive: true for better performance (we don't call preventDefault)
this.#document.defaultView?.addEventListener(
'scroll',
this.#scrollListener,
{
capture: true,
passive: true,
},
);
this.#isActive = true;
this.#logger.debug('Activated scroll listener');
// Delay listener registration to next frame to skip any stale scroll events
this.#pendingActivation = requestAnimationFrame(() => {
this.#scrollListener = (event: Event) => {
const excludeElement = this.closeOnScrollExclude();
if (excludeElement?.contains(event.target as HTMLElement)) {
return;
}
this.#logger.debug('Scroll detected outside panel, emitting close');
this.closeOnScroll.emit();
};
this.#document.defaultView?.addEventListener(
'scroll',
this.#scrollListener,
{ capture: true, passive: true },
);
this.#logger.debug('Activated scroll listener');
});
}
#deactivate(): void {
if (!this.#isActive || !this.#scrollListener) {
return;
if (this.#pendingActivation) {
cancelAnimationFrame(this.#pendingActivation);
this.#pendingActivation = undefined;
}
this.#document.defaultView?.removeEventListener(
'scroll',
this.#scrollListener,
{ capture: true },
);
this.#scrollListener = undefined;
this.#isActive = false;
if (this.#scrollListener) {
this.#document.defaultView?.removeEventListener(
'scroll',
this.#scrollListener,
{ capture: true },
);
this.#scrollListener = undefined;
}
this.#logger.debug('Deactivated scroll listener');
this.#isActive = false;
}
ngOnDestroy(): void {