Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/blockly/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export function getMainWorkspace(): Workspace {
*/
export function setMainWorkspace(workspace: Workspace) {
mainWorkspace = workspace;
if (workspace.rendered) {
getFocusManager().setPopoverFocusRoot(
(workspace as WorkspaceSvg).getInjectionDiv(),
);
}
}

/**
Expand Down
16 changes: 16 additions & 0 deletions packages/blockly/core/dropdowndiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -370,6 +383,8 @@ export function show<T>(
manageEphemeralFocus: boolean,
opt_onHide?: () => void,
): boolean {
getFocusManager().registerPopoverFocusLossHandler(handleFocusLoss);

const parentDiv = common.getParentContainer();
parentDiv?.appendChild(div);

Expand Down Expand Up @@ -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)';
Expand Down
84 changes: 79 additions & 5 deletions packages/blockly/core/focus_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
gonfunko marked this conversation as resolved.

/**
* Represents an IFocusableTree that has been registered for focus management in
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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: ` +
Expand All @@ -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
Expand Down Expand Up @@ -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. */
Expand Down
3 changes: 2 additions & 1 deletion packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
15 changes: 15 additions & 0 deletions packages/blockly/core/widgetdiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -137,6 +150,7 @@ export function show(
if (manageEphemeralFocus) {
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
}
getFocusManager().registerPopoverFocusLossHandler(handleFocusLoss);
}

/**
Expand All @@ -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 = '';
Expand Down