Skip to content

Instantly share code, notes, and snippets.

@bojidaryovchev
Created February 3, 2023 11:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bojidaryovchev/a328b116460b0b87e9022938cba1ebfb to your computer and use it in GitHub Desktop.
Save bojidaryovchev/a328b116460b0b87e9022938cba1ebfb to your computer and use it in GitHub Desktop.
Inspired by cdkTrapFocus, made able to work everywhere
import {
AfterContentInit,
ContentChildren,
Directive,
ElementRef,
HostListener,
Input,
OnDestroy,
OnInit,
QueryList,
} from '@angular/core';
import { combineLatest, startWith, Subject, takeUntil } from 'rxjs';
@Directive({
selector: '[focusable]',
host: {
'[attr.tabindex]': '0',
},
})
export class FocusableDirective implements OnInit, OnDestroy {
@Input() initialFocus?: boolean;
readonly focus$ = new Subject<FocusEvent>();
constructor(public _elementRef: ElementRef<HTMLElement>) {}
ngOnInit(): void {
if (!this.initialFocus) {
return;
}
setTimeout(() => {
this._elementRef.nativeElement.focus();
});
}
ngOnDestroy(): void {
this.focus$.complete();
}
@HostListener('focus', ['$event'])
onFocus(event: FocusEvent): void {
this.focus$.next(event);
}
}
@Directive({
selector: '[trapFocus]',
})
export class TrapFocusDirective implements AfterContentInit, OnDestroy {
@ContentChildren(FocusableDirective, { descendants: true }) focusables?: QueryList<FocusableDirective>;
private readonly terminator$ = new Subject<void>();
private lastEvent?: FocusEvent;
ngAfterContentInit(): void {
if (!this.focusables) {
return;
}
const focusSubjects = this.focusables.map((focusable) => focusable.focus$.pipe(startWith(undefined)));
combineLatest(focusSubjects)
.pipe(takeUntil(this.terminator$))
.subscribe((focusEvents) => {
const orderedEvents = focusEvents.filter(Boolean).sort((a, b) => b!.timeStamp - a!.timeStamp);
this.lastEvent = orderedEvents[0];
});
}
ngOnDestroy(): void {
this.terminator$.next();
this.terminator$.complete();
}
@HostListener('keydown.tab', ['$event'])
onHostKeydownTab(event: KeyboardEvent): void {
event.stopPropagation();
if (!this.lastEvent || !this.focusables) {
return;
}
const lastEventTargetIndex = this.focusables
.toArray()
.map((focusable) => focusable._elementRef.nativeElement)
.indexOf(this.lastEvent.target as HTMLElement);
if (lastEventTargetIndex === this.focusables.length - 1) {
event.preventDefault();
this.focusables.first._elementRef.nativeElement.focus();
}
}
@HostListener('keydown.shift.tab', ['$event'])
onHostKeydownShiftTab(event: KeyboardEvent): void {
event.stopPropagation();
if (!this.lastEvent || !this.focusables) {
return;
}
const lastEventTargetIndex = this.focusables
.toArray()
.map((focusable) => focusable._elementRef.nativeElement)
.indexOf(this.lastEvent.target as HTMLElement);
if (lastEventTargetIndex === 0) {
event.preventDefault();
this.focusables.last._elementRef.nativeElement.focus();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment