mirror of
https://github.com/google/blockly.git
synced 2026-01-08 17:40:09 +01:00
_Note: This is a roll forward of #8920 that was reverted in #8933. See Additional Information below._ ## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8918 Fixes #8919 Fixes part of #8943 Fixes part of #8771 ### Proposed Changes This updates several classes in order to make toolboxes and flyouts focusable: - `IFlyout` is now an `IFocusableTree` with corresponding implementations in `FlyoutBase`. - `IToolbox` is now an `IFocusableTree` with corresponding implementations in `Toolbox`. - `IToolboxItem` is now an `IFocusableNode` with corresponding implementations in `ToolboxItem`. - As the primary toolbox items, `ToolboxCategory` and `ToolboxSeparator` were updated to have -1 tab indexes and defined IDs to help `ToolboxItem` fulfill its contracted for `IFocusableNode.getFocusableElement`. - `FlyoutButton` is now an `IFocusableNode` (with corresponding ID generation, tab index setting, and ID matching for retrieval in `WorkspaceSvg`). Each of these two new focusable trees have specific noteworthy behaviors behaviors: - `Toolbox` will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen). - `Toolbox` will automatically synchronize its selection state with its item nodes being focused. - `FlyoutBase`, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a **new** tab stop being introduced. - `FlyoutBase` holds a workspace (for rendering blocks) and, since `WorkspaceSvg` is already set up to be a focusable tree, it's represented as a subtree to `FlyoutBase`. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience. - Both `FlyoutBase` and `WorkspaceSvg` have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the `T`/`Esc` flows supported in the keyboard navigation plugin playground. One other thing to note: `Toolbox` had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of `FocusManager`'s state guardrails). This only addresses part of #8943: it adds support for `FlyoutButton` which covers both buttons and labels. However, a longer-term solution may be to change `FlyoutItem` itself to force using an `IFocusableNode` as its element. ### Reason for Changes This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin). ### Test Coverage No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915. ### Documentation No documentation changes should be needed here. ### Additional Information This includes changes that have been pulled from #8875. This was originally merged in #8916 but was reverted in #8933 due to https://github.com/google/blockly-keyboard-experimentation/issues/481. Note that this does contain a number of differences from the original PR (namely, changes in `WorkspaceSvg` and `FlyoutButton` in order to make `FlyoutButton`s focusable). Otherwise, this has the same caveats as those noted in #8938 with regards to the experimental keyboard navigation plugin.
191 lines
5.0 KiB
TypeScript
191 lines
5.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2020 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Former goog.module ID: Blockly.IFlyout
|
|
|
|
import type {BlockSvg} from '../block_svg.js';
|
|
import type {FlyoutItem} from '../flyout_item.js';
|
|
import type {Coordinate} from '../utils/coordinate.js';
|
|
import type {Svg} from '../utils/svg.js';
|
|
import type {FlyoutDefinition} from '../utils/toolbox.js';
|
|
import type {WorkspaceSvg} from '../workspace_svg.js';
|
|
import {IFocusableTree} from './i_focusable_tree.js';
|
|
import type {IRegistrable} from './i_registrable.js';
|
|
|
|
/**
|
|
* Interface for a flyout.
|
|
*/
|
|
export interface IFlyout extends IRegistrable, IFocusableTree {
|
|
/** Whether the flyout is laid out horizontally or not. */
|
|
horizontalLayout: boolean;
|
|
|
|
/** Is RTL vs LTR. */
|
|
RTL: boolean;
|
|
|
|
/** The target workspace */
|
|
targetWorkspace: WorkspaceSvg | null;
|
|
|
|
/** Margin around the edges of the blocks in the flyout. */
|
|
readonly MARGIN: number;
|
|
|
|
/** Does the flyout automatically close when a block is created? */
|
|
autoClose: boolean;
|
|
|
|
/** Corner radius of the flyout background. */
|
|
readonly CORNER_RADIUS: number;
|
|
|
|
/**
|
|
* Creates the flyout's DOM. Only needs to be called once. The flyout can
|
|
* either exist as its own svg element or be a g element nested inside a
|
|
* separate svg element.
|
|
*
|
|
* @param tagName The type of tag to put the flyout in. This should be <svg>
|
|
* or <g>.
|
|
* @returns The flyout's SVG group.
|
|
*/
|
|
createDom(
|
|
tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>,
|
|
): SVGElement;
|
|
|
|
/**
|
|
* Initializes the flyout.
|
|
*
|
|
* @param targetWorkspace The workspace in which to create new blocks.
|
|
*/
|
|
init(targetWorkspace: WorkspaceSvg): void;
|
|
|
|
/**
|
|
* Dispose of this flyout.
|
|
* Unlink from all DOM elements to prevent memory leaks.
|
|
*/
|
|
dispose(): void;
|
|
|
|
/**
|
|
* Get the width of the flyout.
|
|
*
|
|
* @returns The width of the flyout.
|
|
*/
|
|
getWidth(): number;
|
|
|
|
/**
|
|
* Get the height of the flyout.
|
|
*
|
|
* @returns The height of the flyout.
|
|
*/
|
|
getHeight(): number;
|
|
|
|
/**
|
|
* Get the workspace inside the flyout.
|
|
*
|
|
* @returns The workspace inside the flyout.
|
|
*/
|
|
getWorkspace(): WorkspaceSvg;
|
|
|
|
/**
|
|
* Is the flyout visible?
|
|
*
|
|
* @returns True if visible.
|
|
*/
|
|
isVisible(): boolean;
|
|
|
|
/**
|
|
* Set whether the flyout is visible. A value of true does not necessarily
|
|
* mean that the flyout is shown. It could be hidden because its container is
|
|
* hidden.
|
|
*
|
|
* @param visible True if visible.
|
|
*/
|
|
setVisible(visible: boolean): void;
|
|
|
|
/**
|
|
* Set whether this flyout's container is visible.
|
|
*
|
|
* @param visible Whether the container is visible.
|
|
*/
|
|
setContainerVisible(visible: boolean): void;
|
|
|
|
/** Hide and empty the flyout. */
|
|
hide(): void;
|
|
|
|
/**
|
|
* Show and populate the flyout.
|
|
*
|
|
* @param flyoutDef Contents to display in the flyout. This is either an array
|
|
* of Nodes, a NodeList, a toolbox definition, or a string with the name
|
|
* of the dynamic category.
|
|
*/
|
|
show(flyoutDef: FlyoutDefinition | string): void;
|
|
|
|
/**
|
|
* Returns the list of flyout items currently present in the flyout.
|
|
* The `show` method parses the flyout definition into a list of actual
|
|
* flyout items. This method should return those concrete items, which
|
|
* may be used for e.g. keyboard navigation.
|
|
*
|
|
* @returns List of flyout items.
|
|
*/
|
|
getContents(): FlyoutItem[];
|
|
|
|
/**
|
|
* Create a copy of this block on the workspace.
|
|
*
|
|
* @param originalBlock The block to copy from the flyout.
|
|
* @returns The newly created block.
|
|
* @throws {Error} if something went wrong with deserialization.
|
|
*/
|
|
createBlock(originalBlock: BlockSvg): BlockSvg;
|
|
|
|
/** Reflow blocks and their mats. */
|
|
reflow(): void;
|
|
|
|
/**
|
|
* @returns True if this flyout may be scrolled with a scrollbar or by
|
|
* dragging.
|
|
*/
|
|
isScrollable(): boolean;
|
|
|
|
/**
|
|
* Calculates the x coordinate for the flyout position.
|
|
*
|
|
* @returns X coordinate.
|
|
*/
|
|
getX(): number;
|
|
|
|
/**
|
|
* Calculates the y coordinate for the flyout position.
|
|
*
|
|
* @returns Y coordinate.
|
|
*/
|
|
getY(): number;
|
|
|
|
/** Position the flyout. */
|
|
position(): void;
|
|
|
|
/**
|
|
* Determine if a drag delta is toward the workspace, based on the position
|
|
* and orientation of the flyout. This is used in determineDragIntention_ to
|
|
* determine if a new block should be created or if the flyout should scroll.
|
|
*
|
|
* @param currentDragDeltaXY How far the pointer has moved from the position
|
|
* at mouse down, in pixel units.
|
|
* @returns True if the drag is toward the workspace.
|
|
*/
|
|
isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean;
|
|
|
|
/**
|
|
* Does this flyout allow you to create a new instance of the given block?
|
|
* Used for deciding if a block can be "dragged out of" the flyout.
|
|
*
|
|
* @param block The block to copy from the flyout.
|
|
* @returns True if you can create a new instance of the block, false
|
|
* otherwise.
|
|
*/
|
|
isBlockCreatable(block: BlockSvg): boolean;
|
|
|
|
/** Scroll the flyout to the beginning of its contents. */
|
|
scrollToStart(): void;
|
|
}
|