mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 23:03:32 +00:00
[PM-19923] Update autofocus directive to be more aggressive in focusing (#14368)
* Update autofocus directive to be more aggressive in focusing * Handle checkVisibility not existing (safari < 17.4) * Tweak phrasing * Change approach
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, ElementRef, Input, NgZone, OnInit, Optional } from "@angular/core";
|
import { AfterContentChecked, Directive, ElementRef, Input, NgZone, Optional } from "@angular/core";
|
||||||
import { take } from "rxjs/operators";
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@@ -12,40 +12,72 @@ import { FocusableElement } from "../shared/focusable-element";
|
|||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
*
|
*
|
||||||
|
* Will focus the element once, when it becomes visible.
|
||||||
|
*
|
||||||
* If the component provides the `FocusableElement` interface, the `focus`
|
* If the component provides the `FocusableElement` interface, the `focus`
|
||||||
* method will be called. Otherwise, the native element will be focused.
|
* method will be called. Otherwise, the native element will be focused.
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[appAutofocus], [bitAutofocus]",
|
selector: "[appAutofocus], [bitAutofocus]",
|
||||||
})
|
})
|
||||||
export class AutofocusDirective implements OnInit {
|
export class AutofocusDirective implements AfterContentChecked {
|
||||||
@Input() set appAutofocus(condition: boolean | string) {
|
@Input() set appAutofocus(condition: boolean | string) {
|
||||||
this.autofocus = condition === "" || condition === true;
|
this.autofocus = condition === "" || condition === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private autofocus: boolean;
|
private autofocus: boolean;
|
||||||
|
|
||||||
|
// Track if we have already focused the element.
|
||||||
|
private focused = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private el: ElementRef,
|
private el: ElementRef,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
@Optional() private focusableElement: FocusableElement,
|
@Optional() private focusableElement: FocusableElement,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
/**
|
||||||
if (!Utils.isMobileBrowser && this.autofocus) {
|
* Using AfterContentChecked is a hack to ensure we only focus once. This is because
|
||||||
|
* the element may not be in the DOM, or not be focusable when the directive is
|
||||||
|
* created, and we want to wait until it is.
|
||||||
|
*
|
||||||
|
* Note: This might break in the future since it relies on Angular change detection
|
||||||
|
* to trigger after the element becomes visible.
|
||||||
|
*/
|
||||||
|
ngAfterContentChecked() {
|
||||||
|
// We only want to focus the element on initial render and it's not a mobile browser
|
||||||
|
if (this.focused || !this.autofocus || Utils.isMobileBrowser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = this.getElement();
|
||||||
|
if (el == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.ngZone.isStable) {
|
if (this.ngZone.isStable) {
|
||||||
this.focus();
|
this.focus();
|
||||||
} else {
|
} else {
|
||||||
this.ngZone.onStable.pipe(take(1)).subscribe(this.focus.bind(this));
|
this.ngZone.onStable.pipe(take(1)).subscribe(this.focus.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to focus the element. If successful we set focused to true to prevent further focus
|
||||||
|
* attempts.
|
||||||
|
*/
|
||||||
|
private focus() {
|
||||||
|
const el = this.getElement();
|
||||||
|
|
||||||
|
el.focus();
|
||||||
|
this.focused = el === document.activeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private focus() {
|
private getElement() {
|
||||||
if (this.focusableElement) {
|
if (this.focusableElement) {
|
||||||
this.focusableElement.getFocusTarget().focus();
|
return this.focusableElement.getFocusTarget();
|
||||||
} else {
|
}
|
||||||
this.el.nativeElement.focus();
|
|
||||||
}
|
return this.el.nativeElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||||||
@Input() autocomplete: string;
|
@Input() autocomplete: string;
|
||||||
|
|
||||||
getFocusTarget() {
|
getFocusTarget() {
|
||||||
return this.input.nativeElement;
|
return this.input?.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(searchText: string) {
|
onChange(searchText: string) {
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
* Used by the `AutofocusDirective` and `A11yGridDirective`.
|
* Used by the `AutofocusDirective` and `A11yGridDirective`.
|
||||||
*/
|
*/
|
||||||
export abstract class FocusableElement {
|
export abstract class FocusableElement {
|
||||||
getFocusTarget: () => HTMLElement;
|
getFocusTarget: () => HTMLElement | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user