diff --git a/plugins/workspace-backpack/README.md b/plugins/workspace-backpack/README.md index 6d30cd9cc2..1218feb73a 100644 --- a/plugins/workspace-backpack/README.md +++ b/plugins/workspace-backpack/README.md @@ -120,27 +120,6 @@ beneficial for performance if you expect blocks stacks to be very large. Note: Currently the empty Backpack context menu is registered globally, while the others are registered per workspace. -### Blockly Languages - -We do not currently support translating the text in this plugin to different -languages. However, if you would like to support multiple languages the messages -can be translated by assigning the following properties of Blockly.Msg - -- `EMPTY_BACKPACK` (Default: "Empty") context menu - Empty the backpack. -- `REMOVE_FROM_BACKPACK` (Default: "Remove from Backpack") context menu - Remove - the selected Block from the backpack. -- `COPY_TO_BACKPACK` (Default: "Copy to Backpack") context menu - Copy the - selected Block to the backpack. -- `COPY_ALL_TO_BACKPACK` (Default: "Copy All Blocks to Backpack") Context menu - - copy all Blocks on the workspace to the backpack. -- `PASTE_ALL_FROM_BACKPACK` (Default: "Paste All Blocks from Backpack") context - menu - Paste all Blocks from the backpack to the workspace. - -```javascript -Blockly.Msg['EMPTY_BACKPACK'] = 'Opróżnij plecak'; // Polish -// Inject workspace, etc... -``` - ## API - `init`: Initializes the backpack. diff --git a/plugins/workspace-backpack/src/backpack.ts b/plugins/workspace-backpack/src/backpack.ts index 9c090cbb3a..153f32a1a3 100644 --- a/plugins/workspace-backpack/src/backpack.ts +++ b/plugins/workspace-backpack/src/backpack.ts @@ -24,7 +24,13 @@ import {Backpackable, isBackpackable} from './backpackable'; */ export class Backpack extends Blockly.DragTarget - implements Blockly.IAutoHideable, Blockly.IPositionable + implements + Blockly.IComponent, + Blockly.IContextMenu, + Blockly.IAutoHideable, + Blockly.IPositionable, + Blockly.IFocusableNode, + Blockly.IContextMenu { /** The unique id for this component. */ id = 'backpack'; @@ -132,6 +138,7 @@ export class Backpack Blockly.ComponentManager.Capability.AUTOHIDEABLE, Blockly.ComponentManager.Capability.DRAG_TARGET, Blockly.ComponentManager.Capability.POSITIONABLE, + Blockly.ComponentManager.Capability.FOCUSABLE, ], }); this.initFlyout(); @@ -219,9 +226,35 @@ export class Backpack protected createDom() { this.svgGroup_ = Blockly.utils.dom.createSvgElement( Blockly.utils.Svg.G, - {}, + { + id: Blockly.utils.idGenerator.getNextUniqueId(), + tabindex: '0', + class: 'blocklyBackpackContainer', + }, null, ); + Blockly.utils.aria.setState( + this.svgGroup_, + Blockly.utils.aria.State.LABEL, + Blockly.Msg['OPEN_BACKPACK'], + ); + Blockly.utils.aria.setRole(this.svgGroup_, Blockly.utils.aria.Role.BUTTON); + const margin = 8; + const cornerRadius = 2; + Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.RECT, + { + width: this.WIDTH_ + margin, + height: this.HEIGHT_ + margin, + class: 'blocklyFocusRing', + x: -1 * (margin / 2), + y: -1 * (margin / 2), + rx: cornerRadius, + ry: cornerRadius, + fill: 'none', + }, + this.svgGroup_, + ); const rnd = Blockly.utils.idGenerator.genUid(); const clip = Blockly.utils.dom.createSvgElement( Blockly.utils.Svg.CLIPPATH, @@ -767,21 +800,24 @@ export class Backpack * Opens the backpack flyout. */ open() { - if (!this.isOpenable()) { + if (!this.isOpenable() || !this.flyout_ || !this.svgGroup_) { return; } + Blockly.utils.aria.setState( + this.svgGroup_, + Blockly.utils.aria.State.LABEL, + Blockly.Msg['CLOSE_BACKPACK'], + ); const jsons = this.contents_.map((text) => JSON.parse(text)); - this.flyout_?.show(jsons); - // TODO: We can remove the setVisible check when updating from ^10.0.0 to - // ^11. - /* eslint-disable @typescript-eslint/no-explicit-any */ - if ( - this.workspace_.scrollbar && - (this.workspace_.scrollbar as any).setVisible - ) { - (this.workspace_.scrollbar as any).setVisible(false); + this.flyout_.show(jsons); + this.workspace_.scrollbar?.setVisible(false); + if (Blockly.keyboardNavigationController.getIsActive()) { + const flyoutWorkspace = this.flyout_.getWorkspace(); + const firstItem = flyoutWorkspace?.getNavigator().getFirstNode(); + if (firstItem) { + Blockly.getFocusManager().focusNode(firstItem); + } } - /* eslint-enable @typescript-eslint/no-explicit-any */ Blockly.Events.fire(new BackpackOpen(true, this.workspace_.id)); } @@ -800,21 +836,18 @@ export class Backpack * Closes the backpack flyout. */ close() { - if (!this.isOpen()) { + if (!this.isOpen() || !this.svgGroup_) { return; } this.flyout_?.hide(); - // TODO: We can remove the setVisible check when updating from ^10.0.0 to - // ^11. - /* eslint-disable @typescript-eslint/no-explicit-any */ - if ( - this.workspace_.scrollbar && - (this.workspace_.scrollbar as any).setVisible - ) { - (this.workspace_.scrollbar as any).setVisible(true); - } - /* eslint-enable @typescript-eslint/no-explicit-any */ + this.workspace_.scrollbar?.setVisible(true); + Blockly.Events.fire(new BackpackOpen(false, this.workspace_.id)); + Blockly.utils.aria.setState( + this.svgGroup_, + Blockly.utils.aria.State.LABEL, + Blockly.Msg['OPEN_BACKPACK'], + ); } /** @@ -836,11 +869,11 @@ export class Backpack * * @param e Mouse event. */ - protected onClick(e: Event) { + protected onClick(e?: Event) { if (e instanceof MouseEvent && Blockly.browserEvents.isRightButton(e)) { return; } - this.open(); + this.isOpen() ? this.close() : this.open(); const uiEvent = new (Blockly.Events.get(Blockly.Events.CLICK))( null, this.workspace_.id, @@ -923,18 +956,76 @@ export class Backpack * @param e A mouse down event. */ protected blockMouseDownWhenOpenable(e: Event) { - if ( - e instanceof MouseEvent && - !Blockly.browserEvents.isRightButton(e) && - this.isOpenable() - ) { - e.stopPropagation(); // Don't start a workspace scroll. + if (e instanceof MouseEvent) { + if (Blockly.browserEvents.isRightButton(e)) { + this.showContextMenu(e); + e.stopPropagation(); + return; + } + + if (this.isOpenable()) { + e.stopPropagation(); // Don't start a workspace scroll. + } + } + } + + getFocusableElement(): HTMLElement | SVGElement { + if (!this.svgGroup_) { + throw new Error('Attempted to focus an uninitialized backpack'); } + + return this.svgGroup_; + } + + getFocusableTree(): Blockly.IFocusableTree { + return this.workspace_; + } + + onNodeFocus(): void {} + + onNodeBlur(): void {} + + canBeFocused(): boolean { + return true; + } + + performAction(e?: Event): void { + this.onClick(e); + } + + /** + * Show the context menu for the backpack. + * + * @param e Event that triggered the display of the context menu. + */ + showContextMenu(e: Event): void { + if (!this.options.contextMenu?.emptyBackpack) return; + + Blockly.ContextMenu.show( + e, + [ + { + text: Blockly.Msg['EMPTY_BACKPACK'], + enabled: !!this.getCount(), + callback: () => { + this.empty(); + }, + weight: 0, + id: 'empty_backpack', + }, + ], + this.workspace_.RTL, + this.workspace_, + new Blockly.utils.Coordinate( + e instanceof MouseEvent ? e.clientX : this.left_, + e instanceof MouseEvent ? e.clientY : this.top_, + ), + ); } } /** - * Base64 encoded data uri for backpack icon. + * Base64 encoded data uri for backpack icon. */ const backpackSvgDataUri = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC' + @@ -951,7 +1042,7 @@ const backpackSvgDataUri = 'YxNHYtMUg4di0xaDdoMVYxM3oiLz48L2c+PC9nPjwvc3ZnPg=='; /** - * Base64 encoded data uri for backpack icon when filled. + * Base64 encoded data uri for backpack icon when filled. */ const backpackFilledSvgDataUri = 'data:image/svg+xml;base64,PD94bWwgdmVyc2' + @@ -1003,7 +1094,7 @@ Blockly.Css.register(` .blocklyBackpack { opacity: 0.4; } -.blocklyBackpackDarken { +.blocklyBackpackContainer:focus .blocklyBackpack, .blocklyBackpackDarken { opacity: 0.6; } .blocklyBackpack:active { diff --git a/plugins/workspace-backpack/src/backpack_helpers.ts b/plugins/workspace-backpack/src/backpack_helpers.ts index 315d96fc94..84130b03f5 100644 --- a/plugins/workspace-backpack/src/backpack_helpers.ts +++ b/plugins/workspace-backpack/src/backpack_helpers.ts @@ -9,49 +9,11 @@ * @author kozbial@google.com (Monica Kozbial) */ -import './msg'; - import * as Blockly from 'blockly/core'; import {Backpack} from './backpack'; import {BackpackContextMenuOptions} from './options'; -/** - * Registers a context menu option to empty the backpack when right-clicked. - * - * @param workspace The workspace to register the context menu option on. - */ -function registerEmptyBackpack(workspace: Blockly.WorkspaceSvg) { - const prevConfigureContextMenu = workspace.configureContextMenu; - workspace.configureContextMenu = (menuOptions, e: Event) => { - const backpack = workspace - .getComponentManager() - .getComponent('backpack') as Backpack; - const backpackClientRect = backpack && backpack.getClientRect(); - if (e instanceof PointerEvent && backpackClientRect) { - if (!backpack || !backpackClientRect.contains(e.clientX, e.clientY)) { - prevConfigureContextMenu && - prevConfigureContextMenu.call(null, menuOptions, e); - return; - } - } - menuOptions.length = 0; - const backpackOptions = { - text: Blockly.Msg['EMPTY_BACKPACK'], - enabled: !!backpack.getCount(), - callback: function () { - backpack.empty(); - }, - scope: { - workspace, - }, - weight: 0, - id: 'empty_backpack', - }; - menuOptions.push(backpackOptions); - }; -} - /** * Registers a context menu option to remove a block from a backpack flyout. */ @@ -240,9 +202,6 @@ export function registerContextMenus( contextMenuOptions: BackpackContextMenuOptions, workspace: Blockly.WorkspaceSvg, ) { - if (contextMenuOptions.emptyBackpack) { - registerEmptyBackpack(workspace); - } if (contextMenuOptions.removeFromBackpack) { registerRemoveFromBackpack(); } diff --git a/plugins/workspace-backpack/src/msg.ts b/plugins/workspace-backpack/src/msg.ts deleted file mode 100644 index 54c225fefb..0000000000 --- a/plugins/workspace-backpack/src/msg.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Translatable messages used in backpack. - * @author kozbial@google.com (Monica Kozbial) - */ - -import * as Blockly from 'blockly/core'; - -// context menu - Copy all Blocks on the workspace to the backpack. -Blockly.Msg['COPY_ALL_TO_BACKPACK'] = 'Copy All Blocks to Backpack'; -// context menu - Copy the selected Block to the backpack. -Blockly.Msg['COPY_TO_BACKPACK'] = 'Copy to Backpack'; -// context menu - Empty the backpack. -Blockly.Msg['EMPTY_BACKPACK'] = 'Empty'; -// context menu - Paste all Blocks from the backpack to the workspace. -Blockly.Msg['PASTE_ALL_FROM_BACKPACK'] = 'Paste All Blocks from Backpack'; -// context menu - Remove the selected Block from the backpack. -Blockly.Msg['REMOVE_FROM_BACKPACK'] = 'Remove from Backpack';