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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion core/flyout_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import * as eventUtils from './events/utils.js';
import {FlyoutItem} from './flyout_item.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.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';
import {IFocusableNode} from './interfaces/i_focusable_node.js';
import {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {Options} from './options.js';
import * as registry from './registry.js';
import * as renderManagement from './render_management.js';
Expand All @@ -43,7 +46,7 @@ import {WorkspaceSvg} from './workspace_svg.js';
*/
export abstract class Flyout
extends DeleteArea
implements IAutoHideable, IFlyout
implements IAutoHideable, IFlyout, IFocusableNode
{
/**
* Position the flyout.
Expand Down Expand Up @@ -303,6 +306,7 @@ 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(
Expand All @@ -317,6 +321,9 @@ export abstract class Flyout
this.workspace_
.getThemeManager()
.subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');

getFocusManager().registerTree(this);

return this.svgGroup_;
}

Expand Down Expand Up @@ -398,6 +405,7 @@ export abstract class Flyout
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
getFocusManager().unregisterTree(this);
}

/**
Expand Down Expand Up @@ -961,4 +969,63 @@ export abstract class Flyout

return null;
}

/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.');
return this.svgGroup_;
}

/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
return this;
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}

/** See IFocusableTree.getRootFocusableNode. */
getRootFocusableNode(): IFocusableNode {
return this;
}

/** See IFocusableTree.getRestoredFocusableNode. */
getRestoredFocusableNode(
_previousNode: IFocusableNode | null,
): IFocusableNode | null {
return null;
}

/** See IFocusableTree.getNestedTrees. */
getNestedTrees(): Array<IFocusableTree> {
return [this.workspace_];
}

/** See IFocusableTree.lookUpFocusableNode. */
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;
}

/** See IFocusableTree.onTreeFocus. */
onTreeFocus(
_node: IFocusableNode,
_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);
}
}
31 changes: 29 additions & 2 deletions core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import type {IASTNodeLocationSvg} from './blockly.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
import {idGenerator} from './utils.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
Expand All @@ -29,7 +32,11 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Class for a button or label in the flyout.
*/
export class FlyoutButton
implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement
implements
IASTNodeLocationSvg,
IBoundedElement,
IRenderedElement,
IFocusableNode
{
/** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5;
Expand Down Expand Up @@ -68,6 +75,9 @@ export class FlyoutButton
*/
cursorSvg: SVGElement | null = null;

/** The unique ID for this FlyoutButton. */
private id: string;

/**
* @param workspace The workspace in which to place this button.
* @param targetWorkspace The flyout's target workspace.
Expand Down Expand Up @@ -105,9 +115,10 @@ export class FlyoutButton
cssClass += ' ' + this.cssClass;
}

this.id = idGenerator.getNextUniqueId();
this.svgGroup = dom.createSvgElement(
Svg.G,
{'class': cssClass},
{'id': this.id, 'class': cssClass, 'tabindex': '-1'},
this.workspace.getCanvas(),
);

Expand Down Expand Up @@ -389,6 +400,22 @@ export class FlyoutButton
getSvgRoot() {
return this.svgGroup;
}

/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
return this.svgGroup;
}

/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
return this.workspace;
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
}

/** CSS for buttons and labels. See css.js for use. */
Expand Down
3 changes: 2 additions & 1 deletion core/interfaces/i_flyout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import type {Coordinate} from '../utils/coordinate.js';
import type {Svg} from '../utils/svg.js';
import type {FlyoutDefinition} from '../utils/toolbox.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import {IFocusableTree} from './i_focusable_tree.js';
import type {IRegistrable} from './i_registrable.js';

/**
* Interface for a flyout.
*/
export interface IFlyout extends IRegistrable {
export interface IFlyout extends IRegistrable, IFocusableTree {
/** Whether the flyout is laid out horizontally or not. */
horizontalLayout: boolean;

Expand Down
3 changes: 2 additions & 1 deletion core/interfaces/i_toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import type {ToolboxInfo} from '../utils/toolbox.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import type {IFlyout} from './i_flyout.js';
import type {IFocusableTree} from './i_focusable_tree.js';
import type {IRegistrable} from './i_registrable.js';
import type {IToolboxItem} from './i_toolbox_item.js';

/**
* Interface for a toolbox.
*/
export interface IToolbox extends IRegistrable {
export interface IToolbox extends IRegistrable, IFocusableTree {
/** Initializes the toolbox. */
init(): void;

Expand Down
4 changes: 3 additions & 1 deletion core/interfaces/i_toolbox_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

// Former goog.module ID: Blockly.IToolboxItem

import type {IFocusableNode} from './i_focusable_node.js';

/**
* Interface for an item in the toolbox.
*/
export interface IToolboxItem {
export interface IToolboxItem extends IFocusableNode {
/**
* Initializes the toolbox item.
* This includes creating the DOM and updating the state of any items based
Expand Down
2 changes: 2 additions & 0 deletions core/toolbox/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ export class ToolboxCategory
*/
protected createContainer_(): HTMLDivElement {
const container = document.createElement('div');
container.tabIndex = -1;
container.id = this.getId();
const className = this.cssConfig_['container'];
if (className) {
dom.addClass(container, className);
Expand Down
2 changes: 2 additions & 0 deletions core/toolbox/separator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class ToolboxSeparator extends ToolboxItem {
*/
protected createDom_(): HTMLDivElement {
const container = document.createElement('div');
container.tabIndex = -1;
container.id = this.getId();
const className = this.cssConfig_['container'];
if (className) {
dom.addClass(container, className);
Expand Down
77 changes: 75 additions & 2 deletions core/toolbox/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import {DeleteArea} from '../delete_area.js';
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 type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js';
import {isDeletable} from '../interfaces/i_deletable.js';
import type {IDraggable} from '../interfaces/i_draggable.js';
import type {IFlyout} from '../interfaces/i_flyout.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import type {IKeyboardAccessible} from '../interfaces/i_keyboard_accessible.js';
import type {ISelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js';
import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js';
Expand All @@ -51,7 +54,12 @@ import {CollapsibleToolboxCategory} from './collapsible_category.js';
*/
export class Toolbox
extends DeleteArea
implements IAutoHideable, IKeyboardAccessible, IStyleable, IToolbox
implements
IAutoHideable,
IKeyboardAccessible,
IStyleable,
IToolbox,
IFocusableNode
{
/**
* The unique ID for this component that is used to register with the
Expand Down Expand Up @@ -163,6 +171,7 @@ export class Toolbox
ComponentManager.Capability.DRAG_TARGET,
],
});
getFocusManager().registerTree(this);
}

/**
Expand All @@ -177,7 +186,6 @@ export class Toolbox
const container = this.createContainer_();

this.contentsDiv_ = this.createContentsContainer_();
this.contentsDiv_.tabIndex = 0;
aria.setRole(this.contentsDiv_, aria.Role.TREE);
container.appendChild(this.contentsDiv_);

Expand All @@ -194,6 +202,7 @@ 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');
Expand Down Expand Up @@ -1077,7 +1086,71 @@ export class Toolbox
this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);
dom.removeNode(this.HtmlDiv);
}

getFocusManager().unregisterTree(this);
}

/** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement {
if (!this.HtmlDiv) throw Error('Toolbox DOM has not yet been created.');
return this.HtmlDiv;
}

/** See IFocusableNode.getFocusableTree. */
getFocusableTree(): IFocusableTree {
return this;
}

/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}

/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}

/** See IFocusableTree.getRootFocusableNode. */
getRootFocusableNode(): IFocusableNode {
return this;
}

/** See IFocusableTree.getRestoredFocusableNode. */
getRestoredFocusableNode(
previousNode: IFocusableNode | null,
): IFocusableNode | null {
// Always try to select the first selectable toolbox item rather than the
// root of the toolbox.
if (!previousNode || previousNode === this) {
return this.getToolboxItems().find((item) => item.isSelectable()) ?? null;
}
return null;
}

/** See IFocusableTree.getNestedTrees. */
getNestedTrees(): Array<IFocusableTree> {
return [];
}

/** See IFocusableTree.lookUpFocusableNode. */
lookUpFocusableNode(id: string): IFocusableNode | null {
return this.getToolboxItemById(id) as IFocusableNode;
}

/** See IFocusableTree.onTreeFocus. */
onTreeFocus(
node: IFocusableNode,
_previousTree: IFocusableTree | null,
): void {
if (node !== this) {
// Only select the item if it isn't already selected so as to not toggle.
if (this.getSelectedItem() !== node) {
this.setSelectedItem(node as IToolboxItem);
}
} else {
this.clearSelection();
}
}

/** See IFocusableTree.onTreeBlur. */
onTreeBlur(_nextTree: IFocusableTree | null): void {}
}

/** CSS for Toolbox. See css.js for use. */
Expand Down
Loading
Loading