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
86 changes: 73 additions & 13 deletions plugins/field-grid-dropdown/src/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

import {
browserEvents,
common,
FieldDropdown,
ImageProperties,
MenuOption,
utils,
WorkspaceSvg,
} from 'blockly/core';
import {GridItem} from './grid_item';

Expand Down Expand Up @@ -171,24 +173,24 @@ export class Grid {

switch (e.key) {
case 'ArrowUp':
this.moveFocus(-1 * this.columns, true);
this.moveFocus(-1, true, false);
break;
case 'ArrowDown':
this.moveFocus(this.columns, true);
this.moveFocus(1, true, false);
break;
case 'ArrowLeft':
this.moveFocus(-1 * (this.rtl ? -1 : 1), true);
this.moveFocus(-1 * (this.rtl ? -1 : 1), true, true);
break;
case 'ArrowRight':
this.moveFocus(1 * (this.rtl ? -1 : 1), true);
this.moveFocus(1 * (this.rtl ? -1 : 1), true, true);
break;
case 'PageUp':
case 'Home':
this.moveFocus(0, false);
this.moveFocus(0, false, true);
break;
case 'PageDown':
case 'End':
this.moveFocus(this.items.length - 1, false);
this.moveFocus(this.items.length - 1, false, true);
break;
case 'Enter':
case 'Space':
Expand Down Expand Up @@ -223,7 +225,7 @@ export class Grid {
const targetId = gridItem.id;
const targetIndex = this.itemIndices.get(targetId);
if (targetIndex === undefined) return;
this.moveFocus(targetIndex, false);
this.moveFocus(targetIndex, false, true);
}

/**
Expand All @@ -236,29 +238,49 @@ export class Grid {
const selected = item.getValue() === value;
item.setSelected(selected);
if (selected) {
this.moveFocus(index, false);
this.moveFocus(index, false, true);
}
}
}

/**
* Moves browser focus to the grid item at the given index.
*
* @param index The index of the item to focus.
* @param movementIndex The absolute or directionally relative index of the
* item to focus.
* @param relative True to interpret the index as relative to the currently
* focused item, false to move focus to it as an absolute value.
* @param horizontal True for a left/right move. False for an up/down move.
* Vertical moves wrap to the next/previous column at column edges, but
* do not wrap around the ends of the grid.
*/
private moveFocus(index: number, relative: boolean) {
let targetIndex = index;
private moveFocus(
movementIndex: number,
relative: boolean,
horizontal: boolean,
) {
let targetIndex = movementIndex;

if (relative) {
const focusedItem = this.getFocusedItem();
if (!focusedItem) return;
targetIndex += this.indexOfItem(focusedItem);
const currentIndex = this.indexOfItem(focusedItem);

if (horizontal) {
targetIndex += currentIndex;
} else {
targetIndex = this.getVerticalTargetIndex(currentIndex, movementIndex);
}
}

const targetItem = this.itemAtIndex(targetIndex);
if (!targetItem) return;
if (!targetItem) {
const workspace = common.getMainWorkspace();
if (workspace instanceof WorkspaceSvg) {
workspace.getAudioManager().playErrorBeep();
}
return;
}

targetItem.focus();
utils.aria.setState(
Expand All @@ -268,6 +290,44 @@ export class Grid {
);
}

/**
* Returns the index to focus after a vertical move from the given index.
*
* Prefers moving within the same column. At a column edge, wraps to the
* first/last item of the adjacent column. Returns an out-of-bounds index
* when movement is blocked at the start or end of the grid.
*
* @param currentIndex The currently focused item index.
* @param direction 1 to move down, -1 to move up.
* @returns The target item index.
*/
private getVerticalTargetIndex(
currentIndex: number,
direction: number,
): number {
// First try moving to the vertically adjacent item in the same column.
const column = currentIndex % this.columns;
const row = Math.floor(currentIndex / this.columns);
const targetIndex = (row + direction) * this.columns + column;

if (this.itemAtIndex(targetIndex)) {
return targetIndex;
}

// We are past this column's edge, so wrap to the adjacent column.
const targetColumn = column + direction;
if (targetColumn < 0 || targetColumn >= this.columns) {
return targetIndex;
}

const columnItems = this.items.filter(
(item, i) => i % this.columns === targetColumn,
);
const item =
direction > 0 ? columnItems[0] : columnItems[columnItems.length - 1];
return this.indexOfItem(item);
}

/**
* Returns the index of the given item within the grid.
*
Expand Down
25 changes: 22 additions & 3 deletions plugins/field-grid-dropdown/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export class FieldGridDropdown extends Blockly.FieldDropdown {

private borderColour?: string;

/** Object representing the grid of choices show in the dropdown. */
private grid?: Grid;
/** Object representing the grid of choices shown in the dropdown. */
private grid: Grid | null = null;

protected override ariaTypeName = Blockly.Msg['ARIA_TYPE_FIELD_GRID'];

/**
* Class for an grid dropdown field.
Expand Down Expand Up @@ -133,11 +135,17 @@ export class FieldGridDropdown extends Blockly.FieldDropdown {
override recomputeAriaContext(): boolean {
const shouldCustomize = super.recomputeAriaContext();
if (!shouldCustomize) return false;
const focusableElement = this.getFocusableElement();
Blockly.utils.aria.setState(
this.getFocusableElement(),
focusableElement,
Blockly.utils.aria.State.HASPOPUP,
'grid',
);
Blockly.utils.aria.setState(
focusableElement,
Blockly.utils.aria.State.EXPANDED,
!!this.grid,
);
return true;
}

Expand Down Expand Up @@ -180,6 +188,17 @@ export class FieldGridDropdown extends Blockly.FieldDropdown {
if (selectedValue) {
this.grid.setSelectedValue(selectedValue);
}
this.recomputeAriaContext();
}

/**
* Disposes of events and DOM-references belonging to the dropdown editor.
*/
protected override dropdownDispose_() {
// Keep aria-expanded accurate on later recomputes.
this.grid = null;
super.dropdownDispose_();
this.recomputeAriaContext();
}

/**
Expand Down
Loading