@@ -11,11 +11,15 @@ import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js';
1111
1212/**
1313 * Type declaration for returning focus to FocusManager upon completing an
14- * ephemeral UI flow (such as a dialog).
14+ * ephemeral UI flow (such as a dialog). Normally, the FocusManager will refocus
15+ * the previously-focused element. If callers do not wish for the FocusManager
16+ * to do so, they may call this method with `restoreFocus` set to false to
17+ * prevent automatic refocusing and leave focus where it is.
18+ *
1519 *
1620 * See FocusManager.takeEphemeralFocus for more details.
1721 */
18- export type ReturnEphemeralFocus = ( ) => void ;
22+ export type ReturnEphemeralFocus = ( restoreFocus ?: boolean ) => void ;
1923
2024/**
2125 * Represents an IFocusableTree that has been registered for focus management in
@@ -83,6 +87,33 @@ export class FocusManager {
8387 private recentlyLostAllFocus : boolean = false ;
8488 private isUpdatingFocusedNode : boolean = false ;
8589
90+ /**
91+ * Root element in which popovers (WidgetDiv, DropDownDiv) currently live.
92+ */
93+ private popoverFocusRoot ?: HTMLElement ;
94+
95+ /**
96+ * Set of callbacks to invoke if the popover focus root loses focus.
97+ */
98+ private popoverFocusLossHandlers : Set < ( ) => void > = new Set ( ) ;
99+
100+ /**
101+ * Handler for focusout in the popover focus root that selectively
102+ * invokes the popover focus loss handlers if focus has truly transitioned
103+ * outside of the focus root, and not e.g. to a different popover.
104+ */
105+ private popoverFocusOutHandler = ( e : FocusEvent ) => {
106+ const target = e . relatedTarget ;
107+ if (
108+ target === null ||
109+ ( target instanceof Node && ! this . popoverFocusRoot ?. contains ( target ) )
110+ ) {
111+ for ( const handler of this . popoverFocusLossHandlers ) {
112+ handler ( ) ;
113+ }
114+ }
115+ } ;
116+
86117 constructor (
87118 addGlobalEventListener : ( type : string , listener : EventListener ) => void ,
88119 ) {
@@ -446,7 +477,7 @@ export class FocusManager {
446477 focusableElement . focus ( { preventScroll : true } ) ;
447478
448479 let hasFinishedEphemeralFocus = false ;
449- return ( ) => {
480+ return ( restoreFocus = true ) => {
450481 if ( hasFinishedEphemeralFocus ) {
451482 throw Error (
452483 `Attempted to finish ephemeral focus twice for element: ` +
@@ -455,8 +486,7 @@ export class FocusManager {
455486 }
456487 hasFinishedEphemeralFocus = true ;
457488 this . currentlyHoldsEphemeralFocus = false ;
458-
459- if ( this . focusedNode ) {
489+ if ( this . focusedNode && restoreFocus ) {
460490 this . activelyFocusNode ( this . focusedNode , null ) ;
461491
462492 // Even though focus was restored, check if it's lost again. It's
@@ -667,6 +697,50 @@ export class FocusManager {
667697 }
668698 return FocusManager . focusManager ;
669699 }
700+
701+ /**
702+ * Sets the current popover focus root. Generally this is active
703+ * workspace's injection div or the explicitly specified parent container for
704+ * the WidgetDiv, DropDownDiv, etc.
705+ *
706+ * @internal
707+ * @param newRoot The new element that contains all popovers.
708+ */
709+ setPopoverFocusRoot ( newRoot : HTMLElement ) {
710+ this . popoverFocusRoot ?. removeEventListener (
711+ 'focusout' ,
712+ this . popoverFocusOutHandler ,
713+ ) ;
714+ this . popoverFocusRoot = newRoot ;
715+ this . popoverFocusRoot . addEventListener (
716+ 'focusout' ,
717+ this . popoverFocusOutHandler ,
718+ ) ;
719+ }
720+
721+ /**
722+ * Registers a callback to be invoked if the popover focus root loses
723+ * focus. This should only be called by popovers that need to react to
724+ * focus changes by e.g. hiding themselves and resigning ephemeral focus.
725+ *
726+ * @internal
727+ * @param handler A callback function.
728+ */
729+ registerPopoverFocusLossHandler ( handler : ( ) => void ) {
730+ this . popoverFocusLossHandlers . add ( handler ) ;
731+ }
732+
733+ /**
734+ * Unregisters a previously-registered popover focus loss handler. This
735+ * should only be invoked by popovers when they no longer need to be
736+ * notified of focus loss, typically when they are hidden.
737+ *
738+ * @internal
739+ * @param handler A previously-registered callback function.
740+ */
741+ unregisterPopoverFocusLossHandler ( handler : ( ) => void ) {
742+ this . popoverFocusLossHandlers . delete ( handler ) ;
743+ }
670744}
671745
672746/** Convenience function for FocusManager.getFocusManager. */
0 commit comments