@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
190 lines (186 loc) • 7.25 kB
JavaScript
import * as i0 from '@angular/core';
import { input, signal, afterRenderEffect, 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 dialog -->
* <st-dialog>
* <input [stAutofocus]="true" />
* </st-dialog>
* ```
*
* @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);
/** Keeps a reference so the afterRenderEffect can be destroyed with the directive */
focusAfterRenderRef;
/** Collection of observers for cleanup */
observers = [];
/** Interval used for polling visibility */
pollingInterval = null;
/** Flag to prevent concurrent visibility checks */
checkingVisibility = false;
constructor(elementRef) {
this.elementRef = elementRef;
this.focusAfterRenderRef = afterRenderEffect(() => {
if (this.focus() && this.isVisible()) {
this.elementRef.nativeElement.focus();
}
});
}
/**
* 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 = globalThis.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
for (const observer of this.observers) {
if (observer) {
observer.disconnect();
}
}
this.observers = [];
// Clear polling interval
if (this.pollingInterval !== null) {
clearInterval(this.pollingInterval);
}
this.focusAfterRenderRef.destroy();
}
/**
* Sets up the IntersectionObserver to detect when the element enters or leaves the viewport
* @private
*/
setupIntersectionObserver() {
if ('IntersectionObserver' in globalThis) {
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 = globalThis.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 that drives the autofocus effect
* @param visible The new visibility state
* @private
*/
updateVisibility(visible) {
if (this.isVisible() !== visible) {
this.isVisible.set(visible);
}
}
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]',
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }] });
/**
* Generated bundle index. Do not edit.
*/
export { AutofocusDirective };
//# sourceMappingURL=sixbell-telco-sdk-directives-auto-focus.mjs.map