@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
207 lines (203 loc) • 7.94 kB
JavaScript
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