mirror of
https://github.com/google/blockly.git
synced 2026-03-17 10:40:10 +01:00
release: merge branch develop into rc/v12.1.0
This commit is contained in:
@@ -849,6 +849,17 @@ export class BlockSvg
|
||||
Tooltip.dispose();
|
||||
ContextMenu.hide();
|
||||
|
||||
// If this block was focused, focus its parent or workspace instead.
|
||||
const focusManager = getFocusManager();
|
||||
if (focusManager.getFocusedNode() === this) {
|
||||
const parent = this.getParent();
|
||||
if (parent) {
|
||||
focusManager.focusNode(parent);
|
||||
} else {
|
||||
setTimeout(() => focusManager.focusTree(this.workspace), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
this.unplug(healStack);
|
||||
blockAnimations.disposeUiEffect(this);
|
||||
|
||||
@@ -173,6 +173,10 @@ import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {LineCursor} from './keyboard_nav/line_cursor.js';
|
||||
import {Marker} from './keyboard_nav/marker.js';
|
||||
import {
|
||||
KeyboardNavigationController,
|
||||
keyboardNavigationController,
|
||||
} from './keyboard_navigation_controller.js';
|
||||
import type {LayerManager} from './layer_manager.js';
|
||||
import * as layers from './layers.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
@@ -580,6 +584,7 @@ export {
|
||||
ImageProperties,
|
||||
Input,
|
||||
InsertionMarkerPreviewer,
|
||||
KeyboardNavigationController,
|
||||
LabelFlyoutInflater,
|
||||
LayerManager,
|
||||
Marker,
|
||||
@@ -631,6 +636,7 @@ export {
|
||||
isSelectable,
|
||||
isSerializable,
|
||||
isVariableBackedParameterModel,
|
||||
keyboardNavigationController,
|
||||
layers,
|
||||
renderManagement,
|
||||
serialization,
|
||||
|
||||
@@ -98,8 +98,8 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
* when automatically positioning.
|
||||
* @param overriddenFocusableElement An optional replacement to the focusable
|
||||
* element that's represented by this bubble (as a focusable node). This
|
||||
* element will have its ID and tabindex overwritten. If not provided, the
|
||||
* focusable element of this node will default to the bubble's SVG root.
|
||||
* element will have its ID overwritten. If not provided, the focusable
|
||||
* element of this node will default to the bubble's SVG root.
|
||||
*/
|
||||
constructor(
|
||||
public readonly workspace: WorkspaceSvg,
|
||||
@@ -138,7 +138,6 @@ export abstract class Bubble implements IBubble, ISelectable {
|
||||
|
||||
this.focusableElement = overriddenFocusableElement ?? this.svgRoot;
|
||||
this.focusableElement.setAttribute('id', this.id);
|
||||
this.focusableElement.setAttribute('tabindex', '-1');
|
||||
|
||||
browserEvents.conditionalBind(
|
||||
this.background,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {IContextMenu} from '../interfaces/i_contextmenu.js';
|
||||
import {ICopyable} from '../interfaces/i_copyable.js';
|
||||
import {IDeletable} from '../interfaces/i_deletable.js';
|
||||
import {IDraggable} from '../interfaces/i_draggable.js';
|
||||
import {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
|
||||
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
|
||||
import {ISelectable} from '../interfaces/i_selectable.js';
|
||||
@@ -42,7 +43,8 @@ export class RenderedWorkspaceComment
|
||||
ISelectable,
|
||||
IDeletable,
|
||||
ICopyable<WorkspaceCommentCopyData>,
|
||||
IContextMenu
|
||||
IContextMenu,
|
||||
IFocusableNode
|
||||
{
|
||||
/** The class encompassing the svg elements making up the workspace comment. */
|
||||
private view: CommentView;
|
||||
@@ -63,7 +65,6 @@ export class RenderedWorkspaceComment
|
||||
this.view.setEditable(this.isEditable());
|
||||
this.view.getSvgRoot().setAttribute('data-id', this.id);
|
||||
this.view.getSvgRoot().setAttribute('id', this.id);
|
||||
this.view.getSvgRoot().setAttribute('tabindex', '-1');
|
||||
|
||||
this.addModelUpdateBindings();
|
||||
|
||||
@@ -207,7 +208,12 @@ export class RenderedWorkspaceComment
|
||||
/** Disposes of the view. */
|
||||
override dispose() {
|
||||
this.disposing = true;
|
||||
const focusManager = getFocusManager();
|
||||
if (focusManager.getFocusedNode() === this) {
|
||||
setTimeout(() => focusManager.focusTree(this.workspace), 0);
|
||||
}
|
||||
if (!this.view.isDeadOrDying()) this.view.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
import type {Block} from './block.js';
|
||||
import {BlockDefinition, Blocks} from './blocks.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import type {Connection} from './connection.js';
|
||||
import {EventType} from './events/type.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
|
||||
import {ShortcutRegistry} from './shortcut_registry.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
@@ -310,4 +312,29 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
|
||||
* is not visible.
|
||||
*
|
||||
* @internal
|
||||
* @param e Key down event.
|
||||
*/
|
||||
export function globalShortcutHandler(e: KeyboardEvent) {
|
||||
const mainWorkspace = getMainWorkspace() as WorkspaceSvg;
|
||||
if (!mainWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
browserEvents.isTargetInput(e) ||
|
||||
(mainWorkspace.rendered && !mainWorkspace.isVisible())
|
||||
) {
|
||||
// When focused on an HTML text input widget, don't trap any keys.
|
||||
// Ignore keypresses on rendered workspaces that have been explicitly
|
||||
// hidden.
|
||||
return;
|
||||
}
|
||||
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
|
||||
}
|
||||
|
||||
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};
|
||||
|
||||
@@ -505,6 +505,6 @@ input[type=number] {
|
||||
.blocklyIconGroup,
|
||||
.blocklyTextarea
|
||||
) {
|
||||
outline-width: 0px;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// Former goog.module ID: Blockly.dropDownDiv
|
||||
|
||||
import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as common from './common.js';
|
||||
import type {Field} from './field.js';
|
||||
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
||||
@@ -86,6 +87,9 @@ let positionToField: boolean | null = null;
|
||||
/** Callback to FocusManager to return ephemeral focus when the div closes. */
|
||||
let returnEphemeralFocus: ReturnEphemeralFocus | null = null;
|
||||
|
||||
/** Identifier for shortcut keydown listener used to unbind it. */
|
||||
let keydownListener: browserEvents.Data | null = null;
|
||||
|
||||
/**
|
||||
* Dropdown bounds info object used to encapsulate sizing information about a
|
||||
* bounding element (bounding box and width/height).
|
||||
@@ -122,6 +126,7 @@ export function createDom() {
|
||||
}
|
||||
div = document.createElement('div');
|
||||
div.className = 'blocklyDropDownDiv';
|
||||
div.tabIndex = -1;
|
||||
const parentDiv = common.getParentContainer() || document.body;
|
||||
parentDiv.appendChild(div);
|
||||
|
||||
@@ -129,6 +134,13 @@ export function createDom() {
|
||||
content.className = 'blocklyDropDownContent';
|
||||
div.appendChild(content);
|
||||
|
||||
keydownListener = browserEvents.conditionalBind(
|
||||
content,
|
||||
'keydown',
|
||||
null,
|
||||
common.globalShortcutHandler,
|
||||
);
|
||||
|
||||
arrow = document.createElement('div');
|
||||
arrow.className = 'blocklyDropDownArrow';
|
||||
div.appendChild(arrow);
|
||||
@@ -167,6 +179,10 @@ export function getContentDiv(): HTMLDivElement {
|
||||
|
||||
/** Clear the content of the drop-down. */
|
||||
export function clearContent() {
|
||||
if (keydownListener) {
|
||||
browserEvents.unbind(keydownListener);
|
||||
keydownListener = null;
|
||||
}
|
||||
div.remove();
|
||||
createDom();
|
||||
}
|
||||
@@ -192,6 +208,11 @@ export function setColour(backgroundColour: string, borderColour: string) {
|
||||
* @param block Block to position the drop-down around.
|
||||
* @param opt_onHide Optional callback for when the drop-down is hidden.
|
||||
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
|
||||
* @param manageEphemeralFocus Whether ephemeral focus should be managed
|
||||
* according to the drop-down div's lifetime. Note that if a false value is
|
||||
* passed in here then callers should manage ephemeral focus directly
|
||||
* otherwise focus may not properly restore when the widget closes. Defaults
|
||||
* to true.
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
export function showPositionedByBlock<T>(
|
||||
@@ -199,10 +220,12 @@ export function showPositionedByBlock<T>(
|
||||
block: BlockSvg,
|
||||
opt_onHide?: () => void,
|
||||
opt_secondaryYOffset?: number,
|
||||
manageEphemeralFocus: boolean = true,
|
||||
): boolean {
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfBlock(block),
|
||||
field as Field,
|
||||
manageEphemeralFocus,
|
||||
opt_onHide,
|
||||
opt_secondaryYOffset,
|
||||
);
|
||||
@@ -217,17 +240,24 @@ export function showPositionedByBlock<T>(
|
||||
* @param field The field to position the dropdown against.
|
||||
* @param opt_onHide Optional callback for when the drop-down is hidden.
|
||||
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
|
||||
* @param manageEphemeralFocus Whether ephemeral focus should be managed
|
||||
* according to the drop-down div's lifetime. Note that if a false value is
|
||||
* passed in here then callers should manage ephemeral focus directly
|
||||
* otherwise focus may not properly restore when the widget closes. Defaults
|
||||
* to true.
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
export function showPositionedByField<T>(
|
||||
field: Field<T>,
|
||||
opt_onHide?: () => void,
|
||||
opt_secondaryYOffset?: number,
|
||||
manageEphemeralFocus: boolean = true,
|
||||
): boolean {
|
||||
positionToField = true;
|
||||
return showPositionedByRect(
|
||||
getScaledBboxOfField(field as Field),
|
||||
field as Field,
|
||||
manageEphemeralFocus,
|
||||
opt_onHide,
|
||||
opt_secondaryYOffset,
|
||||
);
|
||||
@@ -271,16 +301,15 @@ function getScaledBboxOfField(field: Field): Rect {
|
||||
* @param manageEphemeralFocus Whether ephemeral focus should be managed
|
||||
* according to the drop-down div's lifetime. Note that if a false value is
|
||||
* passed in here then callers should manage ephemeral focus directly
|
||||
* otherwise focus may not properly restore when the widget closes. Defaults
|
||||
* to true.
|
||||
* otherwise focus may not properly restore when the widget closes.
|
||||
* @returns True if the menu rendered below block; false if above.
|
||||
*/
|
||||
function showPositionedByRect(
|
||||
bBox: Rect,
|
||||
field: Field,
|
||||
manageEphemeralFocus: boolean,
|
||||
opt_onHide?: () => void,
|
||||
opt_secondaryYOffset?: number,
|
||||
manageEphemeralFocus: boolean = true,
|
||||
): boolean {
|
||||
// If we can fit it, render below the block.
|
||||
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
|
||||
@@ -352,10 +381,6 @@ export function show<T>(
|
||||
dom.addClass(div, renderedClassName);
|
||||
dom.addClass(div, themeClassName);
|
||||
|
||||
if (manageEphemeralFocus) {
|
||||
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
|
||||
}
|
||||
|
||||
// When we change `translate` multiple times in close succession,
|
||||
// Chrome may choose to wait and apply them all at once.
|
||||
// Since we want the translation to initial X, Y to be immediate,
|
||||
@@ -364,7 +389,15 @@ export function show<T>(
|
||||
// making the dropdown appear to fly in from (0, 0).
|
||||
// Using both `left`, `top` for the initial translation and then `translate`
|
||||
// for the animated transition to final X, Y is a workaround.
|
||||
return positionInternal(primaryX, primaryY, secondaryX, secondaryY);
|
||||
const atOrigin = positionInternal(primaryX, primaryY, secondaryX, secondaryY);
|
||||
|
||||
// Ephemeral focus must happen after the div is fully visible in order to
|
||||
// ensure that it properly receives focus.
|
||||
if (manageEphemeralFocus) {
|
||||
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
|
||||
}
|
||||
|
||||
return atOrigin;
|
||||
}
|
||||
|
||||
const internal = {
|
||||
|
||||
@@ -312,7 +312,6 @@ export abstract class Field<T = any>
|
||||
const id = this.id_;
|
||||
if (!id) throw new Error('Expected ID to be defined prior to init.');
|
||||
this.fieldGroup_ = dom.createSvgElement(Svg.G, {
|
||||
'tabindex': '-1',
|
||||
'id': id,
|
||||
});
|
||||
if (!this.isVisible()) {
|
||||
|
||||
@@ -212,6 +212,17 @@ export class FieldImage extends Field<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this field should be clickable.
|
||||
*
|
||||
* @returns Whether this field is clickable.
|
||||
*/
|
||||
isClickable(): boolean {
|
||||
// Images are only clickable if they have a click handler and fulfill the
|
||||
// contract to be clickable: enabled and attached to an editable block.
|
||||
return super.isClickable() && !!this.clickHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* If field click is called, and click handler defined,
|
||||
* call the handler.
|
||||
|
||||
@@ -22,7 +22,6 @@ import {FlyoutItem} from './flyout_item.js';
|
||||
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
||||
import {FlyoutNavigator} from './flyout_navigator.js';
|
||||
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
|
||||
import {getFocusManager} from './focus_manager.js';
|
||||
import {IAutoHideable} from './interfaces/i_autohideable.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||
@@ -308,7 +307,6 @@ export abstract class Flyout
|
||||
// hide/show code will set up proper visibility and size later.
|
||||
this.svgGroup_ = dom.createSvgElement(tagName, {
|
||||
'class': 'blocklyFlyout',
|
||||
'tabindex': '0',
|
||||
});
|
||||
this.svgGroup_.style.display = 'none';
|
||||
this.svgBackground_ = dom.createSvgElement(
|
||||
@@ -324,8 +322,6 @@ export abstract class Flyout
|
||||
.getThemeManager()
|
||||
.subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
|
||||
|
||||
getFocusManager().registerTree(this);
|
||||
|
||||
return this.svgGroup_;
|
||||
}
|
||||
|
||||
@@ -407,7 +403,6 @@ export abstract class Flyout
|
||||
if (this.svgGroup_) {
|
||||
dom.removeNode(this.svgGroup_);
|
||||
}
|
||||
getFocusManager().unregisterTree(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -971,15 +966,22 @@ export abstract class Flyout
|
||||
return null;
|
||||
}
|
||||
|
||||
/** See IFocusableNode.getFocusableElement. */
|
||||
/**
|
||||
* See IFocusableNode.getFocusableElement.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
getFocusableElement(): HTMLElement | SVGElement {
|
||||
if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.');
|
||||
return this.svgGroup_;
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableNode.getFocusableTree. */
|
||||
/**
|
||||
* See IFocusableNode.getFocusableTree.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
getFocusableTree(): IFocusableTree {
|
||||
return this;
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableNode.onNodeFocus. */
|
||||
@@ -990,31 +992,45 @@ export abstract class Flyout
|
||||
|
||||
/** See IFocusableNode.canBeFocused. */
|
||||
canBeFocused(): boolean {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** See IFocusableTree.getRootFocusableNode. */
|
||||
/**
|
||||
* See IFocusableNode.getRootFocusableNode.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
getRootFocusableNode(): IFocusableNode {
|
||||
return this;
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableTree.getRestoredFocusableNode. */
|
||||
/**
|
||||
* See IFocusableNode.getRestoredFocusableNode.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
getRestoredFocusableNode(
|
||||
_previousNode: IFocusableNode | null,
|
||||
): IFocusableNode | null {
|
||||
return null;
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableTree.getNestedTrees. */
|
||||
/**
|
||||
* See IFocusableNode.getNestedTrees.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
getNestedTrees(): Array<IFocusableTree> {
|
||||
return [this.workspace_];
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableTree.lookUpFocusableNode. */
|
||||
/**
|
||||
* See IFocusableNode.lookUpFocusableNode.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
lookUpFocusableNode(_id: string): IFocusableNode | null {
|
||||
// No focusable node needs to be returned since the flyout's subtree is a
|
||||
// workspace that will manage its own focusable state.
|
||||
return null;
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
|
||||
/** See IFocusableTree.onTreeFocus. */
|
||||
@@ -1023,15 +1039,12 @@ export abstract class Flyout
|
||||
_previousTree: IFocusableTree | null,
|
||||
): void {}
|
||||
|
||||
/** See IFocusableTree.onTreeBlur. */
|
||||
onTreeBlur(nextTree: IFocusableTree | null): void {
|
||||
const toolbox = this.targetWorkspace.getToolbox();
|
||||
// If focus is moving to either the toolbox or the flyout's workspace, do
|
||||
// not close the flyout. For anything else, do close it since the flyout is
|
||||
// no longer focused.
|
||||
if (toolbox && nextTree === toolbox) return;
|
||||
if (nextTree === this.workspace_) return;
|
||||
if (toolbox) toolbox.clearSelection();
|
||||
this.autoHide(false);
|
||||
/**
|
||||
* See IFocusableNode.onTreeBlur.
|
||||
*
|
||||
* @deprecated v12: Use the Flyout's workspace for focus operations, instead.
|
||||
*/
|
||||
onTreeBlur(_nextTree: IFocusableTree | null): void {
|
||||
throw new Error('Flyouts are not directly focusable.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class FlyoutButton
|
||||
this.id = idGenerator.getNextUniqueId();
|
||||
this.svgGroup = dom.createSvgElement(
|
||||
Svg.G,
|
||||
{'id': this.id, 'class': cssClass, 'tabindex': '-1'},
|
||||
{'id': this.id, 'class': cssClass},
|
||||
this.workspace.getCanvas(),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,24 @@ import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js';
|
||||
*/
|
||||
export type ReturnEphemeralFocus = () => void;
|
||||
|
||||
/**
|
||||
* Represents an IFocusableTree that has been registered for focus management in
|
||||
* FocusManager.
|
||||
*/
|
||||
class TreeRegistration {
|
||||
/**
|
||||
* Constructs a new TreeRegistration.
|
||||
*
|
||||
* @param tree The tree being registered.
|
||||
* @param rootShouldBeAutoTabbable Whether the tree should have automatic
|
||||
* top-level tab management.
|
||||
*/
|
||||
constructor(
|
||||
readonly tree: IFocusableTree,
|
||||
readonly rootShouldBeAutoTabbable: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A per-page singleton that manages Blockly focus across one or more
|
||||
* IFocusableTrees, and bidirectionally synchronizes this focus with the DOM.
|
||||
@@ -58,24 +76,29 @@ export class FocusManager {
|
||||
|
||||
private focusedNode: IFocusableNode | null = null;
|
||||
private previouslyFocusedNode: IFocusableNode | null = null;
|
||||
private registeredTrees: Array<IFocusableTree> = [];
|
||||
private registeredTrees: Array<TreeRegistration> = [];
|
||||
|
||||
private currentlyHoldsEphemeralFocus: boolean = false;
|
||||
private lockFocusStateChanges: boolean = false;
|
||||
private recentlyLostAllFocus: boolean = false;
|
||||
private isUpdatingFocusedNode: boolean = false;
|
||||
|
||||
constructor(
|
||||
addGlobalEventListener: (type: string, listener: EventListener) => void,
|
||||
) {
|
||||
// Note that 'element' here is the element *gaining* focus.
|
||||
const maybeFocus = (element: Element | EventTarget | null) => {
|
||||
// Skip processing the event if the focused node is currently updating.
|
||||
if (this.isUpdatingFocusedNode) return;
|
||||
|
||||
this.recentlyLostAllFocus = !element;
|
||||
let newNode: IFocusableNode | null | undefined = null;
|
||||
if (element instanceof HTMLElement || element instanceof SVGElement) {
|
||||
// If the target losing or gaining focus maps to any tree, then it
|
||||
// should be updated. Per the contract of findFocusableNodeFor only one
|
||||
// tree should claim the element, so the search can be exited early.
|
||||
for (const tree of this.registeredTrees) {
|
||||
for (const reg of this.registeredTrees) {
|
||||
const tree = reg.tree;
|
||||
newNode = FocusableTreeTraverser.findFocusableNodeFor(element, tree);
|
||||
if (newNode) break;
|
||||
}
|
||||
@@ -128,13 +151,32 @@ export class FocusManager {
|
||||
* This function throws if the provided tree is already currently registered
|
||||
* in this manager. Use isRegistered to check in cases when it can't be
|
||||
* certain whether the tree has been registered.
|
||||
*
|
||||
* The tree's registration can be customized to configure automatic tab stops.
|
||||
* This specifically provides capability for the user to be able to tab
|
||||
* navigate to the root of the tree but only when the tree doesn't hold active
|
||||
* focus. If this functionality is disabled then the tree's root will
|
||||
* automatically be made focusable (but not tabbable) when it is first focused
|
||||
* in the same way as any other focusable node.
|
||||
*
|
||||
* @param tree The IFocusableTree to register.
|
||||
* @param rootShouldBeAutoTabbable Whether the root of this tree should be
|
||||
* added as a top-level page tab stop when it doesn't hold active focus.
|
||||
*/
|
||||
registerTree(tree: IFocusableTree): void {
|
||||
registerTree(
|
||||
tree: IFocusableTree,
|
||||
rootShouldBeAutoTabbable: boolean = false,
|
||||
): void {
|
||||
this.ensureManagerIsUnlocked();
|
||||
if (this.isRegistered(tree)) {
|
||||
throw Error(`Attempted to re-register already registered tree: ${tree}.`);
|
||||
}
|
||||
this.registeredTrees.push(tree);
|
||||
this.registeredTrees.push(
|
||||
new TreeRegistration(tree, rootShouldBeAutoTabbable),
|
||||
);
|
||||
if (rootShouldBeAutoTabbable) {
|
||||
tree.getRootFocusableNode().getFocusableElement().tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +185,15 @@ export class FocusManager {
|
||||
* unregisterTree.
|
||||
*/
|
||||
isRegistered(tree: IFocusableTree): boolean {
|
||||
return this.registeredTrees.findIndex((reg) => reg === tree) !== -1;
|
||||
return !!this.lookUpRegistration(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TreeRegistration for the specified tree, or null if the tree is
|
||||
* not currently registered.
|
||||
*/
|
||||
private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null {
|
||||
return this.registeredTrees.find((reg) => reg.tree === tree) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,13 +204,19 @@ export class FocusManager {
|
||||
*
|
||||
* This function throws if the provided tree is not currently registered in
|
||||
* this manager.
|
||||
*
|
||||
* This function will reset the tree's root element tabindex if the tree was
|
||||
* registered with automatic tab management.
|
||||
*/
|
||||
unregisterTree(tree: IFocusableTree): void {
|
||||
this.ensureManagerIsUnlocked();
|
||||
if (!this.isRegistered(tree)) {
|
||||
throw Error(`Attempted to unregister not registered tree: ${tree}.`);
|
||||
}
|
||||
const treeIndex = this.registeredTrees.findIndex((reg) => reg === tree);
|
||||
const treeIndex = this.registeredTrees.findIndex(
|
||||
(reg) => reg.tree === tree,
|
||||
);
|
||||
const registration = this.registeredTrees[treeIndex];
|
||||
this.registeredTrees.splice(treeIndex, 1);
|
||||
|
||||
const focusedNode = FocusableTreeTraverser.findFocusedNode(tree);
|
||||
@@ -170,6 +226,13 @@ export class FocusManager {
|
||||
this.updateFocusedNode(null);
|
||||
}
|
||||
this.removeHighlight(root);
|
||||
|
||||
if (registration.rootShouldBeAutoTabbable) {
|
||||
tree
|
||||
.getRootFocusableNode()
|
||||
.getFocusableElement()
|
||||
.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,14 +299,43 @@ export class FocusManager {
|
||||
* canBeFocused() method returns false), it will be ignored and any existing
|
||||
* focus state will remain unchanged.
|
||||
*
|
||||
* Note that this may update the specified node's element's tabindex to ensure
|
||||
* that it can be properly read out by screenreaders while focused.
|
||||
*
|
||||
* @param focusableNode The node that should receive active focus.
|
||||
*/
|
||||
focusNode(focusableNode: IFocusableNode): void {
|
||||
this.ensureManagerIsUnlocked();
|
||||
if (this.focusedNode === focusableNode) return; // State is unchanged.
|
||||
const mustRestoreUpdatingNode = !this.currentlyHoldsEphemeralFocus;
|
||||
if (mustRestoreUpdatingNode) {
|
||||
// Disable state syncing from DOM events since possible calls to focus()
|
||||
// below will loop a call back to focusNode().
|
||||
this.isUpdatingFocusedNode = true;
|
||||
}
|
||||
|
||||
// Double check that state wasn't desynchronized in the background. See:
|
||||
// https://github.com/google/blockly-keyboard-experimentation/issues/87.
|
||||
// This is only done for the case where the same node is being focused twice
|
||||
// since other cases should automatically correct (due to the rest of the
|
||||
// routine running as normal).
|
||||
const prevFocusedElement = this.focusedNode?.getFocusableElement();
|
||||
const hasDesyncedState = prevFocusedElement !== document.activeElement;
|
||||
if (this.focusedNode === focusableNode && !hasDesyncedState) {
|
||||
if (mustRestoreUpdatingNode) {
|
||||
// Reenable state syncing from DOM events.
|
||||
this.isUpdatingFocusedNode = false;
|
||||
}
|
||||
return; // State is unchanged.
|
||||
}
|
||||
|
||||
if (!focusableNode.canBeFocused()) {
|
||||
// This node can't be focused.
|
||||
console.warn("Trying to focus a node that can't be focused.");
|
||||
|
||||
if (mustRestoreUpdatingNode) {
|
||||
// Reenable state syncing from DOM events.
|
||||
this.isUpdatingFocusedNode = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,6 +384,10 @@ export class FocusManager {
|
||||
this.activelyFocusNode(nodeToFocus, prevTree ?? null);
|
||||
}
|
||||
this.updateFocusedNode(nodeToFocus);
|
||||
if (mustRestoreUpdatingNode) {
|
||||
// Reenable state syncing from DOM events.
|
||||
this.isUpdatingFocusedNode = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,14 +520,38 @@ export class FocusManager {
|
||||
// node's focusable element (which *is* allowed to be invisible until the
|
||||
// node needs to be focused).
|
||||
this.lockFocusStateChanges = true;
|
||||
if (node.getFocusableTree() !== prevTree) {
|
||||
node.getFocusableTree().onTreeFocus(node, prevTree);
|
||||
const tree = node.getFocusableTree();
|
||||
const elem = node.getFocusableElement();
|
||||
const nextTreeReg = this.lookUpRegistration(tree);
|
||||
const treeIsTabManaged = nextTreeReg?.rootShouldBeAutoTabbable;
|
||||
if (tree !== prevTree) {
|
||||
tree.onTreeFocus(node, prevTree);
|
||||
|
||||
if (treeIsTabManaged) {
|
||||
// If this node's tree has its tab auto-managed, ensure that it's no
|
||||
// longer tabbable now that it holds active focus.
|
||||
tree.getRootFocusableNode().getFocusableElement().tabIndex = -1;
|
||||
}
|
||||
}
|
||||
node.onNodeFocus();
|
||||
this.lockFocusStateChanges = false;
|
||||
|
||||
// The tab index should be set in all cases where:
|
||||
// - It doesn't overwrite an pre-set tab index for the node.
|
||||
// - The node is part of a tree whose tab index is unmanaged.
|
||||
// OR
|
||||
// - The node is part of a managed tree but this isn't the root. Managed
|
||||
// roots are ignored since they are always overwritten to have a tab index
|
||||
// of -1 with active focus so that they cannot be tab navigated.
|
||||
//
|
||||
// Setting the tab index ensures that the node's focusable element can
|
||||
// actually receive DOM focus.
|
||||
if (!treeIsTabManaged || node !== tree.getRootFocusableNode()) {
|
||||
if (!elem.hasAttribute('tabindex')) elem.tabIndex = -1;
|
||||
}
|
||||
|
||||
this.setNodeToVisualActiveFocus(node);
|
||||
node.getFocusableElement().focus();
|
||||
elem.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -451,13 +571,21 @@ export class FocusManager {
|
||||
nextTree: IFocusableTree | null,
|
||||
): void {
|
||||
this.lockFocusStateChanges = true;
|
||||
if (node.getFocusableTree() !== nextTree) {
|
||||
node.getFocusableTree().onTreeBlur(nextTree);
|
||||
const tree = node.getFocusableTree();
|
||||
if (tree !== nextTree) {
|
||||
tree.onTreeBlur(nextTree);
|
||||
|
||||
const reg = this.lookUpRegistration(tree);
|
||||
if (reg?.rootShouldBeAutoTabbable) {
|
||||
// If this node's tree has its tab auto-managed, ensure that it's now
|
||||
// tabbable since it no longer holds active focus.
|
||||
tree.getRootFocusableNode().getFocusableElement().tabIndex = 0;
|
||||
}
|
||||
}
|
||||
node.onNodeBlur();
|
||||
this.lockFocusStateChanges = false;
|
||||
|
||||
if (node.getFocusableTree() !== nextTree) {
|
||||
if (tree !== nextTree) {
|
||||
this.setNodeToVisualPassiveFocus(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
|
||||
import {IDragger} from './interfaces/i_dragger.js';
|
||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||
import type {IIcon} from './interfaces/i_icon.js';
|
||||
import {keyboardNavigationController} from './keyboard_navigation_controller.js';
|
||||
import * as registry from './registry.js';
|
||||
import * as Tooltip from './tooltip.js';
|
||||
import * as Touch from './touch.js';
|
||||
@@ -541,8 +542,10 @@ export class Gesture {
|
||||
// have higher priority than workspaces. The ordering within drags does
|
||||
// not matter, because the three types of dragging are exclusive.
|
||||
if (this.dragger) {
|
||||
keyboardNavigationController.setIsActive(false);
|
||||
this.dragger.onDragEnd(e, this.currentDragDeltaXY);
|
||||
} else if (this.workspaceDragger) {
|
||||
keyboardNavigationController.setIsActive(false);
|
||||
this.workspaceDragger.endDrag(this.currentDragDeltaXY);
|
||||
} else if (this.isBubbleClick()) {
|
||||
// Do nothing, bubbles don't currently respond to clicks.
|
||||
@@ -743,6 +746,8 @@ export class Gesture {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
keyboardNavigationController.setIsActive(false);
|
||||
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ export abstract class Icon implements IIcon {
|
||||
const svgBlock = this.sourceBlock as BlockSvg;
|
||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyIconGroup',
|
||||
'tabindex': '-1',
|
||||
'id': this.id,
|
||||
});
|
||||
svgBlock.getSvgRoot().appendChild(this.svgRoot);
|
||||
@@ -178,4 +177,13 @@ export abstract class Icon implements IIcon {
|
||||
canBeFocused(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the block that this icon is attached to.
|
||||
*
|
||||
* @returns The block this icon is attached to.
|
||||
*/
|
||||
getSourceBlock(): Block {
|
||||
return this.sourceBlock;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import * as dropDownDiv from './dropdowndiv.js';
|
||||
import {Grid} from './grid.js';
|
||||
import {Options} from './options.js';
|
||||
import {ScrollbarPair} from './scrollbar_pair.js';
|
||||
import {ShortcutRegistry} from './shortcut_registry.js';
|
||||
import * as Tooltip from './tooltip.js';
|
||||
import * as Touch from './touch.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
@@ -72,17 +71,12 @@ export function inject(
|
||||
common.setMainWorkspace(workspace);
|
||||
});
|
||||
|
||||
browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown);
|
||||
browserEvents.conditionalBind(
|
||||
dropDownDiv.getContentDiv(),
|
||||
subContainer,
|
||||
'keydown',
|
||||
null,
|
||||
onKeyDown,
|
||||
common.globalShortcutHandler,
|
||||
);
|
||||
const widgetContainer = WidgetDiv.getDiv();
|
||||
if (widgetContainer) {
|
||||
browserEvents.conditionalBind(widgetContainer, 'keydown', null, onKeyDown);
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
@@ -292,32 +286,6 @@ function init(mainWorkspace: WorkspaceSvg) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
|
||||
* is not visible.
|
||||
*
|
||||
* @param e Key down event.
|
||||
*/
|
||||
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there
|
||||
// are multiple workspaces and non-main workspaces are able to accept input.
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
|
||||
if (!mainWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
browserEvents.isTargetInput(e) ||
|
||||
(mainWorkspace.rendered && !mainWorkspace.isVisible())
|
||||
) {
|
||||
// When focused on an HTML text input widget, don't trap any keys.
|
||||
// Ignore keypresses on rendered workspaces that have been explicitly
|
||||
// hidden.
|
||||
return;
|
||||
}
|
||||
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether event handlers have been bound. Document event handlers will only
|
||||
* be bound once, even if Blockly is destroyed and reinjected.
|
||||
|
||||
@@ -19,13 +19,11 @@ export interface IFocusableNode {
|
||||
* - blocklyActiveFocus
|
||||
* - blocklyPassiveFocus
|
||||
*
|
||||
* The returned element must also have a valid ID specified, and unique across
|
||||
* the entire page. Failing to have a properly unique ID could result in
|
||||
* trying to focus one node (such as via a mouse click) leading to another
|
||||
* node with the same ID actually becoming focused by FocusManager. The
|
||||
* returned element must also have a negative tabindex (since the focus
|
||||
* manager itself will manage its tab index and a tab index must be present in
|
||||
* order for the element to be focusable in the DOM).
|
||||
* The returned element must also have a valid ID specified, and this ID
|
||||
* should be unique across the entire page. Failing to have a properly unique
|
||||
* ID could result in trying to focus one node (such as via a mouse click)
|
||||
* leading to another node with the same ID actually becoming focused by
|
||||
* FocusManager.
|
||||
*
|
||||
* The returned element must be visible if the node is ever focused via
|
||||
* FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an
|
||||
@@ -34,7 +32,11 @@ export interface IFocusableNode {
|
||||
*
|
||||
* It's expected the actual returned element will not change for the lifetime
|
||||
* of the node (that is, its properties can change but a new element should
|
||||
* never be returned).
|
||||
* never be returned). Also, the returned element will have its tabindex
|
||||
* overwritten throughout the lifecycle of this node and FocusManager.
|
||||
*
|
||||
* If a node requires the ability to be focused directly without first being
|
||||
* focused via FocusManager then it must set its own tab index.
|
||||
*
|
||||
* @returns The HTMLElement or SVGElement which can both receive focus and be
|
||||
* visually represented as actively or passively focused for this node.
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import type {Field} from '../field.js';
|
||||
import type {Icon} from '../icons/icon.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
@@ -21,15 +24,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* @returns The first field or input of the given block, if any.
|
||||
*/
|
||||
getFirstChild(current: BlockSvg): IFocusableNode | null {
|
||||
for (const input of current.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
return field;
|
||||
}
|
||||
if (input.connection?.targetBlock())
|
||||
return input.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
|
||||
return null;
|
||||
const candidates = getBlockNavigationCandidates(current);
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,41 +50,16 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* Returns the next peer node of the given block.
|
||||
*
|
||||
* @param current The block to find the following element of.
|
||||
* @returns The first block of the next stack if the given block is a terminal
|
||||
* @returns The first node of the next input/stack if the given block is a terminal
|
||||
* block, or its next connection.
|
||||
*/
|
||||
getNextSibling(current: BlockSvg): IFocusableNode | null {
|
||||
if (current.nextConnection?.targetBlock()) {
|
||||
return current.nextConnection?.targetBlock();
|
||||
}
|
||||
|
||||
const parent = this.getParent(current);
|
||||
let navigatingCrossStacks = false;
|
||||
let siblings: (BlockSvg | Field)[] = [];
|
||||
if (parent instanceof BlockSvg) {
|
||||
for (let i = 0, input; (input = parent.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
siblings.push(...input.fieldRow);
|
||||
const child = input.connection.targetBlock();
|
||||
if (child) {
|
||||
siblings.push(child as BlockSvg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parent instanceof WorkspaceSvg) {
|
||||
siblings = parent.getTopBlocks(true);
|
||||
navigatingCrossStacks = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentIndex = siblings.indexOf(
|
||||
navigatingCrossStacks ? current.getRootBlock() : current,
|
||||
);
|
||||
if (currentIndex >= 0 && currentIndex < siblings.length - 1) {
|
||||
return siblings[currentIndex + 1];
|
||||
} else if (currentIndex === siblings.length - 1 && navigatingCrossStacks) {
|
||||
return siblings[0];
|
||||
} else if (current.outputConnection?.targetBlock()) {
|
||||
return navigateBlock(current, 1);
|
||||
} else if (this.getParent(current) instanceof WorkspaceSvg) {
|
||||
return navigateStacks(current, 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -104,43 +75,13 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
getPreviousSibling(current: BlockSvg): IFocusableNode | null {
|
||||
if (current.previousConnection?.targetBlock()) {
|
||||
return current.previousConnection?.targetBlock();
|
||||
} else if (current.outputConnection?.targetBlock()) {
|
||||
return navigateBlock(current, -1);
|
||||
} else if (this.getParent(current) instanceof WorkspaceSvg) {
|
||||
return navigateStacks(current, -1);
|
||||
}
|
||||
|
||||
const parent = this.getParent(current);
|
||||
let navigatingCrossStacks = false;
|
||||
let siblings: (BlockSvg | Field)[] = [];
|
||||
if (parent instanceof BlockSvg) {
|
||||
for (let i = 0, input; (input = parent.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
siblings.push(...input.fieldRow);
|
||||
const child = input.connection.targetBlock();
|
||||
if (child) {
|
||||
siblings.push(child as BlockSvg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parent instanceof WorkspaceSvg) {
|
||||
siblings = parent.getTopBlocks(true);
|
||||
navigatingCrossStacks = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentIndex = siblings.indexOf(current);
|
||||
let result: IFocusableNode | null = null;
|
||||
if (currentIndex >= 1) {
|
||||
result = siblings[currentIndex - 1];
|
||||
} else if (currentIndex === 0 && navigatingCrossStacks) {
|
||||
result = siblings[siblings.length - 1];
|
||||
}
|
||||
|
||||
// If navigating to a previous stack, our previous sibling is the last
|
||||
// block in it.
|
||||
if (navigatingCrossStacks && result instanceof BlockSvg) {
|
||||
return result.lastConnectionInStack(false)?.getSourceBlock() ?? result;
|
||||
}
|
||||
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,3 +104,88 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
return current instanceof BlockSvg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of the navigable children of the given block.
|
||||
*
|
||||
* @param block The block to retrieve the navigable children of.
|
||||
* @returns A list of navigable/focusable children of the given block.
|
||||
*/
|
||||
function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] {
|
||||
const candidates: IFocusableNode[] = block.getIcons();
|
||||
|
||||
for (const input of block.inputList) {
|
||||
if (!input.isVisible()) continue;
|
||||
candidates.push(...input.fieldRow);
|
||||
if (input.connection?.targetBlock()) {
|
||||
candidates.push(input.connection.targetBlock() as BlockSvg);
|
||||
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
|
||||
candidates.push(input.connection as RenderedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next/previous stack relative to the given block's stack.
|
||||
*
|
||||
* @param current The block whose stack will be navigated relative to.
|
||||
* @param delta The difference in index to navigate; positive values navigate
|
||||
* to the nth next stack, while negative values navigate to the nth previous
|
||||
* stack.
|
||||
* @returns The first block in the stack offset by `delta` relative to the
|
||||
* current block's stack, or the last block in the stack offset by `delta`
|
||||
* relative to the current block's stack when navigating backwards.
|
||||
*/
|
||||
export function navigateStacks(current: BlockSvg, delta: number) {
|
||||
const stacks = current.workspace.getTopBlocks(true);
|
||||
const currentIndex = stacks.indexOf(current.getRootBlock());
|
||||
const targetIndex = currentIndex + delta;
|
||||
let result: BlockSvg | null = null;
|
||||
if (targetIndex >= 0 && targetIndex < stacks.length) {
|
||||
result = stacks[targetIndex];
|
||||
} else if (targetIndex < 0) {
|
||||
result = stacks[stacks.length - 1];
|
||||
} else if (targetIndex >= stacks.length) {
|
||||
result = stacks[0];
|
||||
}
|
||||
|
||||
// When navigating to a previous stack, our previous sibling is the last
|
||||
// block in it.
|
||||
if (delta < 0 && result) {
|
||||
return result.lastConnectionInStack(false)?.getSourceBlock() ?? result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next navigable item relative to the provided block child.
|
||||
*
|
||||
* @param current The navigable block child item to navigate relative to.
|
||||
* @param delta The difference in index to navigate; positive values navigate
|
||||
* forward by n, while negative values navigate backwards by n.
|
||||
* @returns The navigable block child offset by `delta` relative to `current`.
|
||||
*/
|
||||
export function navigateBlock(
|
||||
current: Icon | Field | RenderedConnection | BlockSvg,
|
||||
delta: number,
|
||||
): IFocusableNode | null {
|
||||
const block =
|
||||
current instanceof BlockSvg
|
||||
? current.outputConnection.targetBlock()
|
||||
: current.getSourceBlock();
|
||||
if (!(block instanceof BlockSvg)) return null;
|
||||
|
||||
const candidates = getBlockNavigationCandidates(block);
|
||||
const currentIndex = candidates.indexOf(current);
|
||||
if (currentIndex === -1) return null;
|
||||
|
||||
const targetIndex = currentIndex + delta;
|
||||
if (targetIndex >= 0 && targetIndex < candidates.length) {
|
||||
return candidates[targetIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {ConnectionType} from '../connection_type.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import {navigateBlock} from './block_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a connection.
|
||||
@@ -37,17 +38,7 @@ export class ConnectionNavigationPolicy
|
||||
* @returns The given connection's parent connection or block.
|
||||
*/
|
||||
getParent(current: RenderedConnection): IFocusableNode | null {
|
||||
if (current.type === ConnectionType.OUTPUT_VALUE) {
|
||||
return current.targetConnection ?? current.getSourceBlock();
|
||||
} else if (current.getParentInput()) {
|
||||
return current.getSourceBlock();
|
||||
}
|
||||
|
||||
const topBlock = current.getSourceBlock().getTopStackBlock();
|
||||
return (
|
||||
(this.getParentConnection(topBlock)?.targetConnection?.getParentInput()
|
||||
?.connection as RenderedConnection) ?? topBlock
|
||||
);
|
||||
return current.getSourceBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,19 +49,7 @@ export class ConnectionNavigationPolicy
|
||||
*/
|
||||
getNextSibling(current: RenderedConnection): IFocusableNode | null {
|
||||
if (current.getParentInput()) {
|
||||
const parentInput = current.getParentInput();
|
||||
const block = parentInput?.getSourceBlock();
|
||||
if (!block || !parentInput) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(parentInput);
|
||||
for (let i = curIdx + 1; i < block.inputList.length; i++) {
|
||||
const input = block.inputList[i];
|
||||
const fieldRow = input.fieldRow;
|
||||
if (fieldRow.length) return fieldRow[0];
|
||||
if (input.connection) return input.connection as RenderedConnection;
|
||||
}
|
||||
|
||||
return null;
|
||||
return navigateBlock(current, 1);
|
||||
} else if (current.type === ConnectionType.NEXT_STATEMENT) {
|
||||
const nextBlock = current.targetConnection;
|
||||
// If this connection is the last one in the stack, our next sibling is
|
||||
@@ -103,20 +82,7 @@ export class ConnectionNavigationPolicy
|
||||
*/
|
||||
getPreviousSibling(current: RenderedConnection): IFocusableNode | null {
|
||||
if (current.getParentInput()) {
|
||||
const parentInput = current.getParentInput();
|
||||
const block = parentInput?.getSourceBlock();
|
||||
if (!block || !parentInput) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(parentInput);
|
||||
for (let i = curIdx; i >= 0; i--) {
|
||||
const input = block.inputList[i];
|
||||
if (input.connection && input !== parentInput) {
|
||||
return input.connection as RenderedConnection;
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
if (fieldRow.length) return fieldRow[fieldRow.length - 1];
|
||||
}
|
||||
return null;
|
||||
return navigateBlock(current, -1);
|
||||
} else if (
|
||||
current.type === ConnectionType.PREVIOUS_STATEMENT ||
|
||||
current.type === ConnectionType.OUTPUT_VALUE
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {BlockSvg} from '../block_svg.js';
|
||||
import {Field} from '../field.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {navigateBlock} from './block_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a field.
|
||||
@@ -40,22 +41,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
* @returns The next field or input in the given field's block.
|
||||
*/
|
||||
getNextSibling(current: Field<any>): IFocusableNode | null {
|
||||
const input = current.getParentInput();
|
||||
const block = current.getSourceBlock();
|
||||
if (!block) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(input);
|
||||
let fieldIdx = input.fieldRow.indexOf(current) + 1;
|
||||
for (let i = curIdx; i < block.inputList.length; i++) {
|
||||
const newInput = block.inputList[i];
|
||||
const fieldRow = newInput.fieldRow;
|
||||
if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx];
|
||||
fieldIdx = 0;
|
||||
if (newInput.connection?.targetBlock()) {
|
||||
return newInput.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return navigateBlock(current, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,27 +51,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
* @returns The preceding field or input in the given field's block.
|
||||
*/
|
||||
getPreviousSibling(current: Field<any>): IFocusableNode | null {
|
||||
const parentInput = current.getParentInput();
|
||||
const block = current.getSourceBlock();
|
||||
if (!block) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(parentInput);
|
||||
let fieldIdx = parentInput.fieldRow.indexOf(current) - 1;
|
||||
for (let i = curIdx; i >= 0; i--) {
|
||||
const input = block.inputList[i];
|
||||
if (input.connection?.targetBlock() && input !== parentInput) {
|
||||
return input.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
if (fieldIdx > -1) return fieldRow[fieldIdx];
|
||||
|
||||
// Reset the fieldIdx to the length of the field row of the previous
|
||||
// input.
|
||||
if (i - 1 >= 0) {
|
||||
fieldIdx = block.inputList[i - 1].fieldRow.length - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return navigateBlock(current, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +63,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
isNavigable(current: Field<any>): boolean {
|
||||
return (
|
||||
current.canBeFocused() &&
|
||||
current.isVisible() &&
|
||||
(current.isClickable() || current.isCurrentlyEditable()) &&
|
||||
!(
|
||||
current.getSourceBlock()?.isSimpleReporter() &&
|
||||
|
||||
76
core/keyboard_nav/icon_navigation_policy.ts
Normal file
76
core/keyboard_nav/icon_navigation_policy.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {Icon} from '../icons/icon.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import {navigateBlock} from './block_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from an icon.
|
||||
*/
|
||||
export class IconNavigationPolicy implements INavigationPolicy<Icon> {
|
||||
/**
|
||||
* Returns the first child of the given icon.
|
||||
*
|
||||
* @param _current The icon to return the first child of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getFirstChild(_current: Icon): IFocusableNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given icon.
|
||||
*
|
||||
* @param current The icon to return the parent of.
|
||||
* @returns The source block of the given icon.
|
||||
*/
|
||||
getParent(current: Icon): IFocusableNode | null {
|
||||
return current.getSourceBlock() as BlockSvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next peer node of the given icon.
|
||||
*
|
||||
* @param current The icon to find the following element of.
|
||||
* @returns The next icon, field or input following this icon, if any.
|
||||
*/
|
||||
getNextSibling(current: Icon): IFocusableNode | null {
|
||||
return navigateBlock(current, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous peer node of the given icon.
|
||||
*
|
||||
* @param current The icon to find the preceding element of.
|
||||
* @returns The icon's previous icon, if any.
|
||||
*/
|
||||
getPreviousSibling(current: Icon): IFocusableNode | null {
|
||||
return navigateBlock(current, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the given icon can be navigated to.
|
||||
*
|
||||
* @param current The instance to check for navigability.
|
||||
* @returns True if the given icon can be focused.
|
||||
*/
|
||||
isNavigable(current: Icon): boolean {
|
||||
return current.canBeFocused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given object can be navigated from by this policy.
|
||||
*
|
||||
* @param current The object to check if this policy applies to.
|
||||
* @returns True if the object is an Icon.
|
||||
*/
|
||||
isApplicable(current: any): current is Icon {
|
||||
return current instanceof Icon;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {Field} from '../field.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
@@ -377,7 +378,7 @@ export class LineCursor extends Marker {
|
||||
// Ensure the current node matches what's currently focused.
|
||||
const focused = getFocusManager().getFocusedNode();
|
||||
const block = this.getSourceBlockFromNode(focused);
|
||||
if (!block || block.workspace === this.workspace) {
|
||||
if (block && block.workspace === this.workspace) {
|
||||
// If the current focused node corresponds to a block then ensure that it
|
||||
// belongs to the correct workspace for this cursor.
|
||||
this.setCurNode(focused);
|
||||
@@ -406,6 +407,11 @@ export class LineCursor extends Marker {
|
||||
newNode.workspace.scrollBoundsIntoView(
|
||||
newNode.getBoundingRectangleWithoutChildren(),
|
||||
);
|
||||
} else if (newNode instanceof Field) {
|
||||
const block = newNode.getSourceBlock() as BlockSvg;
|
||||
block.workspace.scrollBoundsIntoView(
|
||||
block.getBoundingRectangleWithoutChildren(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
core/keyboard_navigation_controller.ts
Normal file
63
core/keyboard_navigation_controller.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* The KeyboardNavigationController handles coordinating Blockly-wide
|
||||
* keyboard navigation behavior, such as enabling/disabling full
|
||||
* cursor visualization.
|
||||
*/
|
||||
export class KeyboardNavigationController {
|
||||
/** Whether the user is actively using keyboard navigation. */
|
||||
private isActive = false;
|
||||
/** Css class name added to body if keyboard nav is active. */
|
||||
private activeClassName = 'blocklyKeyboardNavigation';
|
||||
|
||||
/**
|
||||
* Sets whether a user is actively using keyboard navigation.
|
||||
*
|
||||
* If they are, apply a css class to the entire page so that
|
||||
* focused items can apply additional styling for keyboard users.
|
||||
*
|
||||
* Note that since enabling keyboard navigation presents significant UX changes
|
||||
* (such as cursor visualization and move mode), callers should take care to
|
||||
* only set active keyboard navigation when they have a high confidence in that
|
||||
* being the correct state. In general, in any given mouse or key input situation
|
||||
* callers can choose one of three paths:
|
||||
* 1. Do nothing. This should be the choice for neutral actions that don't
|
||||
* predominantly imply keyboard or mouse usage (such as clicking to select a block).
|
||||
* 2. Disable keyboard navigation. This is the best choice when a user is definitely
|
||||
* predominantly using the mouse (such as using a right click to open the context menu).
|
||||
* 3. Enable keyboard navigation. This is the best choice when there's high confidence
|
||||
* a user actually intends to use it (such as attempting to use the arrow keys to move
|
||||
* around).
|
||||
*
|
||||
* @param isUsing
|
||||
*/
|
||||
setIsActive(isUsing: boolean = true) {
|
||||
this.isActive = isUsing;
|
||||
this.updateActiveVisualization();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the user is actively using keyboard navigation
|
||||
* (e.g., has recently taken some action that is only relevant to keyboard users)
|
||||
*/
|
||||
getIsActive(): boolean {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/** Adds or removes the css class that indicates keyboard navigation is active. */
|
||||
private updateActiveVisualization() {
|
||||
if (this.isActive) {
|
||||
document.body.classList.add(this.activeClassName);
|
||||
} else {
|
||||
document.body.classList.remove(this.activeClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton instance of the keyboard navigation controller. */
|
||||
export const keyboardNavigationController = new KeyboardNavigationController();
|
||||
@@ -9,6 +9,7 @@ import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
|
||||
import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js';
|
||||
import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
|
||||
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
|
||||
import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js';
|
||||
import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js';
|
||||
|
||||
type RuleList<T> = INavigationPolicy<T>[];
|
||||
@@ -27,6 +28,7 @@ export class Navigator {
|
||||
new FieldNavigationPolicy(),
|
||||
new ConnectionNavigationPolicy(),
|
||||
new WorkspaceNavigationPolicy(),
|
||||
new IconNavigationPolicy(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,7 +50,7 @@ export class PathObject implements IPathObject {
|
||||
/** The primary path of the block. */
|
||||
this.svgPath = dom.createSvgElement(
|
||||
Svg.PATH,
|
||||
{'class': 'blocklyPath', 'tabindex': '-1'},
|
||||
{'class': 'blocklyPath'},
|
||||
this.svgRoot,
|
||||
);
|
||||
|
||||
@@ -239,7 +239,6 @@ export class PathObject implements IPathObject {
|
||||
'id': connection.id,
|
||||
'class': 'blocklyHighlightedConnectionPath',
|
||||
'style': 'display: none;',
|
||||
'tabindex': '-1',
|
||||
'd': connectionPath,
|
||||
'transform': transformation,
|
||||
},
|
||||
|
||||
@@ -10,13 +10,22 @@ import {BlockSvg} from './block_svg.js';
|
||||
import * as clipboard from './clipboard.js';
|
||||
import * as eventUtils from './events/utils.js';
|
||||
import {Gesture} from './gesture.js';
|
||||
import {ICopyData, isCopyable} from './interfaces/i_copyable.js';
|
||||
import {isDeletable} from './interfaces/i_deletable.js';
|
||||
import {isDraggable} from './interfaces/i_draggable.js';
|
||||
import {
|
||||
ICopyable,
|
||||
ICopyData,
|
||||
isCopyable as isICopyable,
|
||||
} from './interfaces/i_copyable.js';
|
||||
import {
|
||||
IDeletable,
|
||||
isDeletable as isIDeletable,
|
||||
} from './interfaces/i_deletable.js';
|
||||
import {IDraggable, isDraggable} from './interfaces/i_draggable.js';
|
||||
import {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import {KeyCodes} from './utils/keycodes.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import {WorkspaceSvg} from './workspace_svg.js';
|
||||
|
||||
/**
|
||||
@@ -61,7 +70,7 @@ export function registerDelete() {
|
||||
return (
|
||||
!workspace.isReadOnly() &&
|
||||
focused != null &&
|
||||
isDeletable(focused) &&
|
||||
isIDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
!Gesture.inProgress()
|
||||
);
|
||||
@@ -75,7 +84,7 @@ export function registerDelete() {
|
||||
const focused = scope.focusedNode;
|
||||
if (focused instanceof BlockSvg) {
|
||||
focused.checkAndDelete();
|
||||
} else if (isDeletable(focused) && focused.isDeletable()) {
|
||||
} else if (isIDeletable(focused) && focused.isDeletable()) {
|
||||
eventUtils.setGroup(true);
|
||||
focused.dispose();
|
||||
eventUtils.setGroup(false);
|
||||
@@ -91,6 +100,39 @@ let copyData: ICopyData | null = null;
|
||||
let copyWorkspace: WorkspaceSvg | null = null;
|
||||
let copyCoords: Coordinate | null = null;
|
||||
|
||||
/**
|
||||
* Determine if a focusable node can be copied using cut or copy.
|
||||
*
|
||||
* Unfortunately the ICopyable interface doesn't include an isCopyable
|
||||
* method, so we must use some other criteria to make the decision.
|
||||
* Specifically,
|
||||
*
|
||||
* - It must be an ICopyable.
|
||||
* - So that a pasted copy can be manipluated and/or disposed of, it
|
||||
* must be both an IDraggable and an IDeletable.
|
||||
* - Additionally, both .isMovable() and .isDeletable() must return
|
||||
* true (i.e., can currently be moved and deleted).
|
||||
*
|
||||
* TODO(#9098): Revise these criteria. The latter criteria prevents
|
||||
* shadow blocks from being copied; additionally, there are likely to
|
||||
* be other circumstances were it is desirable to allow movable /
|
||||
* copyable copies of a currently-unmovable / -copyable block to be
|
||||
* made.
|
||||
*
|
||||
* @param focused The focused object.
|
||||
*/
|
||||
function isCopyable(
|
||||
focused: IFocusableNode,
|
||||
): focused is ICopyable<ICopyData> & IDeletable & IDraggable {
|
||||
return (
|
||||
isICopyable(focused) &&
|
||||
isIDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isDraggable(focused) &&
|
||||
focused.isMovable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c.
|
||||
*/
|
||||
@@ -109,11 +151,7 @@ export function registerCopy() {
|
||||
return (
|
||||
!workspace.isReadOnly() &&
|
||||
!Gesture.inProgress() &&
|
||||
focused != null &&
|
||||
isDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isDraggable(focused) &&
|
||||
focused.isMovable() &&
|
||||
!!focused &&
|
||||
isCopyable(focused)
|
||||
);
|
||||
},
|
||||
@@ -123,7 +161,7 @@ export function registerCopy() {
|
||||
e.preventDefault();
|
||||
workspace.hideChaff();
|
||||
const focused = scope.focusedNode;
|
||||
if (!focused || !isCopyable(focused)) return false;
|
||||
if (!focused || !isICopyable(focused)) return false;
|
||||
copyData = focused.toCopyData();
|
||||
copyWorkspace =
|
||||
focused.workspace instanceof WorkspaceSvg
|
||||
@@ -157,13 +195,11 @@ export function registerCut() {
|
||||
return (
|
||||
!workspace.isReadOnly() &&
|
||||
!Gesture.inProgress() &&
|
||||
focused != null &&
|
||||
isDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isDraggable(focused) &&
|
||||
focused.isMovable() &&
|
||||
!!focused &&
|
||||
isCopyable(focused) &&
|
||||
!focused.workspace.isFlyout
|
||||
// Extra criteria for cut (not just copy):
|
||||
!focused.workspace.isFlyout &&
|
||||
focused.isDeletable()
|
||||
);
|
||||
},
|
||||
callback(workspace, e, shortcut, scope) {
|
||||
@@ -176,9 +212,9 @@ export function registerCut() {
|
||||
focused.checkAndDelete();
|
||||
return true;
|
||||
} else if (
|
||||
isDeletable(focused) &&
|
||||
isIDeletable(focused) &&
|
||||
focused.isDeletable() &&
|
||||
isCopyable(focused)
|
||||
isICopyable(focused)
|
||||
) {
|
||||
copyData = focused.toCopyData();
|
||||
copyWorkspace = workspace;
|
||||
@@ -212,8 +248,22 @@ export function registerPaste() {
|
||||
preconditionFn(workspace) {
|
||||
return !workspace.isReadOnly() && !Gesture.inProgress();
|
||||
},
|
||||
callback() {
|
||||
callback(workspace: WorkspaceSvg, e: Event) {
|
||||
if (!copyData || !copyWorkspace) return false;
|
||||
|
||||
if (e instanceof PointerEvent) {
|
||||
// The event that triggers a shortcut would conventionally be a KeyboardEvent.
|
||||
// However, it may be a PointerEvent if a context menu item was used as a
|
||||
// wrapper for this callback, in which case the new block(s) should be pasted
|
||||
// at the mouse coordinates where the menu was opened, and this PointerEvent
|
||||
// is where the menu was opened.
|
||||
const mouseCoords = svgMath.screenToWsCoordinates(
|
||||
copyWorkspace,
|
||||
new Coordinate(e.clientX, e.clientY),
|
||||
);
|
||||
return !!clipboard.paste(copyData, copyWorkspace, mouseCoords);
|
||||
}
|
||||
|
||||
if (!copyCoords) {
|
||||
// If we don't have location data about the original copyable, let the
|
||||
// paster determine position.
|
||||
|
||||
@@ -278,7 +278,9 @@ export class ShortcutRegistry {
|
||||
* Undefined if no shortcuts exist.
|
||||
*/
|
||||
getShortcutNamesByKeyCode(keyCode: string): string[] | undefined {
|
||||
return this.keyMap.get(keyCode) || [];
|
||||
// Copy the list of shortcuts in case one of them unregisters itself
|
||||
// in its callback.
|
||||
return this.keyMap.get(keyCode)?.slice() || [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -225,6 +225,8 @@ export class ToolboxCategory
|
||||
*/
|
||||
protected createContainer_(): HTMLDivElement {
|
||||
const container = document.createElement('div');
|
||||
// Ensure that the category has a tab index to ensure it receives focus when
|
||||
// clicked (since clicking isn't managed by the toolbox).
|
||||
container.tabIndex = -1;
|
||||
container.id = this.getId();
|
||||
const className = this.cssConfig_['container'];
|
||||
|
||||
@@ -54,6 +54,8 @@ export class ToolboxSeparator extends ToolboxItem {
|
||||
*/
|
||||
protected createDom_(): HTMLDivElement {
|
||||
const container = document.createElement('div');
|
||||
// Ensure that the separator has a tab index to ensure it receives focus
|
||||
// when clicked (since clicking isn't managed by the toolbox).
|
||||
container.tabIndex = -1;
|
||||
container.id = this.getId();
|
||||
const className = this.cssConfig_['container'];
|
||||
|
||||
@@ -22,7 +22,10 @@ import '../events/events_toolbox_item_select.js';
|
||||
import {EventType} from '../events/type.js';
|
||||
import * as eventUtils from '../events/utils.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import type {IAutoHideable} from '../interfaces/i_autohideable.js';
|
||||
import {
|
||||
isAutoHideable,
|
||||
type IAutoHideable,
|
||||
} from '../interfaces/i_autohideable.js';
|
||||
import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js';
|
||||
import {isDeletable} from '../interfaces/i_deletable.js';
|
||||
import type {IDraggable} from '../interfaces/i_draggable.js';
|
||||
@@ -169,7 +172,7 @@ export class Toolbox
|
||||
ComponentManager.Capability.DRAG_TARGET,
|
||||
],
|
||||
});
|
||||
getFocusManager().registerTree(this);
|
||||
getFocusManager().registerTree(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +203,6 @@ export class Toolbox
|
||||
*/
|
||||
protected createContainer_(): HTMLDivElement {
|
||||
const toolboxContainer = document.createElement('div');
|
||||
toolboxContainer.tabIndex = 0;
|
||||
toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v');
|
||||
dom.addClass(toolboxContainer, 'blocklyToolbox');
|
||||
toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR');
|
||||
@@ -1142,7 +1144,16 @@ export class Toolbox
|
||||
}
|
||||
|
||||
/** See IFocusableTree.onTreeBlur. */
|
||||
onTreeBlur(_nextTree: IFocusableTree | null): void {}
|
||||
onTreeBlur(nextTree: IFocusableTree | null): void {
|
||||
// If navigating to anything other than the toolbox's flyout then clear the
|
||||
// selection so that the toolbox's flyout can automatically close.
|
||||
if (!nextTree || nextTree !== this.flyout?.getWorkspace()) {
|
||||
this.clearSelection();
|
||||
if (this.flyout && isAutoHideable(this.flyout)) {
|
||||
this.flyout.autoHide(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** CSS for Toolbox. See css.js for use. */
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
// Former goog.module ID: Blockly.WidgetDiv
|
||||
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import * as common from './common.js';
|
||||
import {Field} from './field.js';
|
||||
import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js';
|
||||
@@ -66,14 +67,23 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) {
|
||||
export function createDom() {
|
||||
const container = common.getParentContainer() || document.body;
|
||||
|
||||
if (document.querySelector('.' + containerClassName)) {
|
||||
containerDiv = document.querySelector('.' + containerClassName);
|
||||
const existingContainer = document.querySelector('div.' + containerClassName);
|
||||
if (existingContainer) {
|
||||
containerDiv = existingContainer as HTMLDivElement;
|
||||
} else {
|
||||
containerDiv = document.createElement('div') as HTMLDivElement;
|
||||
containerDiv = document.createElement('div');
|
||||
containerDiv.className = containerClassName;
|
||||
containerDiv.tabIndex = -1;
|
||||
}
|
||||
|
||||
container.appendChild(containerDiv!);
|
||||
browserEvents.conditionalBind(
|
||||
containerDiv,
|
||||
'keydown',
|
||||
null,
|
||||
common.globalShortcutHandler,
|
||||
);
|
||||
|
||||
container.appendChild(containerDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -762,8 +762,6 @@ export class WorkspaceSvg
|
||||
*/
|
||||
this.svgGroup_ = dom.createSvgElement(Svg.G, {
|
||||
'class': 'blocklyWorkspace',
|
||||
// Only the top-level workspace should be tabbable.
|
||||
'tabindex': injectionDiv ? '0' : '-1',
|
||||
'id': this.id,
|
||||
});
|
||||
if (injectionDiv) {
|
||||
@@ -849,7 +847,8 @@ export class WorkspaceSvg
|
||||
isParentWorkspace ? this.getInjectionDiv() : undefined,
|
||||
);
|
||||
|
||||
getFocusManager().registerTree(this);
|
||||
// Only the top-level and flyout workspaces should be tabbable.
|
||||
getFocusManager().registerTree(this, !!this.injectionDiv || this.isFlyout);
|
||||
|
||||
return this.svgGroup_;
|
||||
}
|
||||
@@ -2807,13 +2806,12 @@ export class WorkspaceSvg
|
||||
/** See IFocusableTree.onTreeBlur. */
|
||||
onTreeBlur(nextTree: IFocusableTree | null): void {
|
||||
// If the flyout loses focus, make sure to close it unless focus is being
|
||||
// lost to a different element on the page.
|
||||
if (nextTree && this.isFlyout && this.targetWorkspace) {
|
||||
// lost to the toolbox.
|
||||
if (this.isFlyout && this.targetWorkspace) {
|
||||
// Only hide the flyout if the flyout's workspace is losing focus and that
|
||||
// focus isn't returning to the flyout itself or the toolbox.
|
||||
const flyout = this.targetWorkspace.getFlyout();
|
||||
const toolbox = this.targetWorkspace.getToolbox();
|
||||
if (flyout && nextTree === flyout) return;
|
||||
if (toolbox && nextTree === toolbox) return;
|
||||
if (toolbox) toolbox.clearSelection();
|
||||
if (flyout && isAutoHideable(flyout)) flyout.autoHide(false);
|
||||
|
||||
477
package-lock.json
generated
477
package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blockly/block-test": "^6.0.4",
|
||||
"@blockly/dev-tools": "^8.0.6",
|
||||
"@blockly/dev-tools": "^9.0.0",
|
||||
"@blockly/theme-modern": "^6.0.3",
|
||||
"@hyperjump/browser": "^1.1.4",
|
||||
"@hyperjump/json-schema": "^1.5.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"http-server": "^14.0.0",
|
||||
"json5": "^2.2.0",
|
||||
"markdown-tables-to-json": "^0.1.7",
|
||||
"mocha": "^10.0.0",
|
||||
"mocha": "^11.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
@@ -101,16 +101,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools": {
|
||||
"version": "8.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-8.0.12.tgz",
|
||||
"integrity": "sha512-jE0y/Z7ggmM2JS4l0Xf2ic3eecuM+ZDjUZNCcM2k6yy0VDJoxOPN63Cq2soswXQRuKHfzRMHY48rCvoKL3MqPA==",
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.0.tgz",
|
||||
"integrity": "sha512-c2JJbj5Q9mGdy0iUvE5OBOl1zmSMJrSokORgnmrhxGCiJ6QexPGCsi1QAn6uzpUtGKjhpnEAQ6+jX7ROZe7QQg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@blockly/block-test": "^6.0.11",
|
||||
"@blockly/theme-dark": "^7.0.10",
|
||||
"@blockly/theme-deuteranopia": "^6.0.10",
|
||||
"@blockly/theme-highcontrast": "^6.0.10",
|
||||
"@blockly/theme-tritanopia": "^6.0.10",
|
||||
"@blockly/block-test": "^7.0.0",
|
||||
"@blockly/theme-dark": "^8.0.0",
|
||||
"@blockly/theme-deuteranopia": "^7.0.0",
|
||||
"@blockly/theme-highcontrast": "^7.0.0",
|
||||
"@blockly/theme-tritanopia": "^7.0.0",
|
||||
"chai": "^4.2.0",
|
||||
"dat.gui": "^0.7.7",
|
||||
"lodash.assign": "^4.2.0",
|
||||
@@ -122,7 +123,20 @@
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools/node_modules/@blockly/block-test": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.0.tgz",
|
||||
"integrity": "sha512-Y+Iwg1hHmOaqXveTOiZNXHH+jNBP+LC5L8ZxKKWeO8aB9DZD5G2hgApHfLaxeZzqnCl8zspvGnrrlFy9foEdWw==",
|
||||
"dev": true,
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/dev-tools/node_modules/assertion-error": {
|
||||
@@ -195,39 +209,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-dark": {
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-7.0.10.tgz",
|
||||
"integrity": "sha512-Wc6n115vt9alxzPkEwYtvBBGoPUV3gaYE00dvSKhqXTNoy1Xioujj9kT9VkGmdMO2mhgnJNczSpvxG8tcd4zLQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.0.tgz",
|
||||
"integrity": "sha512-Fq8ifjCwbJW305Su7SNBP8jXs4h1hp2EdQ9cMGOCr/racRIYfDRRBqjy0ZRLLqI7BsgZKxKy6Aa+OjgWEKeKfw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-deuteranopia": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-6.0.10.tgz",
|
||||
"integrity": "sha512-im5nIvf/Z0f1vJ9DK5Euu6URfY8G44xeFsat2b7TySF0BfAUWkGsagK3C6D5NatigPxKZqz3exC9zeXEtprAcg==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.0.tgz",
|
||||
"integrity": "sha512-zKhlnD/AF3MR9+Rlwus3vAPq8gwCZaZ08VEupvz5b98mk36suRlIrQanM8HVLGcozxiEvUNrTNOGO5kj8PeTWA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-highcontrast": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-6.0.10.tgz",
|
||||
"integrity": "sha512-s1hehl/b50IhebCs20hm2hFWbUTqJ2YSGdR0gnp2NLfNNRWwyZHZk+q4aG3k4L0YBWjNfE3XiRCkDISy83dBIA==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.0.tgz",
|
||||
"integrity": "sha512-6Apkw5iUlOq1DoOJgwsfo8Iha2OkxXMSNHqb8ZVVmUhCHjce0XMXgq1Rqty/2l/C2AKB+WWLZEWxOyGWYrQViQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-modern": {
|
||||
@@ -243,15 +260,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@blockly/theme-tritanopia": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-6.0.10.tgz",
|
||||
"integrity": "sha512-QNIvUHokGMLnCWUzERRZa6sSkD5RIUynWDI+KNurBH21NeWnSNScQiNu0dS/w5MSkZ/Iqqbi79UZoF49SzEayg==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.0.tgz",
|
||||
"integrity": "sha512-22TFAuY8ilKsQomDC8GXMHsCfdR8l75yPPFl6AOCcok2FJLkiyhjGpAy2cNexA9P2xP/rW7vdsG3wC8ukWihUA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"blockly": "^11.0.0"
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
@@ -379,16 +397,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
@@ -1222,9 +1244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.0.tgz",
|
||||
"integrity": "sha512-HdHF4rny4JCvIcm7V1dpvpctIGqM3/Me255CB44vW7hDG1zYMmcBMjpNqZEDxdCfXGLkx5kP0+Jz5DUS+ukqtA==",
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz",
|
||||
"integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1244,9 +1266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -1523,21 +1545,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz",
|
||||
"integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.31.1",
|
||||
"@typescript-eslint/type-utils": "8.31.1",
|
||||
"@typescript-eslint/utils": "8.31.1",
|
||||
"@typescript-eslint/visitor-keys": "8.31.1",
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/type-utils": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1552,17 +1574,27 @@
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
|
||||
"integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz",
|
||||
"integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
|
||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.31.1",
|
||||
"@typescript-eslint/types": "8.31.1",
|
||||
"@typescript-eslint/typescript-estree": "8.31.1",
|
||||
"@typescript-eslint/visitor-keys": "8.31.1",
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1578,14 +1610,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz",
|
||||
"integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
|
||||
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.31.1",
|
||||
"@typescript-eslint/visitor-keys": "8.31.1"
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1596,16 +1628,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz",
|
||||
"integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
|
||||
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.31.1",
|
||||
"@typescript-eslint/utils": "8.31.1",
|
||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1620,9 +1652,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz",
|
||||
"integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
|
||||
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1634,20 +1666,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz",
|
||||
"integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
|
||||
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.31.1",
|
||||
"@typescript-eslint/visitor-keys": "8.31.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.0.1"
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1687,9 +1719,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -1700,16 +1732,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz",
|
||||
"integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
|
||||
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.31.1",
|
||||
"@typescript-eslint/types": "8.31.1",
|
||||
"@typescript-eslint/typescript-estree": "8.31.1"
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"@typescript-eslint/typescript-estree": "8.32.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1724,13 +1756,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz",
|
||||
"integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
|
||||
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.31.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1755,15 +1787,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/config": {
|
||||
"version": "9.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.12.5.tgz",
|
||||
"integrity": "sha512-T4pOgY7FLj0+SBc58n81JZidCJKfqaSb9Ql9lOd38tmorEwTKjcPAzQQY1Ftzqv49kjBHvXdlupy685VVKNepA==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.14.0.tgz",
|
||||
"integrity": "sha512-mW6VAXfUgd2j+8YJfFWvg8Ba/7g1Brr6/+MFBpp5rTQsw/2bN3PBJsQbWpNl99OCgoS8vgc5Ykps5ZUEeffSVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@wdio/logger": "9.4.4",
|
||||
"@wdio/types": "9.12.3",
|
||||
"@wdio/utils": "9.12.5",
|
||||
"@wdio/types": "9.14.0",
|
||||
"@wdio/utils": "9.14.0",
|
||||
"deepmerge-ts": "^7.0.3",
|
||||
"glob": "^10.2.2",
|
||||
"import-meta-resolve": "^4.0.0"
|
||||
@@ -1905,9 +1937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/protocols": {
|
||||
"version": "9.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.12.5.tgz",
|
||||
"integrity": "sha512-i+yc0EZtZOh5fFuwHxvcnXeTXk2ZjFICRbcAxTNE0F2Jr4uOydvcAOw4EIIRmb9NWUSPf/bGZAA+4SEXmxmjUA==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.14.0.tgz",
|
||||
"integrity": "sha512-inJR+G8iiFrk8/JPMfxpy6wA7rvMIZFV0T8vDN1Io7sGGj+EXX7ujpDxoCns53qxV4RytnSlgHRcCaASPFcecQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1925,9 +1957,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/types": {
|
||||
"version": "9.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.12.3.tgz",
|
||||
"integrity": "sha512-MlnQ3WG1CQAjmUmeKtv3timGR91hSsCwQW9T1kqpu0VaJ/qbw3sWgtArMqRvgWB2H6IGueqQwDQ9qHlP013w9Q==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.14.0.tgz",
|
||||
"integrity": "sha512-Zqc4sxaQLIXdI1EHItIuVIOn7LvPmDvl9JEANwiJ35ck82Xlj+X55Gd9NtELSwChzKgODD0OBzlLgXyxTr69KA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1938,15 +1970,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/utils": {
|
||||
"version": "9.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.12.5.tgz",
|
||||
"integrity": "sha512-yddJj7VyA3kGuAuDU63ZdRBK4D1jwSU+52KwlZtOeqDdT/i6KAwRVYNYMwwmsGuM4GpY3q5h944YylBQNkKkjQ==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.14.0.tgz",
|
||||
"integrity": "sha512-oJapwraSflOe0CmeF3TBocdt983hq9mCutLCfie4QmE+TKRlCsZz4iidG1NRAZPGdKB32nfHtyQlW0Dfxwn6RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^2.2.0",
|
||||
"@wdio/logger": "9.4.4",
|
||||
"@wdio/types": "9.12.3",
|
||||
"@wdio/types": "9.14.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^7.0.3",
|
||||
"edgedriver": "^6.1.1",
|
||||
@@ -1969,9 +2001,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.7.60",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.60.tgz",
|
||||
"integrity": "sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==",
|
||||
"version": "2.7.61",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.61.tgz",
|
||||
"integrity": "sha512-+tZvY10nkW0pJoU88XFWLBd2O9PJPvEnDhSY/jQHfIroN5W5qGfPgFHKC4lkx0+9Vw/0IAkNHf1XBVInBkM9Vw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -2105,15 +2137,6 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-gray": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
|
||||
@@ -2637,9 +2660,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
|
||||
"integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
|
||||
"integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
@@ -5305,9 +5328,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
|
||||
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
|
||||
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -7275,30 +7298,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz",
|
||||
"integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.3.0.tgz",
|
||||
"integrity": "sha512-J0RLIM89xi8y6l77bgbX+03PeBRDQCOVQpnwOcCN7b8hCmbh6JvGI2ZDJ5WMoHz+IaPU+S4lvTd0j51GmBAdgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-colors": "^4.1.3",
|
||||
"browser-stdout": "^1.3.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"debug": "^4.3.5",
|
||||
"diff": "^5.2.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"find-up": "^5.0.0",
|
||||
"glob": "^8.1.0",
|
||||
"glob": "^10.4.5",
|
||||
"he": "^1.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"log-symbols": "^4.1.0",
|
||||
"minimatch": "^5.1.6",
|
||||
"ms": "^2.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"supports-color": "^8.1.1",
|
||||
"workerpool": "^6.5.1",
|
||||
"yargs": "^16.2.0",
|
||||
"yargs-parser": "^20.2.9",
|
||||
"yargs": "^17.7.2",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"yargs-unparser": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -7306,7 +7330,7 @@
|
||||
"mocha": "bin/mocha.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/brace-expansion": {
|
||||
@@ -7314,35 +7338,93 @@
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"node_modules/mocha/node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mocha/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -7350,6 +7432,44 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mocha/node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
@@ -7365,33 +7485,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz",
|
||||
@@ -7461,6 +7554,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -9759,15 +9853,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.31.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz",
|
||||
"integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==",
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
|
||||
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@typescript-eslint/utils": "8.31.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"@typescript-eslint/utils": "8.32.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -9824,10 +9918,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
|
||||
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
@@ -10127,19 +10222,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver": {
|
||||
"version": "9.12.5",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.12.5.tgz",
|
||||
"integrity": "sha512-CQCb1kDh52VtzPOIWc6XOdRz9q07LMAm9XwL+ABLSd0gueJq+GZoUTqHVX1YwVF0EQlFnw0JYJok0hxGH7m7cw==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.14.0.tgz",
|
||||
"integrity": "sha512-0mVjxafQ5GNdK4l/FVmmmXGUfLHCSBE4Ml2LG23rxgmw53CThAos6h01UgIEINonxIzgKEmwfqJioo3/frbpbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@wdio/config": "9.12.5",
|
||||
"@wdio/config": "9.14.0",
|
||||
"@wdio/logger": "9.4.4",
|
||||
"@wdio/protocols": "9.12.5",
|
||||
"@wdio/types": "9.12.3",
|
||||
"@wdio/utils": "9.12.5",
|
||||
"@wdio/protocols": "9.14.0",
|
||||
"@wdio/types": "9.14.0",
|
||||
"@wdio/utils": "9.14.0",
|
||||
"deepmerge-ts": "^7.0.3",
|
||||
"undici": "^6.20.1",
|
||||
"ws": "^8.8.0"
|
||||
@@ -10149,20 +10244,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriverio": {
|
||||
"version": "9.12.5",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.12.5.tgz",
|
||||
"integrity": "sha512-ho7gEOdPkpMlZJ5fbCX6+zAllnVdYl8X9RZ4x3tDabf3ByEzReqexaTVou8ayWmNngGjarWlXX3ov1BIdhQTLQ==",
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.14.0.tgz",
|
||||
"integrity": "sha512-GP0p6J+yjcCXF9uXW7HjB6IEh33OKmZcLTSg/W2rnVYSWgsUEYPujKSXe5I8q5a99QID7OOKNKVMfs5ANoZ2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||
"@wdio/config": "9.12.5",
|
||||
"@wdio/config": "9.14.0",
|
||||
"@wdio/logger": "9.4.4",
|
||||
"@wdio/protocols": "9.12.5",
|
||||
"@wdio/protocols": "9.14.0",
|
||||
"@wdio/repl": "9.4.4",
|
||||
"@wdio/types": "9.12.3",
|
||||
"@wdio/utils": "9.12.5",
|
||||
"@wdio/types": "9.14.0",
|
||||
"@wdio/utils": "9.14.0",
|
||||
"archiver": "^7.0.1",
|
||||
"aria-query": "^5.3.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@@ -10179,7 +10274,7 @@
|
||||
"rgb2hex": "0.2.5",
|
||||
"serialize-error": "^11.0.3",
|
||||
"urlpattern-polyfill": "^10.0.0",
|
||||
"webdriver": "9.12.5"
|
||||
"webdriver": "9.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@blockly/block-test": "^6.0.4",
|
||||
"@blockly/dev-tools": "^8.0.6",
|
||||
"@blockly/dev-tools": "^9.0.0",
|
||||
"@blockly/theme-modern": "^6.0.3",
|
||||
"@hyperjump/browser": "^1.1.4",
|
||||
"@hyperjump/json-schema": "^1.5.0",
|
||||
@@ -132,7 +132,7 @@
|
||||
"http-server": "^14.0.0",
|
||||
"json5": "^2.2.0",
|
||||
"markdown-tables-to-json": "^0.1.7",
|
||||
"mocha": "^10.0.0",
|
||||
"mocha": "^11.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
|
||||
@@ -31,7 +31,7 @@ suite('Basic block tests', function (done) {
|
||||
|
||||
test('Drag three blocks into the workspace', async function () {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await dragNthBlockFromFlyout(this.browser, 'Align', 0, 250, 50 * i);
|
||||
await dragNthBlockFromFlyout(this.browser, 'Align', 0, 50, 50);
|
||||
chai.assert.equal((await getAllBlocks(this.browser)).length, i);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,15 +126,15 @@ suite('Disabling', function () {
|
||||
this.browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
10,
|
||||
10,
|
||||
15,
|
||||
0,
|
||||
);
|
||||
const child = await dragBlockTypeFromFlyout(
|
||||
this.browser,
|
||||
'Logic',
|
||||
'logic_boolean',
|
||||
110,
|
||||
110,
|
||||
100,
|
||||
0,
|
||||
);
|
||||
await connect(this.browser, child, 'OUTPUT', parent, 'IF0');
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
@@ -152,18 +152,20 @@ suite('Disabling', function () {
|
||||
this.browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
10,
|
||||
10,
|
||||
15,
|
||||
0,
|
||||
);
|
||||
const child = await dragBlockTypeFromFlyout(
|
||||
this.browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
110,
|
||||
110,
|
||||
100,
|
||||
0,
|
||||
);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
await connect(this.browser, child, 'PREVIOUS', parent, 'DO0');
|
||||
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
await contextMenuSelect(this.browser, parent, 'Disable Block');
|
||||
|
||||
chai.assert.isTrue(await getIsDisabled(this.browser, child.id));
|
||||
@@ -178,16 +180,17 @@ suite('Disabling', function () {
|
||||
this.browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
10,
|
||||
10,
|
||||
15,
|
||||
0,
|
||||
);
|
||||
const child = await dragBlockTypeFromFlyout(
|
||||
this.browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
110,
|
||||
110,
|
||||
100,
|
||||
0,
|
||||
);
|
||||
|
||||
await connect(this.browser, child, 'PREVIOUS', parent, 'NEXT');
|
||||
|
||||
await contextMenuSelect(this.browser, parent, 'Disable Block');
|
||||
|
||||
@@ -141,7 +141,7 @@ suite('Delete blocks', function (done) {
|
||||
test('Delete block using backspace key', async function () {
|
||||
const before = (await getAllBlocks(this.browser)).length;
|
||||
// Get first print block, click to select it, and delete it using backspace key.
|
||||
await clickBlock(this.browser, this.firstBlock, {button: 1});
|
||||
await clickBlock(this.browser, this.firstBlock.id, {button: 1});
|
||||
await this.browser.keys([Key.Backspace]);
|
||||
const after = (await getAllBlocks(this.browser)).length;
|
||||
chai.assert.equal(
|
||||
@@ -154,7 +154,7 @@ suite('Delete blocks', function (done) {
|
||||
test('Delete block using delete key', async function () {
|
||||
const before = (await getAllBlocks(this.browser)).length;
|
||||
// Get first print block, click to select it, and delete it using delete key.
|
||||
await clickBlock(this.browser, this.firstBlock, {button: 1});
|
||||
await clickBlock(this.browser, this.firstBlock.id, {button: 1});
|
||||
await this.browser.keys([Key.Delete]);
|
||||
const after = (await getAllBlocks(this.browser)).length;
|
||||
chai.assert.equal(
|
||||
@@ -176,10 +176,11 @@ suite('Delete blocks', function (done) {
|
||||
);
|
||||
});
|
||||
|
||||
test('Undo block deletion', async function () {
|
||||
// TODO(#9029) enable this test once deleting a block doesn't lose focus
|
||||
test.skip('Undo block deletion', async function () {
|
||||
const before = (await getAllBlocks(this.browser)).length;
|
||||
// Get first print block, click to select it, and delete it using backspace key.
|
||||
await clickBlock(this.browser, this.firstBlock, {button: 1});
|
||||
await clickBlock(this.browser, this.firstBlock.id, {button: 1});
|
||||
await this.browser.keys([Key.Backspace]);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
// Undo
|
||||
@@ -187,8 +188,8 @@ suite('Delete blocks', function (done) {
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
const after = (await getAllBlocks(this.browser)).length;
|
||||
chai.assert.equal(
|
||||
before,
|
||||
after,
|
||||
before,
|
||||
'Expected there to be the original number of blocks after undoing a delete',
|
||||
);
|
||||
});
|
||||
@@ -196,7 +197,7 @@ suite('Delete blocks', function (done) {
|
||||
test('Redo block deletion', async function () {
|
||||
const before = (await getAllBlocks(this.browser)).length;
|
||||
// Get first print block, click to select it, and delete it using backspace key.
|
||||
await clickBlock(this.browser, this.firstBlock, {button: 1});
|
||||
await clickBlock(this.browser, this.firstBlock.id, {button: 1});
|
||||
await this.browser.keys([Key.Backspace]);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
// Undo
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
import * as chai from 'chai';
|
||||
import {Key} from 'webdriverio';
|
||||
import {
|
||||
clickBlock,
|
||||
getAllBlocks,
|
||||
getBlockElementById,
|
||||
PAUSE_TIME,
|
||||
testFileLocations,
|
||||
testSetup,
|
||||
@@ -33,18 +33,15 @@ suite('This tests loading Large Configuration and Deletion', function (done) {
|
||||
});
|
||||
|
||||
test('deleting block results in the correct number of blocks', async function () {
|
||||
const fourthRepeatDo = await getBlockElementById(
|
||||
this.browser,
|
||||
'E8bF[-r:B~cabGLP#QYd',
|
||||
);
|
||||
await fourthRepeatDo.click({x: -100, y: -40});
|
||||
await clickBlock(this.browser, 'E8bF[-r:B~cabGLP#QYd', {button: 1});
|
||||
await this.browser.keys([Key.Delete]);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
const allBlocks = await getAllBlocks(this.browser);
|
||||
chai.assert.equal(allBlocks.length, 10);
|
||||
});
|
||||
|
||||
test('undoing delete block results in the correct number of blocks', async function () {
|
||||
// TODO(#8793) Re-enable test after deleting a block updates focus correctly.
|
||||
test.skip('undoing delete block results in the correct number of blocks', async function () {
|
||||
await this.browser.keys([Key.Ctrl, 'z']);
|
||||
await this.browser.pause(PAUSE_TIME);
|
||||
const allBlocks = await getAllBlocks(this.browser);
|
||||
|
||||
@@ -34,16 +34,15 @@ async function testMutator(browser, delta) {
|
||||
browser,
|
||||
'Logic',
|
||||
'controls_if',
|
||||
delta * 50,
|
||||
delta * 150,
|
||||
50,
|
||||
);
|
||||
await openMutatorForBlock(browser, mutatorBlock);
|
||||
await browser.pause(PAUSE_TIME);
|
||||
await dragBlockFromMutatorFlyout(
|
||||
browser,
|
||||
mutatorBlock,
|
||||
'controls_if_elseif',
|
||||
delta * 50,
|
||||
delta * 150,
|
||||
50,
|
||||
);
|
||||
await browser.pause(PAUSE_TIME);
|
||||
@@ -67,8 +66,8 @@ async function testMutator(browser, delta) {
|
||||
'g:nth-child(2) > svg:nth-child(1) > g > g.blocklyBlockCanvas > ' +
|
||||
'g.blocklyDraggable',
|
||||
);
|
||||
// For some reason this needs a lot more time.
|
||||
await browser.pause(2000);
|
||||
|
||||
await browser.pause(PAUSE_TIME);
|
||||
await connect(
|
||||
browser,
|
||||
await getBlockElementById(browser, elseIfQuarkId),
|
||||
|
||||
@@ -165,28 +165,35 @@ export async function getBlockElementById(browser, id) {
|
||||
* causes problems if it has holes (e.g. statement inputs). Instead, this tries
|
||||
* to get the first text field on the block. It falls back on the block's SVG root.
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @param block The block to click, as an interactable element.
|
||||
* @param blockId The id of the block to click, as an interactable element.
|
||||
* @param clickOptions The options to pass to webdriverio's element.click function.
|
||||
* @return A Promise that resolves when the actions are completed.
|
||||
*/
|
||||
export async function clickBlock(browser, block, clickOptions) {
|
||||
export async function clickBlock(browser, blockId, clickOptions) {
|
||||
const findableId = 'clickTargetElement';
|
||||
// In the browser context, find the element that we want and give it a findable ID.
|
||||
await browser.execute(
|
||||
(blockId, newElemId) => {
|
||||
const block = Blockly.getMainWorkspace().getBlockById(blockId);
|
||||
for (const input of block.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
if (field instanceof Blockly.FieldLabel) {
|
||||
field.getSvgRoot().id = newElemId;
|
||||
return;
|
||||
// Ensure the block we want to click is within the viewport.
|
||||
Blockly.getMainWorkspace().scrollBoundsIntoView(
|
||||
block.getBoundingRectangleWithoutChildren(),
|
||||
10,
|
||||
);
|
||||
if (!block.isCollapsed()) {
|
||||
for (const input of block.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
if (field instanceof Blockly.FieldLabel) {
|
||||
field.getSvgRoot().id = newElemId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No label field found. Fall back to the block's SVG root.
|
||||
block.getSvgRoot().id = findableId;
|
||||
block.getSvgRoot().id = newElemId;
|
||||
},
|
||||
block.id,
|
||||
blockId,
|
||||
findableId,
|
||||
);
|
||||
|
||||
@@ -477,8 +484,8 @@ export async function dragBlockTypeFromFlyout(
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags the specified block type from the mutator flyout of the given block and
|
||||
* returns the root element of the block.
|
||||
* Drags the specified block type from the mutator flyout of the given block
|
||||
* and returns the root element of the block.
|
||||
*
|
||||
* @param browser The active WebdriverIO Browser object.
|
||||
* @param mutatorBlock The block with the mutator attached that we want to drag
|
||||
@@ -512,7 +519,18 @@ export async function dragBlockFromMutatorFlyout(
|
||||
);
|
||||
const flyoutBlock = await getBlockElementById(browser, id);
|
||||
await flyoutBlock.dragAndDrop({x: x, y: y});
|
||||
return await getSelectedBlockElement(browser);
|
||||
|
||||
const draggedBlockId = await browser.execute(
|
||||
(mutatorBlockId, blockType) => {
|
||||
return Blockly.getMainWorkspace()
|
||||
.getBlockById(mutatorBlockId)
|
||||
.mutator.getWorkspace()
|
||||
.getBlocksByType(blockType)[0].id;
|
||||
},
|
||||
mutatorBlock.id,
|
||||
type,
|
||||
);
|
||||
return await getBlockElementById(browser, draggedBlockId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,8 +544,9 @@ export async function dragBlockFromMutatorFlyout(
|
||||
* @return A Promise that resolves when the actions are completed.
|
||||
*/
|
||||
export async function contextMenuSelect(browser, block, itemText) {
|
||||
await clickBlock(browser, block, {button: 2});
|
||||
await clickBlock(browser, block.id, {button: 2});
|
||||
|
||||
await browser.pause(PAUSE_TIME);
|
||||
const item = await browser.$(`div=${itemText}`);
|
||||
await item.waitForExist();
|
||||
await item.click();
|
||||
|
||||
@@ -246,7 +246,7 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
assert.equal(node, this.blockA);
|
||||
assert.equal(node, this.blockA.inputList[0].connection);
|
||||
});
|
||||
});
|
||||
suite('one c-hat block', function () {
|
||||
@@ -340,7 +340,7 @@ suite('Cursor', function () {
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
const blockB = this.workspace.getBlockById('B');
|
||||
assert.equal(node, blockB);
|
||||
assert.equal(node, blockB.inputList[0].connection);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Rect} from '../../build/src/core/utils/rect.js';
|
||||
import * as style from '../../build/src/core/utils/style.js';
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
@@ -11,9 +13,32 @@ import {
|
||||
} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('DropDownDiv', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = Blockly.inject('blocklyDiv');
|
||||
this.setUpBlockWithField = function () {
|
||||
const blockJson = {
|
||||
'type': 'text',
|
||||
'id': 'block_id',
|
||||
'x': 10,
|
||||
'y': 20,
|
||||
'fields': {
|
||||
'TEXT': '',
|
||||
},
|
||||
};
|
||||
Blockly.serialization.blocks.append(blockJson, this.workspace);
|
||||
return this.workspace.getBlockById('block_id');
|
||||
};
|
||||
// The workspace needs to be visible for focus-specific tests.
|
||||
document.getElementById('blocklyDiv').style.visibility = 'visible';
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
document.getElementById('blocklyDiv').style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
suite('Positioning', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.boundsStub = sinon
|
||||
.stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo')
|
||||
.returns({
|
||||
@@ -41,9 +66,6 @@ suite('DropDownDiv', function () {
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
test('Below, in Bounds', function () {
|
||||
const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics(
|
||||
50,
|
||||
@@ -113,4 +135,324 @@ suite('DropDownDiv', function () {
|
||||
assert.isNotOk(metrics.arrowAtTop);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Keyboard Shortcuts', function () {
|
||||
setup(function () {
|
||||
this.boundsStub = sinon
|
||||
.stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo')
|
||||
.returns({
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
});
|
||||
teardown(function () {
|
||||
this.boundsStub.restore();
|
||||
});
|
||||
test('Escape dismisses DropDownDiv', function () {
|
||||
let hidden = false;
|
||||
Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, () => {
|
||||
hidden = true;
|
||||
});
|
||||
assert.isFalse(hidden);
|
||||
Blockly.DropDownDiv.getContentDiv().dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
keyCode: 27, // example values.
|
||||
}),
|
||||
);
|
||||
assert.isTrue(hidden);
|
||||
});
|
||||
});
|
||||
|
||||
suite('show()', function () {
|
||||
test('without bounds set throws error', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
|
||||
const errorMsgRegex = /Cannot read properties of null.+?/;
|
||||
assert.throws(
|
||||
() => Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false),
|
||||
errorMsgRegex,
|
||||
);
|
||||
});
|
||||
|
||||
test('with bounds set positions and shows div near specified location', function () {
|
||||
Blockly.DropDownDiv.setBoundsElement(document.body);
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
|
||||
Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false);
|
||||
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '1');
|
||||
assert.strictEqual(dropDownDivElem.style.left, '45px');
|
||||
assert.strictEqual(dropDownDivElem.style.top, '60px');
|
||||
});
|
||||
});
|
||||
|
||||
suite('showPositionedByField()', function () {
|
||||
test('shows div near field', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const fieldBounds = field.getScaledBBox();
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByField(field);
|
||||
|
||||
// The div should show below the field and centered horizontally.
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
const divWidth = style.getSize(dropDownDivElem).width;
|
||||
const expectedLeft = Math.floor(
|
||||
fieldBounds.left + fieldBounds.getWidth() / 2 - divWidth / 2,
|
||||
);
|
||||
const expectedTop = Math.floor(fieldBounds.bottom); // Should show beneath.
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '1');
|
||||
assert.strictEqual(dropDownDivElem.style.left, `${expectedLeft}px`);
|
||||
assert.strictEqual(dropDownDivElem.style.top, `${expectedTop}px`);
|
||||
});
|
||||
|
||||
test('with hide callback does not call callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByField(field, onHideCallback);
|
||||
|
||||
// Simply showing the div should never call the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 0);
|
||||
});
|
||||
|
||||
test('without managed ephemeral focus does not change focused node', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByField(field, null, null, false);
|
||||
|
||||
// Since managing ephemeral focus is disabled the current focused node shouldn't be changed.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('with managed ephemeral focus focuses drop-down div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByField(field, null, null, true);
|
||||
|
||||
// Managing ephemeral focus won't change getFocusedNode() but will change the actual element
|
||||
// with DOM focus.
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, dropDownDivElem);
|
||||
});
|
||||
});
|
||||
|
||||
suite('showPositionedByBlock()', function () {
|
||||
test('shows div near block', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
// Note that the offset must be computed before showing the div since otherwise it can move
|
||||
// slightly after the div is shown.
|
||||
const blockOffset = style.getPageOffset(block.getSvgRoot());
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByBlock(field, block);
|
||||
|
||||
// The div should show below the block and centered horizontally.
|
||||
const blockLocalBounds = block.getBoundingRectangle();
|
||||
const blockBounds = Rect.createFromPoint(
|
||||
blockOffset,
|
||||
blockLocalBounds.getWidth(),
|
||||
blockLocalBounds.getHeight(),
|
||||
);
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
const divWidth = style.getSize(dropDownDivElem).width;
|
||||
const expectedLeft = Math.floor(
|
||||
blockBounds.left + blockBounds.getWidth() / 2 - divWidth / 2,
|
||||
);
|
||||
const expectedTop = Math.floor(blockBounds.bottom); // Should show beneath.
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '1');
|
||||
assert.strictEqual(dropDownDivElem.style.left, `${expectedLeft}px`);
|
||||
assert.strictEqual(dropDownDivElem.style.top, `${expectedTop}px`);
|
||||
});
|
||||
|
||||
test('with hide callback does not call callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByBlock(field, block, onHideCallback);
|
||||
|
||||
// Simply showing the div should never call the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 0);
|
||||
});
|
||||
|
||||
test('without managed ephemeral focus does not change focused node', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByBlock(
|
||||
field,
|
||||
block,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
// Since managing ephemeral focus is disabled the current focused node shouldn't be changed.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('with managed ephemeral focus focuses drop-down div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.DropDownDiv.showPositionedByBlock(field, block, null, null, true);
|
||||
|
||||
// Managing ephemeral focus won't change getFocusedNode() but will change the actual element
|
||||
// with DOM focus.
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, dropDownDivElem);
|
||||
});
|
||||
});
|
||||
|
||||
suite('hideWithoutAnimation()', function () {
|
||||
test('when not showing drop-down div keeps opacity at 0', function () {
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '0');
|
||||
});
|
||||
|
||||
suite('for div positioned by field', function () {
|
||||
test('hides div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.DropDownDiv.showPositionedByField(field);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Technically this will trigger a CSS animation, but the property is still set to 0.
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '0');
|
||||
});
|
||||
|
||||
test('hide callback calls callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
Blockly.DropDownDiv.showPositionedByField(field, onHideCallback);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div should trigger the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 1);
|
||||
});
|
||||
|
||||
test('without ephemeral focus does not change focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.DropDownDiv.showPositionedByField(field, null, null, false);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div shouldn't change what would have already been focused.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('with ephemeral focus restores DOM focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.DropDownDiv.showPositionedByField(field, null, null, true);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div should restore focus back to the block.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
});
|
||||
|
||||
suite('for div positioned by block', function () {
|
||||
test('hides div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.DropDownDiv.showPositionedByBlock(field, block);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Technically this will trigger a CSS animation, but the property is still set to 0.
|
||||
const dropDownDivElem = document.querySelector('.blocklyDropDownDiv');
|
||||
assert.strictEqual(dropDownDivElem.style.opacity, '0');
|
||||
});
|
||||
|
||||
test('hide callback calls callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
Blockly.DropDownDiv.showPositionedByBlock(field, block, onHideCallback);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div should trigger the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 1);
|
||||
});
|
||||
|
||||
test('without ephemeral focus does not change focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.DropDownDiv.showPositionedByBlock(
|
||||
field,
|
||||
block,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div shouldn't change what would have already been focused.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('with ephemeral focus restores DOM focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.DropDownDiv.showPositionedByBlock(
|
||||
field,
|
||||
block,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
Blockly.DropDownDiv.hideWithoutAnimation();
|
||||
|
||||
// Hiding the div should restore focus back to the block.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
suite('Image Fields', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = Blockly.inject('blocklyDiv');
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
@@ -237,5 +238,114 @@ suite('Image Fields', function () {
|
||||
assert.isTrue(field.getFlipRtl());
|
||||
});
|
||||
});
|
||||
suite('isClickable', function () {
|
||||
setup(function () {
|
||||
this.onClick = function () {
|
||||
console.log('on click');
|
||||
};
|
||||
this.setUpBlockWithFieldImages = function () {
|
||||
const blockJson = {
|
||||
'type': 'text',
|
||||
'id': 'block_id',
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'fields': {
|
||||
'TEXT': '',
|
||||
},
|
||||
};
|
||||
Blockly.serialization.blocks.append(blockJson, this.workspace);
|
||||
return this.workspace.getBlockById('block_id');
|
||||
};
|
||||
this.extractFieldImage = function (block) {
|
||||
const fields = Array.from(block.getFields());
|
||||
// Sanity check (as a precondition).
|
||||
assert.strictEqual(fields.length, 3);
|
||||
const imageField = fields[0];
|
||||
// Sanity check (as a precondition).
|
||||
assert.isTrue(imageField instanceof Blockly.FieldImage);
|
||||
return imageField;
|
||||
};
|
||||
});
|
||||
|
||||
test('Unattached field without click handler returns false', function () {
|
||||
const field = new Blockly.FieldImage('src', 10, 10, null);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('Unattached field with click handler returns false', function () {
|
||||
const field = new Blockly.FieldImage('src', 10, 10, this.onClick);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached but disabled field without click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
field.setEnabled(false);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached but disabled field with click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
field.setEnabled(false);
|
||||
field.setOnClickHandler(this.onClick);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached, enabled, but not editable field without click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
block.setEditable(false);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached, enabled, but not editable field with click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
block.setEditable(false);
|
||||
field.setOnClickHandler(this.onClick);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached, enabled, editable field without click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
test('For attached, enabled, editable field with click handler returns true', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
field.setOnClickHandler(this.onClick);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isTrue(isClickable);
|
||||
});
|
||||
test('For attached, enabled, editable field with removed click handler returns false', function () {
|
||||
const block = this.setUpBlockWithFieldImages();
|
||||
const field = this.extractFieldImage(block);
|
||||
field.setOnClickHandler(this.onClick);
|
||||
field.setOnClickHandler(null);
|
||||
|
||||
const isClickable = field.isClickable();
|
||||
|
||||
assert.isFalse(isClickable);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,97 +39,76 @@
|
||||
<div id="mocha"></div>
|
||||
<div id="failureCount" style="display: none" tests_failed="unset"></div>
|
||||
<div id="failureMessages" style="display: none"></div>
|
||||
<div id="testFocusableTree1" tabindex="-1">
|
||||
<div id="testFocusableTree1">
|
||||
Focusable tree 1
|
||||
<div id="testFocusableTree1.node1" style="margin-left: 1em" tabindex="-1">
|
||||
<div id="testFocusableTree1.node1" style="margin-left: 1em">
|
||||
Tree 1 node 1
|
||||
<div
|
||||
id="testFocusableTree1.node1.child1"
|
||||
style="margin-left: 2em"
|
||||
tabindex="-1">
|
||||
<div id="testFocusableTree1.node1.child1" style="margin-left: 2em">
|
||||
Tree 1 node 1 child 1
|
||||
<div
|
||||
id="testFocusableTree1.node1.child1.unregisteredChild1"
|
||||
style="margin-left: 3em"
|
||||
tabindex="-1">
|
||||
style="margin-left: 3em">
|
||||
Tree 1 node 1 child 1 child 1 (unregistered)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="testFocusableTree1.node2" style="margin-left: 1em" tabindex="-1">
|
||||
<div id="testFocusableTree1.node2" style="margin-left: 1em">
|
||||
Tree 1 node 2
|
||||
<div
|
||||
id="testFocusableTree1.node2.unregisteredChild1"
|
||||
style="margin-left: 2em"
|
||||
tabindex="-1">
|
||||
style="margin-left: 2em">
|
||||
Tree 1 node 2 child 2 (unregistered)
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="testFocusableTree1.unregisteredChild1"
|
||||
style="margin-left: 1em"
|
||||
tabindex="-1">
|
||||
<div id="testFocusableTree1.unregisteredChild1" style="margin-left: 1em">
|
||||
Tree 1 child 1 (unregistered)
|
||||
</div>
|
||||
</div>
|
||||
<div id="testFocusableTree2" tabindex="-1">
|
||||
<div id="testFocusableTree2">
|
||||
Focusable tree 2
|
||||
<div id="testFocusableTree2.node1" style="margin-left: 1em" tabindex="-1">
|
||||
<div id="testFocusableTree2.node1" style="margin-left: 1em">
|
||||
Tree 2 node 1
|
||||
<div
|
||||
id="testFocusableNestedTree4"
|
||||
style="margin-left: 2em"
|
||||
tabindex="-1">
|
||||
<div id="testFocusableNestedTree4" style="margin-left: 2em">
|
||||
Nested tree 4
|
||||
<div
|
||||
id="testFocusableNestedTree4.node1"
|
||||
style="margin-left: 3em"
|
||||
tabindex="-1">
|
||||
<div id="testFocusableNestedTree4.node1" style="margin-left: 3em">
|
||||
Tree 4 node 1 (nested)
|
||||
<div
|
||||
id="testFocusableNestedTree4.node1.unregisteredChild1"
|
||||
style="margin-left: 4em"
|
||||
tabindex="-1">
|
||||
style="margin-left: 4em">
|
||||
Tree 4 node 1 child 1 (unregistered)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="testFocusableNestedTree5" style="margin-left: 1em" tabindex="-1">
|
||||
<div id="testFocusableNestedTree5" style="margin-left: 1em">
|
||||
Nested tree 5
|
||||
<div
|
||||
id="testFocusableNestedTree5.node1"
|
||||
style="margin-left: 2em"
|
||||
tabindex="-1">
|
||||
<div id="testFocusableNestedTree5.node1" style="margin-left: 2em">
|
||||
Tree 5 node 1 (nested)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="testUnregisteredFocusableTree3" tabindex="-1">
|
||||
<div id="testUnregisteredFocusableTree3">
|
||||
Unregistered tree 3
|
||||
<div
|
||||
id="testUnregisteredFocusableTree3.node1"
|
||||
style="margin-left: 1em"
|
||||
tabindex="-1">
|
||||
<div id="testUnregisteredFocusableTree3.node1" style="margin-left: 1em">
|
||||
Tree 3 node 1 (unregistered)
|
||||
</div>
|
||||
</div>
|
||||
<div id="testUnfocusableElement">Unfocusable element</div>
|
||||
<div id="nonTreeElementForEphemeralFocus" tabindex="-1" />
|
||||
<div id="nonTreeElementForEphemeralFocus" />
|
||||
<svg width="250" height="250">
|
||||
<g id="testFocusableGroup1" tabindex="-1">
|
||||
<g id="testFocusableGroup1.node1" tabindex="-1">
|
||||
<g id="testFocusableGroup1">
|
||||
<g id="testFocusableGroup1.node1">
|
||||
<rect x="0" y="0" width="250" height="30" fill="grey" />
|
||||
<text x="10" y="20" class="svgText">Group 1 node 1</text>
|
||||
<g id="testFocusableGroup1.node1.child1" tabindex="-1">
|
||||
<g id="testFocusableGroup1.node1.child1">
|
||||
<rect x="0" y="30" width="250" height="30" fill="lightgrey" />
|
||||
<text x="10" y="50" class="svgText">Tree 1 node 1 child 1</text>
|
||||
</g>
|
||||
</g>
|
||||
<g id="testFocusableGroup1.node2" tabindex="-1">
|
||||
<g id="testFocusableGroup1.node2">
|
||||
<rect x="0" y="60" width="250" height="30" fill="grey" />
|
||||
<text x="10" y="80" class="svgText">Group 1 node 2</text>
|
||||
<g id="testFocusableGroup1.node2.unregisteredChild1" tabindex="-1">
|
||||
<g id="testFocusableGroup1.node2.unregisteredChild1">
|
||||
<rect x="0" y="90" width="250" height="30" fill="lightgrey" />
|
||||
<text x="10" y="110" class="svgText">
|
||||
Tree 1 node 2 child 2 (unregistered)
|
||||
@@ -137,27 +116,27 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="testFocusableGroup2" tabindex="-1">
|
||||
<g id="testFocusableGroup2.node1" tabindex="-1">
|
||||
<g id="testFocusableGroup2">
|
||||
<g id="testFocusableGroup2.node1">
|
||||
<rect x="0" y="120" width="250" height="30" fill="grey" />
|
||||
<text x="10" y="140" class="svgText">Group 2 node 1</text>
|
||||
</g>
|
||||
<g id="testFocusableNestedGroup4" tabindex="-1">
|
||||
<g id="testFocusableNestedGroup4.node1" tabindex="-1">
|
||||
<g id="testFocusableNestedGroup4">
|
||||
<g id="testFocusableNestedGroup4.node1">
|
||||
<rect x="0" y="150" width="250" height="30" fill="lightgrey" />
|
||||
<text x="10" y="170" class="svgText">Group 4 node 1 (nested)</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="testUnregisteredFocusableGroup3" tabindex="-1">
|
||||
<g id="testUnregisteredFocusableGroup3.node1" tabindex="-1">
|
||||
<g id="testUnregisteredFocusableGroup3">
|
||||
<g id="testUnregisteredFocusableGroup3.node1">
|
||||
<rect x="0" y="180" width="250" height="30" fill="grey" />
|
||||
<text x="10" y="200" class="svgText">
|
||||
Tree 3 node 1 (unregistered)
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
<g id="nonTreeGroupForEphemeralFocus" tabindex="-1"></g>
|
||||
<g id="nonTreeGroupForEphemeralFocus"></g>
|
||||
</svg>
|
||||
<!-- Load mocha et al. before Blockly and the test modules so that
|
||||
we can safely import the test modules that make calls
|
||||
@@ -240,6 +219,7 @@
|
||||
import './jso_deserialization_test.js';
|
||||
import './jso_serialization_test.js';
|
||||
import './json_test.js';
|
||||
import './keyboard_navigation_controller_test.js';
|
||||
import './layering_test.js';
|
||||
import './blocks/lists_test.js';
|
||||
import './blocks/logic_ternary_test.js';
|
||||
|
||||
37
tests/mocha/keyboard_navigation_controller_test.js
Normal file
37
tests/mocha/keyboard_navigation_controller_test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('Keyboard Navigation Controller', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.keyboardNavigationController.setIsActive(false);
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
Blockly.keyboardNavigationController.setIsActive(false);
|
||||
});
|
||||
|
||||
test('Setting active keyboard navigation adds css class', function () {
|
||||
Blockly.keyboardNavigationController.setIsActive(true);
|
||||
assert.isTrue(
|
||||
document.body.classList.contains('blocklyKeyboardNavigation'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Disabling active keyboard navigation removes css class', function () {
|
||||
Blockly.keyboardNavigationController.setIsActive(false);
|
||||
assert.isFalse(
|
||||
document.body.classList.contains('blocklyKeyboardNavigation'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,20 @@ suite('Navigation', function () {
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'double_value_input',
|
||||
'message0': '%1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME1',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.navigator = this.workspace.getNavigator();
|
||||
@@ -80,6 +94,7 @@ suite('Navigation', function () {
|
||||
const statementInput3 = this.workspace.newBlock('input_statement');
|
||||
const statementInput4 = this.workspace.newBlock('input_statement');
|
||||
const fieldWithOutput = this.workspace.newBlock('field_input');
|
||||
const doubleValueInput = this.workspace.newBlock('double_value_input');
|
||||
const valueInput = this.workspace.newBlock('value_input');
|
||||
|
||||
statementInput1.nextConnection.connect(statementInput2.previousConnection);
|
||||
@@ -97,6 +112,7 @@ suite('Navigation', function () {
|
||||
statementInput4: statementInput4,
|
||||
fieldWithOutput: fieldWithOutput,
|
||||
valueInput: valueInput,
|
||||
doubleValueInput,
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
@@ -164,6 +180,30 @@ suite('Navigation', function () {
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'hidden_field',
|
||||
'message0': '%1 %2 %3',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'ONE',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'TWO',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'THREE',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'fields_and_input2',
|
||||
'message0': '%1 %2 %3 %4 bye',
|
||||
@@ -222,17 +262,116 @@ suite('Navigation', function () {
|
||||
'helpUrl': '',
|
||||
'nextStatement': null,
|
||||
},
|
||||
{
|
||||
'type': 'hidden_input',
|
||||
'message0': '%1 hi %2 %3 %4 %5 %6',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'ONE',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'TWO',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'SECOND',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'THREE',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'THIRD',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'buttons',
|
||||
'message0': 'If %1 %2 Then %3 %4 more %5 %6 %7',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_image',
|
||||
'name': 'BUTTON1',
|
||||
'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif',
|
||||
'width': 30,
|
||||
'height': 30,
|
||||
'alt': '*',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'VALUE1',
|
||||
'check': '',
|
||||
},
|
||||
{
|
||||
'type': 'field_image',
|
||||
'name': 'BUTTON2',
|
||||
'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif',
|
||||
'width': 30,
|
||||
'height': 30,
|
||||
'alt': '*',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'name': 'DUMMY1',
|
||||
'check': '',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'VALUE2',
|
||||
'check': '',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'STATEMENT1',
|
||||
'check': 'Number',
|
||||
},
|
||||
{
|
||||
'type': 'field_image',
|
||||
'name': 'BUTTON3',
|
||||
'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif',
|
||||
'width': 30,
|
||||
'height': 30,
|
||||
'alt': '*',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
]);
|
||||
const noNextConnection = this.workspace.newBlock('top_connection');
|
||||
const fieldAndInputs = this.workspace.newBlock('fields_and_input');
|
||||
const twoFields = this.workspace.newBlock('two_fields');
|
||||
const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2');
|
||||
const noPrevConnection = this.workspace.newBlock('start_block');
|
||||
const hiddenField = this.workspace.newBlock('hidden_field');
|
||||
const hiddenInput = this.workspace.newBlock('hidden_input');
|
||||
this.blocks.noNextConnection = noNextConnection;
|
||||
this.blocks.fieldAndInputs = fieldAndInputs;
|
||||
this.blocks.twoFields = twoFields;
|
||||
this.blocks.fieldAndInputs2 = fieldAndInputs2;
|
||||
this.blocks.noPrevConnection = noPrevConnection;
|
||||
this.blocks.hiddenField = hiddenField;
|
||||
this.blocks.hiddenInput = hiddenInput;
|
||||
|
||||
hiddenField.inputList[0].fieldRow[1].setVisible(false);
|
||||
hiddenInput.inputList[1].setVisible(false);
|
||||
|
||||
const dummyInput = this.workspace.newBlock('dummy_input');
|
||||
const dummyInputValue = this.workspace.newBlock('dummy_inputValue');
|
||||
@@ -245,6 +384,39 @@ suite('Navigation', function () {
|
||||
const outputNextBlock = this.workspace.newBlock('output_next');
|
||||
this.blocks.secondBlock = secondBlock;
|
||||
this.blocks.outputNextBlock = outputNextBlock;
|
||||
|
||||
const buttonBlock = this.workspace.newBlock('buttons', 'button_block');
|
||||
const buttonInput1 = this.workspace.newBlock(
|
||||
'field_input',
|
||||
'button_input1',
|
||||
);
|
||||
const buttonInput2 = this.workspace.newBlock(
|
||||
'field_input',
|
||||
'button_input2',
|
||||
);
|
||||
const buttonNext = this.workspace.newBlock(
|
||||
'input_statement',
|
||||
'button_next',
|
||||
);
|
||||
buttonBlock.inputList[0].connection.connect(
|
||||
buttonInput1.outputConnection,
|
||||
);
|
||||
buttonBlock.inputList[2].connection.connect(
|
||||
buttonInput2.outputConnection,
|
||||
);
|
||||
buttonBlock.nextConnection.connect(buttonNext.previousConnection);
|
||||
// Make buttons by adding a click handler
|
||||
const clickHandler = function () {
|
||||
return;
|
||||
};
|
||||
buttonBlock.getField('BUTTON1').setOnClickHandler(clickHandler);
|
||||
buttonBlock.getField('BUTTON2').setOnClickHandler(clickHandler);
|
||||
buttonBlock.getField('BUTTON3').setOnClickHandler(clickHandler);
|
||||
this.blocks.buttonBlock = buttonBlock;
|
||||
this.blocks.buttonInput1 = buttonInput1;
|
||||
this.blocks.buttonInput2 = buttonInput2;
|
||||
this.blocks.buttonNext = buttonNext;
|
||||
|
||||
this.workspace.cleanUp();
|
||||
});
|
||||
suite('Next', function () {
|
||||
@@ -275,16 +447,9 @@ suite('Navigation', function () {
|
||||
assert.equal(nextNode, prevConnection);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const input = this.blocks.doubleValueInput.inputList[0];
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[1].connection;
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.equal(nextNode, inputConnection);
|
||||
});
|
||||
test('fromInputToStatementInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[1];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[2].connection;
|
||||
this.blocks.doubleValueInput.inputList[1].connection;
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.equal(nextNode, inputConnection);
|
||||
});
|
||||
@@ -322,6 +487,69 @@ suite('Navigation', function () {
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('skipsHiddenField', function () {
|
||||
const field = this.blocks.hiddenField.inputList[0].fieldRow[0];
|
||||
const field2 = this.blocks.hiddenField.inputList[0].fieldRow[2];
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.equal(nextNode.name, field2.name);
|
||||
});
|
||||
test('skipsHiddenInput', function () {
|
||||
const field = this.blocks.hiddenInput.inputList[0].fieldRow[0];
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.equal(
|
||||
nextNode,
|
||||
this.blocks.hiddenInput.inputList[2].fieldRow[0],
|
||||
);
|
||||
});
|
||||
test('from icon to icon', function () {
|
||||
this.blocks.statementInput1.setCommentText('test');
|
||||
this.blocks.statementInput1.setWarningText('test');
|
||||
const icons = this.blocks.statementInput1.getIcons();
|
||||
const nextNode = this.navigator.getNextSibling(icons[0]);
|
||||
assert.equal(nextNode, icons[1]);
|
||||
});
|
||||
test('from icon to field', function () {
|
||||
this.blocks.statementInput1.setCommentText('test');
|
||||
this.blocks.statementInput1.setWarningText('test');
|
||||
const icons = this.blocks.statementInput1.getIcons();
|
||||
const nextNode = this.navigator.getNextSibling(icons[1]);
|
||||
assert.equal(
|
||||
nextNode,
|
||||
this.blocks.statementInput1.inputList[0].fieldRow[0],
|
||||
);
|
||||
});
|
||||
test('from icon to null', function () {
|
||||
this.blocks.dummyInput.setCommentText('test');
|
||||
const icons = this.blocks.dummyInput.getIcons();
|
||||
const nextNode = this.navigator.getNextSibling(icons[0]);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromBlockToFieldInNextInput', function () {
|
||||
const field = this.blocks.buttonBlock.getField('BUTTON2');
|
||||
const nextNode = this.navigator.getNextSibling(
|
||||
this.blocks.buttonInput1,
|
||||
);
|
||||
assert.equal(nextNode, field);
|
||||
});
|
||||
test('fromBlockToFieldSkippingInput', function () {
|
||||
const field = this.blocks.buttonBlock.getField('BUTTON3');
|
||||
const nextNode = this.navigator.getNextSibling(
|
||||
this.blocks.buttonInput2,
|
||||
);
|
||||
assert.equal(nextNode, field);
|
||||
});
|
||||
test('skipsChildrenOfCollapsedBlocks', function () {
|
||||
this.blocks.buttonBlock.setCollapsed(true);
|
||||
const nextNode = this.navigator.getNextSibling(this.blocks.buttonBlock);
|
||||
assert.equal(nextNode.id, this.blocks.buttonNext.id);
|
||||
});
|
||||
test('fromFieldSkipsHiddenInputs', function () {
|
||||
this.blocks.buttonBlock.inputList[2].setVisible(false);
|
||||
const fieldStart = this.blocks.buttonBlock.getField('BUTTON2');
|
||||
const fieldEnd = this.blocks.buttonBlock.getField('BUTTON3');
|
||||
const nextNode = this.navigator.getNextSibling(fieldStart);
|
||||
assert.equal(nextNode.name, fieldEnd.name);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Previous', function () {
|
||||
@@ -356,6 +584,11 @@ suite('Navigation', function () {
|
||||
assert.equal(prevNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromInputToField', function () {
|
||||
// Disconnect the block that was connected to the input we're testing,
|
||||
// because we only navigate to/from empty input connections (if they're
|
||||
// connected navigation targets the connected block, bypassing the
|
||||
// connection).
|
||||
this.blocks.fieldWithOutput.outputConnection.disconnect();
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(input.connection);
|
||||
assert.equal(prevNode, input.fieldRow[1]);
|
||||
@@ -366,9 +599,9 @@ suite('Navigation', function () {
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[2];
|
||||
const input = this.blocks.doubleValueInput.inputList[1];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[1].connection;
|
||||
this.blocks.doubleValueInput.inputList[0].connection;
|
||||
const prevNode = this.navigator.getPreviousSibling(input.connection);
|
||||
assert.equal(prevNode, inputConnection);
|
||||
});
|
||||
@@ -400,6 +633,70 @@ suite('Navigation', function () {
|
||||
const prevNode = this.navigator.getPreviousSibling(field);
|
||||
assert.equal(prevNode, field2);
|
||||
});
|
||||
test('skipsHiddenField', function () {
|
||||
const field = this.blocks.hiddenField.inputList[0].fieldRow[2];
|
||||
const field2 = this.blocks.hiddenField.inputList[0].fieldRow[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(field);
|
||||
assert.equal(prevNode.name, field2.name);
|
||||
});
|
||||
test('skipsHiddenInput', function () {
|
||||
const field = this.blocks.hiddenInput.inputList[2].fieldRow[0];
|
||||
const nextNode = this.navigator.getPreviousSibling(field);
|
||||
assert.equal(
|
||||
nextNode,
|
||||
this.blocks.hiddenInput.inputList[0].fieldRow[0],
|
||||
);
|
||||
});
|
||||
test('from icon to icon', function () {
|
||||
this.blocks.statementInput1.setCommentText('test');
|
||||
this.blocks.statementInput1.setWarningText('test');
|
||||
const icons = this.blocks.statementInput1.getIcons();
|
||||
const prevNode = this.navigator.getPreviousSibling(icons[1]);
|
||||
assert.equal(prevNode, icons[0]);
|
||||
});
|
||||
test('from field to icon', function () {
|
||||
this.blocks.statementInput1.setCommentText('test');
|
||||
this.blocks.statementInput1.setWarningText('test');
|
||||
const icons = this.blocks.statementInput1.getIcons();
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.statementInput1.inputList[0].fieldRow[0],
|
||||
);
|
||||
assert.equal(prevNode, icons[1]);
|
||||
});
|
||||
test('from icon to null', function () {
|
||||
this.blocks.dummyInput.setCommentText('test');
|
||||
const icons = this.blocks.dummyInput.getIcons();
|
||||
const prevNode = this.navigator.getPreviousSibling(icons[0]);
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromBlockToFieldInSameInput', function () {
|
||||
const field = this.blocks.buttonBlock.getField('BUTTON1');
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.buttonInput1,
|
||||
);
|
||||
assert.equal(prevNode, field);
|
||||
});
|
||||
test('fromBlockToFieldInPrevInput', function () {
|
||||
const field = this.blocks.buttonBlock.getField('BUTTON2');
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.buttonInput2,
|
||||
);
|
||||
assert.equal(prevNode, field);
|
||||
});
|
||||
test('skipsChildrenOfCollapsedBlocks', function () {
|
||||
this.blocks.buttonBlock.setCollapsed(true);
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.buttonNext,
|
||||
);
|
||||
assert.equal(prevNode.id, this.blocks.buttonBlock.id);
|
||||
});
|
||||
test('fromFieldSkipsHiddenInputs', function () {
|
||||
this.blocks.buttonBlock.inputList[2].setVisible(false);
|
||||
const fieldStart = this.blocks.buttonBlock.getField('BUTTON3');
|
||||
const fieldEnd = this.blocks.buttonBlock.getField('BUTTON2');
|
||||
const nextNode = this.navigator.getPreviousSibling(fieldStart);
|
||||
assert.equal(nextNode.name, fieldEnd.name);
|
||||
});
|
||||
});
|
||||
|
||||
suite('In', function () {
|
||||
@@ -428,10 +725,10 @@ suite('Navigation', function () {
|
||||
const inNode = this.navigator.getFirstChild(input.connection);
|
||||
assert.equal(inNode, previousConnection);
|
||||
});
|
||||
test('fromBlockToField', function () {
|
||||
const field = this.blocks.valueInput.getField('NAME');
|
||||
test('fromBlockToInput', function () {
|
||||
const connection = this.blocks.valueInput.inputList[0].connection;
|
||||
const inNode = this.navigator.getFirstChild(this.blocks.valueInput);
|
||||
assert.equal(inNode, field);
|
||||
assert.equal(inNode, connection);
|
||||
});
|
||||
test('fromBlockToField', function () {
|
||||
const inNode = this.navigator.getFirstChild(
|
||||
@@ -448,7 +745,10 @@ suite('Navigation', function () {
|
||||
const inNode = this.navigator.getFirstChild(
|
||||
this.blocks.dummyInputValue,
|
||||
);
|
||||
assert.equal(inNode, null);
|
||||
assert.equal(
|
||||
inNode,
|
||||
this.blocks.dummyInputValue.inputList[1].connection,
|
||||
);
|
||||
});
|
||||
test('fromOuputToNull', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
@@ -468,6 +768,23 @@ suite('Navigation', function () {
|
||||
const inNode = this.navigator.getFirstChild(this.emptyWorkspace);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('from block to icon', function () {
|
||||
this.blocks.dummyInput.setCommentText('test');
|
||||
const icons = this.blocks.dummyInput.getIcons();
|
||||
const inNode = this.navigator.getFirstChild(this.blocks.dummyInput);
|
||||
assert.equal(inNode, icons[0]);
|
||||
});
|
||||
test('from icon to null', function () {
|
||||
this.blocks.dummyInput.setCommentText('test');
|
||||
const icons = this.blocks.dummyInput.getIcons();
|
||||
const inNode = this.navigator.getFirstChild(icons[0]);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('skipsChildrenOfCollapsedBlocks', function () {
|
||||
this.blocks.buttonBlock.setCollapsed(true);
|
||||
const inNode = this.navigator.getFirstChild(this.blocks.buttonBlock);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Out', function () {
|
||||
@@ -487,13 +804,10 @@ suite('Navigation', function () {
|
||||
const outNode = this.navigator.getParent(input.connection);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromOutputToInput', function () {
|
||||
test('fromOutputToBlock', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const outNode = this.navigator.getParent(output);
|
||||
assert.equal(
|
||||
outNode,
|
||||
this.blocks.statementInput1.inputList[0].connection,
|
||||
);
|
||||
assert.equal(outNode, this.blocks.fieldWithOutput);
|
||||
});
|
||||
test('fromOutputToBlock', function () {
|
||||
const output = this.blocks.fieldWithOutput2.outputConnection;
|
||||
@@ -505,43 +819,29 @@ suite('Navigation', function () {
|
||||
const outNode = this.navigator.getParent(field);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromPreviousToInput', function () {
|
||||
const previous = this.blocks.statementInput3.previousConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const outNode = this.navigator.getParent(previous);
|
||||
assert.equal(outNode, inputConnection);
|
||||
});
|
||||
test('fromPreviousToBlock', function () {
|
||||
const previous = this.blocks.statementInput2.previousConnection;
|
||||
const outNode = this.navigator.getParent(previous);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromNextToInput', function () {
|
||||
const next = this.blocks.statementInput3.nextConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, inputConnection);
|
||||
assert.equal(outNode, this.blocks.statementInput2);
|
||||
});
|
||||
test('fromNextToBlock', function () {
|
||||
const next = this.blocks.statementInput2.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
assert.equal(outNode, this.blocks.statementInput2);
|
||||
});
|
||||
test('fromNextToBlock_NoPreviousConnection', function () {
|
||||
const next = this.blocks.secondBlock.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.noPrevConnection);
|
||||
assert.equal(outNode, this.blocks.secondBlock);
|
||||
});
|
||||
/**
|
||||
* This is where there is a block with both an output connection and a
|
||||
* next connection attached to an input.
|
||||
*/
|
||||
test('fromNextToInput_OutputAndPreviousConnection', function () {
|
||||
test('fromNextToBlock_OutputAndPreviousConnection', function () {
|
||||
const next = this.blocks.outputNextBlock.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.secondBlock.inputList[0].connection);
|
||||
assert.equal(outNode, this.blocks.outputNextBlock);
|
||||
});
|
||||
test('fromBlockToWorkspace', function () {
|
||||
const outNode = this.navigator.getParent(this.blocks.statementInput2);
|
||||
@@ -565,6 +865,12 @@ suite('Navigation', function () {
|
||||
const outNode = this.navigator.getParent(this.blocks.outputNextBlock);
|
||||
assert.equal(outNode, inputConnection);
|
||||
});
|
||||
test('from icon to block', function () {
|
||||
this.blocks.dummyInput.setCommentText('test');
|
||||
const icons = this.blocks.dummyInput.getIcons();
|
||||
const outNode = this.navigator.getParent(icons[0]);
|
||||
assert.equal(outNode, this.blocks.dummyInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,9 +13,26 @@ import {
|
||||
suite('WidgetDiv', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = Blockly.inject('blocklyDiv');
|
||||
this.setUpBlockWithField = function () {
|
||||
const blockJson = {
|
||||
'type': 'text',
|
||||
'id': 'block_id',
|
||||
'x': 10,
|
||||
'y': 20,
|
||||
'fields': {
|
||||
'TEXT': '',
|
||||
},
|
||||
};
|
||||
Blockly.serialization.blocks.append(blockJson, this.workspace);
|
||||
return this.workspace.getBlockById('block_id');
|
||||
};
|
||||
// The workspace needs to be visible for focus-specific tests.
|
||||
document.getElementById('blocklyDiv').style.visibility = 'visible';
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
document.getElementById('blocklyDiv').style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
suite('positionWithAnchor', function () {
|
||||
@@ -269,4 +286,142 @@ suite('WidgetDiv', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Keyboard Shortcuts', function () {
|
||||
test('Escape dismisses WidgetDiv', function () {
|
||||
let hidden = false;
|
||||
Blockly.WidgetDiv.show(
|
||||
this,
|
||||
false,
|
||||
() => {
|
||||
hidden = true;
|
||||
},
|
||||
this.workspace,
|
||||
false,
|
||||
);
|
||||
assert.isFalse(hidden);
|
||||
Blockly.WidgetDiv.getDiv().dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
keyCode: 27, // example values.
|
||||
}),
|
||||
);
|
||||
assert.isTrue(hidden);
|
||||
});
|
||||
});
|
||||
|
||||
suite('show()', function () {
|
||||
test('shows nowhere', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
|
||||
Blockly.WidgetDiv.show(field, false, () => {});
|
||||
|
||||
// By default the div will not have a position.
|
||||
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
|
||||
assert.strictEqual(widgetDivElem.style.display, 'block');
|
||||
assert.strictEqual(widgetDivElem.style.left, '');
|
||||
assert.strictEqual(widgetDivElem.style.top, '');
|
||||
});
|
||||
|
||||
test('with hide callback does not call callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
|
||||
Blockly.WidgetDiv.show(field, false, () => {});
|
||||
|
||||
// Simply showing the div should never call the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 0);
|
||||
});
|
||||
|
||||
test('without managed ephemeral focus does not change focused node', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.WidgetDiv.show(field, false, () => {}, null, false);
|
||||
|
||||
// Since managing ephemeral focus is disabled the current focused node shouldn't be changed.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('with managed ephemeral focus focuses widget div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
|
||||
Blockly.WidgetDiv.show(field, false, () => {}, null, true);
|
||||
|
||||
// Managing ephemeral focus won't change getFocusedNode() but will change the actual element
|
||||
// with DOM focus.
|
||||
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, widgetDivElem);
|
||||
});
|
||||
});
|
||||
|
||||
suite('hide()', function () {
|
||||
test('initially keeps display empty', function () {
|
||||
Blockly.WidgetDiv.hide();
|
||||
|
||||
// The display property starts as empty and stays that way until an owner is attached.
|
||||
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
|
||||
assert.strictEqual(widgetDivElem.style.display, '');
|
||||
});
|
||||
|
||||
test('for showing div hides div', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.WidgetDiv.show(field, false, () => {});
|
||||
|
||||
Blockly.WidgetDiv.hide();
|
||||
|
||||
// Technically this will trigger a CSS animation, but the property is still set to 0.
|
||||
const widgetDivElem = document.querySelector('.blocklyWidgetDiv');
|
||||
assert.strictEqual(widgetDivElem.style.display, 'none');
|
||||
});
|
||||
|
||||
test('for showing div and hide callback calls callback', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
const onHideCallback = sinon.stub();
|
||||
Blockly.WidgetDiv.show(field, false, onHideCallback);
|
||||
|
||||
Blockly.WidgetDiv.hide();
|
||||
|
||||
// Hiding the div should trigger the hide callback.
|
||||
assert.strictEqual(onHideCallback.callCount, 1);
|
||||
});
|
||||
|
||||
test('for showing div without ephemeral focus does not change focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.WidgetDiv.show(field, false, () => {}, null, false);
|
||||
|
||||
Blockly.WidgetDiv.hide();
|
||||
|
||||
// Hiding the div shouldn't change what would have already been focused.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
|
||||
test('for showing div with ephemeral focus restores DOM focus', function () {
|
||||
const block = this.setUpBlockWithField();
|
||||
const field = Array.from(block.getFields())[0];
|
||||
Blockly.getFocusManager().focusNode(block);
|
||||
Blockly.WidgetDiv.show(field, false, () => {}, null, true);
|
||||
|
||||
Blockly.WidgetDiv.hide();
|
||||
|
||||
// Hiding the div should restore focus back to the block.
|
||||
const blockFocusableElem = block.getFocusableElement();
|
||||
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block);
|
||||
assert.strictEqual(document.activeElement, blockFocusableElem);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user