UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

207 lines (203 loc) 7.94 kB
import * as i0 from '@angular/core'; import { input, signal, effect, Directive } from '@angular/core'; /** * Directive that automatically focuses an element when it becomes visible. * * This directive uses multiple strategies to detect when an element becomes visible: * - IntersectionObserver to detect when the element enters the viewport * - MutationObserver to detect DOM changes that might affect visibility * - Polling at regular intervals as a fallback mechanism * * The directive works reliably with modals, dialogs, accordions, and other UI components * that dynamically show and hide content. * * @example * ```html * <!-- Basic usage --> * <input [stAutofocus]="true" /> * * <!-- Conditional autofocus --> * <input [stAutofocus]="shouldFocus" /> * * <!-- In a form inside a modal --> * <st-modal> * <input [stAutofocus]="true" /> * </st-modal> * ``` * * @usageNotes * The directive takes a boolean input that determines whether autofocus should be applied. * When this value is true, the directive will attempt to focus the element once it becomes visible, * and will continue to monitor the element's visibility state in case it changes. * * This is particularly useful for: * - Elements in modals that need focus when opened * - Elements in accordions/collapsible sections that should receive focus when expanded * - Elements that are conditionally rendered and need focus when they appear * * @publicApi */ class AutofocusDirective { elementRef; /** * Controls whether autofocus should be applied to the element. * Set to true to enable autofocus. */ focus = input(false); /** Tracks the current visibility state of the element */ isVisible = signal(false); /** Forces effect to run when visibility changes, even to the same value */ changeTimestamp = signal(0); /** Collection of observers for cleanup */ observers = []; /** Interval used for polling visibility */ pollingInterval = null; /** Flag to prevent concurrent visibility checks */ checkingVisibility = false; /** Timeout reference for focus operation */ focusTimeout = null; constructor(elementRef) { this.elementRef = elementRef; // Effect that triggers focus when element becomes visible effect(() => { const visible = this.isVisible(); const timestamp = this.changeTimestamp(); if (this.focus() && visible) { // Clear any existing timeout first if (this.focusTimeout !== null) { clearTimeout(this.focusTimeout); } // Store the timeout reference this.focusTimeout = window.setTimeout(() => { this.elementRef.nativeElement.focus(); }, 50); } }); } /** * Sets up visibility observers after the view is initialized */ ngAfterViewInit() { if (this.focus()) { // Set up both observer types this.setupIntersectionObserver(); this.setupMutationObserver(); // Lightweight polling (every 500ms) as fallback for edge cases this.pollingInterval = window.setInterval(() => { this.checkVisibility(true); }, 500); // Initial visibility check this.checkVisibility(true); } } /** * Cleans up all observers and timers to prevent memory leaks */ ngOnDestroy() { // Cleanup all observers this.observers.forEach((observer) => { if (observer) { observer.disconnect(); } }); this.observers = []; // Clear polling interval if (this.pollingInterval !== null) { clearInterval(this.pollingInterval); } // Clear focus timeout if (this.focusTimeout !== null) { clearTimeout(this.focusTimeout); this.focusTimeout = null; } } /** * Sets up the IntersectionObserver to detect when the element enters or leaves the viewport * @private */ setupIntersectionObserver() { if ('IntersectionObserver' in window) { const intersectionObserver = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.checkVisibility(true); } else { this.updateVisibility(false); } }, { threshold: 0.1 }); intersectionObserver.observe(this.elementRef.nativeElement); this.observers.push(intersectionObserver); } } /** * Sets up MutationObserver to detect style/class changes that might affect visibility * Also monitors parent elements to detect when containers like modals appear/disappear * @private */ setupMutationObserver() { const mutationObserver = new MutationObserver(() => { this.checkVisibility(true); }); // Watch this element mutationObserver.observe(this.elementRef.nativeElement, { attributes: true, attributeFilter: ['style', 'class'], }); // Watch parent elements - important for modals, accordions, etc. let parent = this.elementRef.nativeElement.parentElement; let level = 0; // Watch up to 3 parent levels while (parent && level < 3) { mutationObserver.observe(parent, { attributes: true, attributeFilter: ['style', 'class'], childList: true, }); parent = parent.parentElement; level++; } this.observers.push(mutationObserver); } /** * Checks if the element is truly visible by examining computed style and dimensions * @param force Whether to force a check even if one is already in progress * @private */ checkVisibility(force = false) { if (this.checkingVisibility && !force) return; this.checkingVisibility = true; const el = this.elementRef.nativeElement; requestAnimationFrame(() => { const style = window.getComputedStyle(el); const isNowVisible = el.offsetWidth > 0 && el.offsetHeight > 0 && style.visibility !== 'hidden' && style.display !== 'none'; this.updateVisibility(isNowVisible); this.checkingVisibility = false; }); } /** * Updates the visibility signal and triggers the effect by updating the timestamp * @param visible The new visibility state * @private */ updateVisibility(visible) { if (this.isVisible() !== visible) { this.isVisible.set(visible); // Always update timestamp to force the effect to run this.changeTimestamp.set(Date.now()); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AutofocusDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.0", type: AutofocusDirective, isStandalone: true, selector: "[stAutofocus]", inputs: { focus: { classPropertyName: "focus", publicName: "focus", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AutofocusDirective, decorators: [{ type: Directive, args: [{ selector: '[stAutofocus]', }] }], ctorParameters: () => [{ type: i0.ElementRef }] }); /** * Generated bundle index. Do not edit. */ export { AutofocusDirective }; //# sourceMappingURL=sixbell-telco-sdk-directives-auto-focus.mjs.map