mirror of
https://github.com/google/blockly.git
synced 2026-05-01 17:40:11 +02:00
fix: Improve focus handling when clicking outside injection div (#9749)
* fix: Improve focus handling when clicking outside injection div * chore: Use 'popover' in place of 'quasimodal' * chore: Clarify docs
This commit is contained in:
@@ -86,6 +86,11 @@ export function getMainWorkspace(): Workspace {
|
||||
*/
|
||||
export function setMainWorkspace(workspace: Workspace) {
|
||||
mainWorkspace = workspace;
|
||||
if (workspace.rendered) {
|
||||
getFocusManager().setPopoverFocusRoot(
|
||||
(workspace as WorkspaceSvg).getInjectionDiv(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,6 +151,19 @@ export function createDom() {
|
||||
'transform ' + ANIMATION_TIME + 's, ' + 'opacity ' + ANIMATION_TIME + 's';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals with the root element that contains this and other popovers losing
|
||||
* focus by returning ephemeral focus if we hold it and hiding the DropDownDiv.
|
||||
*/
|
||||
function handleFocusLoss() {
|
||||
if (returnEphemeralFocus) {
|
||||
returnEphemeralFocus(false);
|
||||
returnEphemeralFocus = null;
|
||||
}
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an element to maintain bounds within. Drop-downs will appear
|
||||
* within the box of this element if possible.
|
||||
@@ -370,6 +383,8 @@ export function show<T>(
|
||||
manageEphemeralFocus: boolean,
|
||||
opt_onHide?: () => void,
|
||||
): boolean {
|
||||
getFocusManager().registerPopoverFocusLossHandler(handleFocusLoss);
|
||||
|
||||
const parentDiv = common.getParentContainer();
|
||||
parentDiv?.appendChild(div);
|
||||
|
||||
@@ -669,6 +684,7 @@ export function hideIfOwner<T>(
|
||||
|
||||
/** Hide the menu, triggering animation. */
|
||||
export function hide() {
|
||||
getFocusManager().unregisterPopoverFocusLossHandler(handleFocusLoss);
|
||||
// Start the animation by setting the translation and fading out.
|
||||
// Reset to (initialX, initialY) - i.e., no translation.
|
||||
div.style.transform = 'translate(0, 0)';
|
||||
|
||||
@@ -11,11 +11,15 @@ import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js';
|
||||
|
||||
/**
|
||||
* Type declaration for returning focus to FocusManager upon completing an
|
||||
* ephemeral UI flow (such as a dialog).
|
||||
* ephemeral UI flow (such as a dialog). Normally, the FocusManager will refocus
|
||||
* the previously-focused element. If callers do not wish for the FocusManager
|
||||
* to do so, they may call this method with `restoreFocus` set to false to
|
||||
* prevent automatic refocusing and leave focus where it is.
|
||||
*
|
||||
*
|
||||
* See FocusManager.takeEphemeralFocus for more details.
|
||||
*/
|
||||
export type ReturnEphemeralFocus = () => void;
|
||||
export type ReturnEphemeralFocus = (restoreFocus?: boolean) => void;
|
||||
|
||||
/**
|
||||
* Represents an IFocusableTree that has been registered for focus management in
|
||||
@@ -83,6 +87,33 @@ export class FocusManager {
|
||||
private recentlyLostAllFocus: boolean = false;
|
||||
private isUpdatingFocusedNode: boolean = false;
|
||||
|
||||
/**
|
||||
* Root element in which popovers (WidgetDiv, DropDownDiv) currently live.
|
||||
*/
|
||||
private popoverFocusRoot?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Set of callbacks to invoke if the popover focus root loses focus.
|
||||
*/
|
||||
private popoverFocusLossHandlers: Set<() => void> = new Set();
|
||||
|
||||
/**
|
||||
* Handler for focusout in the popover focus root that selectively
|
||||
* invokes the popover focus loss handlers if focus has truly transitioned
|
||||
* outside of the focus root, and not e.g. to a different popover.
|
||||
*/
|
||||
private popoverFocusOutHandler = (e: FocusEvent) => {
|
||||
const target = e.relatedTarget;
|
||||
if (
|
||||
target === null ||
|
||||
(target instanceof Node && !this.popoverFocusRoot?.contains(target))
|
||||
) {
|
||||
for (const handler of this.popoverFocusLossHandlers) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
addGlobalEventListener: (type: string, listener: EventListener) => void,
|
||||
) {
|
||||
@@ -446,7 +477,7 @@ export class FocusManager {
|
||||
focusableElement.focus({preventScroll: true});
|
||||
|
||||
let hasFinishedEphemeralFocus = false;
|
||||
return () => {
|
||||
return (restoreFocus = true) => {
|
||||
if (hasFinishedEphemeralFocus) {
|
||||
throw Error(
|
||||
`Attempted to finish ephemeral focus twice for element: ` +
|
||||
@@ -455,8 +486,7 @@ export class FocusManager {
|
||||
}
|
||||
hasFinishedEphemeralFocus = true;
|
||||
this.currentlyHoldsEphemeralFocus = false;
|
||||
|
||||
if (this.focusedNode) {
|
||||
if (this.focusedNode && restoreFocus) {
|
||||
this.activelyFocusNode(this.focusedNode, null);
|
||||
|
||||
// Even though focus was restored, check if it's lost again. It's
|
||||
@@ -667,6 +697,50 @@ export class FocusManager {
|
||||
}
|
||||
return FocusManager.focusManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current popover focus root. Generally this is active
|
||||
* workspace's injection div or the explicitly specified parent container for
|
||||
* the WidgetDiv, DropDownDiv, etc.
|
||||
*
|
||||
* @internal
|
||||
* @param newRoot The new element that contains all popovers.
|
||||
*/
|
||||
setPopoverFocusRoot(newRoot: HTMLElement) {
|
||||
this.popoverFocusRoot?.removeEventListener(
|
||||
'focusout',
|
||||
this.popoverFocusOutHandler,
|
||||
);
|
||||
this.popoverFocusRoot = newRoot;
|
||||
this.popoverFocusRoot.addEventListener(
|
||||
'focusout',
|
||||
this.popoverFocusOutHandler,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked if the popover focus root loses
|
||||
* focus. This should only be called by popovers that need to react to
|
||||
* focus changes by e.g. hiding themselves and resigning ephemeral focus.
|
||||
*
|
||||
* @internal
|
||||
* @param handler A callback function.
|
||||
*/
|
||||
registerPopoverFocusLossHandler(handler: () => void) {
|
||||
this.popoverFocusLossHandlers.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a previously-registered popover focus loss handler. This
|
||||
* should only be invoked by popovers when they no longer need to be
|
||||
* notified of focus loss, typically when they are hidden.
|
||||
*
|
||||
* @internal
|
||||
* @param handler A previously-registered callback function.
|
||||
*/
|
||||
unregisterPopoverFocusLossHandler(handler: () => void) {
|
||||
this.popoverFocusLossHandlers.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience function for FocusManager.getFocusManager. */
|
||||
|
||||
@@ -904,7 +904,8 @@ export function registerPerformAction() {
|
||||
preconditionFn: (workspace) =>
|
||||
!workspace.isDragging() &&
|
||||
!dropDownDiv.isVisible() &&
|
||||
!widgetDiv.isVisible(),
|
||||
!widgetDiv.isVisible() &&
|
||||
!getFocusManager().ephemeralFocusTaken(),
|
||||
callback: (_workspace, e) => {
|
||||
keyboardNavigationController.setIsActive(true);
|
||||
const focusedNode = getFocusManager().getFocusedNode();
|
||||
|
||||
@@ -61,6 +61,19 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals with the root element that contains this and other popovers losing
|
||||
* focus by returning ephemeral focus if we hold it and hiding the WidgetDiv.
|
||||
*/
|
||||
function handleFocusLoss() {
|
||||
if (returnEphemeralFocus) {
|
||||
returnEphemeralFocus(false);
|
||||
returnEphemeralFocus = null;
|
||||
}
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the widget div and inject it onto the page.
|
||||
*/
|
||||
@@ -137,6 +150,7 @@ export function show(
|
||||
if (manageEphemeralFocus) {
|
||||
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
|
||||
}
|
||||
getFocusManager().registerPopoverFocusLossHandler(handleFocusLoss);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +164,7 @@ export function hide() {
|
||||
|
||||
const div = containerDiv;
|
||||
if (!div) return;
|
||||
getFocusManager().unregisterPopoverFocusLossHandler(handleFocusLoss);
|
||||
div.style.display = 'none';
|
||||
div.style.left = '';
|
||||
div.style.top = '';
|
||||
|
||||
Reference in New Issue
Block a user