1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00

return focus to trigger without passing element

This commit is contained in:
Bryan Cunningham
2025-12-18 10:20:34 -05:00
parent 8ac1fb58db
commit cddbe90645
6 changed files with 50 additions and 17 deletions

View File

@@ -964,18 +964,12 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
formConfig: CipherFormConfig,
activeCollectionId?: CollectionId,
) {
this.vaultItemDialogRef = VaultItemDialogComponent.open(
this.dialogService,
{
mode,
formConfig,
activeCollectionId,
restore: this.restore,
},
{
restoreFocus: this.newButtonEl(),
},
);
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
mode,
formConfig,
activeCollectionId,
restore: this.restore,
});
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
this.vaultItemDialogRef = undefined;
@@ -1145,7 +1139,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
initialTab: tab,
limitNestedCollections: true,
},
restoreFocus: this.newButtonEl(),
// Don't specify restoreFocus - let it default to capturing the currently focused element
});
const result = await lastValueFrom(dialog.closed);

View File

@@ -237,11 +237,17 @@ export class DialogService {
backdropClass: this.backDropClasses,
scrollStrategy: this.defaultScrollStrategy,
positionStrategy: config?.positionStrategy ?? new ResponsivePositionStrategy(),
restoreFocus: true,
injector,
...config,
};
ref.cdkDialogRefBase = this.dialog.open<R, D, C>(componentOrTemplateRef, _config);
// Delay dialog opening to allow menus to close and restore focus to their triggers.
// This ensures proper focus restoration when dialogs opened from menus are closed.
setTimeout(() => {
ref.cdkDialogRefBase = this.dialog.open<R, D, C>(componentOrTemplateRef, _config);
}, 0);
return ref;
}

View File

@@ -1,12 +1,13 @@
@let isDrawer = dialogRef?.isDrawer;
<section
#dialogSection
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-border tw-border-solid tw-border-secondary-100 tw-bg-background tw-text-main"
[ngClass]="[
width,
isDrawer ? 'tw-h-full tw-border-t-0' : 'tw-rounded-t-xl md:tw-rounded-xl tw-shadow-lg',
]"
cdkTrapFocus
cdkTrapFocusAutoCapture
[cdkTrapFocusAutoCapture]="true"
>
@let showHeaderBorder = bodyHasScrolledFrom().top;
<header

View File

@@ -11,6 +11,7 @@ import {
DestroyRef,
computed,
signal,
AfterViewInit,
} from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import { combineLatest, switchMap } from "rxjs";
@@ -48,10 +49,11 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai
SpinnerComponent,
],
})
export class DialogComponent {
export class DialogComponent implements AfterViewInit {
private readonly destroyRef = inject(DestroyRef);
private readonly scrollableBody = viewChild.required(CdkScrollable);
private readonly scrollBottom = viewChild.required<ElementRef<HTMLDivElement>>("scrollBottom");
private readonly dialogSection = viewChild.required<ElementRef<HTMLElement>>("dialogSection");
protected dialogRef = inject(DialogRef, { optional: true });
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
@@ -141,4 +143,16 @@ export class DialogComponent {
onAnimationEnd() {
this.animationCompleted.set(true);
}
ngAfterViewInit() {
// Use setTimeout to ensure focus is captured after Angular's change detection
// and the dialog's focus trap has been initialized
setTimeout(() => {
const section = this.dialogSection().nativeElement;
const focusableElement = section.querySelector("[cdkFocusInitial]") as HTMLElement;
if (focusableElement && document.activeElement !== focusableElement) {
focusableElement.focus();
}
}, 0);
}
}

View File

@@ -185,6 +185,12 @@ export class MenuTriggerForDirective implements OnDestroy {
this.isOpen = false;
this.disposeAll();
this.menu().closed.emit();
// Restore focus to the trigger button when the menu closes.
// This ensures that when a dialog opens from a menu item, the CDK Dialog's
// restoreFocus will capture the menu trigger button as the element to restore to.
// The dialog's ngAfterViewInit will then explicitly move focus into the dialog.
this.elementRef.nativeElement.focus();
}
private setupClosingActions(isContextMenu: boolean) {

View File

@@ -51,6 +51,7 @@ export class TooltipDirective implements OnInit {
private readonly isVisible = signal(false);
private overlayRef: OverlayRef | undefined;
private showTimeoutId: ReturnType<typeof setTimeout> | undefined;
private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private overlay = inject(Overlay);
private viewContainerRef = inject(ViewContainerRef);
@@ -82,12 +83,22 @@ export class TooltipDirective implements OnInit {
);
private destroyTooltip = () => {
// Clear any pending show timeout to prevent tooltip from appearing after hide
if (this.showTimeoutId !== undefined) {
clearTimeout(this.showTimeoutId);
this.showTimeoutId = undefined;
}
this.overlayRef?.dispose();
this.overlayRef = undefined;
this.isVisible.set(false);
};
protected showTooltip = () => {
// Clear any existing timeout before starting a new one
if (this.showTimeoutId !== undefined) {
clearTimeout(this.showTimeoutId);
}
if (!this.overlayRef) {
this.overlayRef = this.overlay.create({
...this.defaultPopoverConfig,
@@ -97,8 +108,9 @@ export class TooltipDirective implements OnInit {
this.overlayRef.attach(this.tooltipPortal);
}
setTimeout(() => {
this.showTimeoutId = setTimeout(() => {
this.isVisible.set(true);
this.showTimeoutId = undefined;
}, TOOLTIP_DELAY_MS);
};