Browse Source

feat(shared): add AutocompleteAutoScrollRepositionDirective

- New input[matAutocomplete]/textarea[matAutocomplete] directive
  that keeps an open MatAutocomplete panel attached to its anchor
  while the page scrolls or the window resizes: tracks the
  input's visibility via IntersectionObserver and re-requests
  panel position (or closes the panel when off-screen) when the
  nearest scroll container or window emits a resize/scroll event.
- Declare and export the directive from SharedModule so it lights
  up across every screen that uses mat-autocomplete.
- Drop the transparent backdrop from
  MAT_AUTOCOMPLETE_DEFAULT_OPTIONS (the directive now handles
  scroll-and-resize repositioning, so the backdrop-driven outside-
  click close is no longer needed) and keep the existing
  hideSingleSelectionIndicator default.
pull/15772/head
Igor Kulikov 4 months ago
parent
commit
cab7c51027
  1. 80
      ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts
  2. 7
      ui-ngx/src/app/shared/shared.module.ts

80
ui-ngx/src/app/shared/directives/autocomplete-auto-scroll-reposition.directive.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2026 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Directive,
ElementRef,
OnDestroy,
inject, AfterViewInit, Renderer2,
} from '@angular/core';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { Subscription } from 'rxjs';
import { onParentScrollOrWindowResize } from '@core/utils';
@Directive({
selector: 'input[matAutocomplete], textarea[matAutocomplete]',
standalone: false
})
export class AutocompleteAutoScrollRepositionDirective implements AfterViewInit, OnDestroy {
private readonly trigger = inject(MatAutocompleteTrigger, { host: true });
private readonly elementRef = inject(ElementRef<HTMLElement>);
private readonly renderer = inject(Renderer2);
private parentScrollSubscription: Subscription = null;
private isIntersecting: boolean = false;
private intersectionObserver = new IntersectionObserver((entries) => {
if (this.isIntersecting !== entries[0].isIntersecting) {
this.isIntersecting = entries[0].isIntersecting;
this.updatePanelVisibility();
}
}, {threshold: [0.5]});
constructor() {
}
ngAfterViewInit(): void {
this.parentScrollSubscription = onParentScrollOrWindowResize(this.elementRef.nativeElement).subscribe(() => {
if (this.trigger.panelOpen) {
this.trigger.updatePosition();
}
});
this.intersectionObserver.observe(this.elementRef.nativeElement);
}
ngOnDestroy(): void {
if (this.parentScrollSubscription) {
this.parentScrollSubscription.unsubscribe();
this.parentScrollSubscription = null;
}
if (this.intersectionObserver) {
this.intersectionObserver.unobserve(this.elementRef.nativeElement);
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
}
private updatePanelVisibility(): void {
if (this.trigger.panelOpen) {
if (this.isIntersecting) {
this.renderer.removeStyle(this.trigger.autocomplete.panel.nativeElement, 'display');
} else {
this.renderer.setStyle(this.trigger.autocomplete.panel.nativeElement, 'display', 'none');
}
}
}
}

7
ui-ngx/src/app/shared/shared.module.ts

@ -230,6 +230,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component';
import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle';
import { PhotoSwipeGalleryDirective } from '@shared/directives/photoswipe-gallery.directive';
import {
AutocompleteAutoScrollRepositionDirective
} from '@shared/directives/autocomplete-auto-scroll-reposition.directive';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -278,8 +281,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
{
provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS,
useValue: {
hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop',
hideSingleSelectionIndicator: true
}
},
@ -389,6 +390,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ContextMenuDirective,
ChipOverflowDirective,
PhotoSwipeGalleryDirective,
AutocompleteAutoScrollRepositionDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -655,6 +657,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ContextMenuDirective,
ChipOverflowDirective,
PhotoSwipeGalleryDirective,
AutocompleteAutoScrollRepositionDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

Loading…
Cancel
Save