Skip to content
Open
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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/blockly/core/utils/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export enum Role {
REGION = 'region',
/** See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/row_role. */
ROW = 'row',
/** See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/search_role. */
SEARCH = 'search',
/** See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/separator_role. */
SEPARATOR = 'separator',
/** See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role. */
Expand Down
9 changes: 8 additions & 1 deletion packages/blockly/msg/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -664,5 +664,12 @@
"FIELD_MULTILINEINPUT_NEW_LINE": "New line",
"ZOOM_TO_FIT_ARIA_LABEL": "Zoom to fit",
"MINIMAP_ARIA_LABEL": "Workspace minimap. Use the arrow keys to pan the workspace.",
"ARIA_LABEL_TRASH_EMPTY": "Trash, currently empty"
"ARIA_LABEL_TRASH_EMPTY": "Trash, currently empty",
"WORKSPACE_SEARCH_PLACEHOLDER": "Search",
"WORKSPACE_SEARCH_INPUT_LABEL": "Search workspace. Press Enter for the next match, Shift+Enter for the previous match. Press Escape to close search and focus the current match.",
"WORKSPACE_SEARCH_FIND_NEXT": "Find next",
"WORKSPACE_SEARCH_FIND_PREVIOUS": "Find previous",
"WORKSPACE_SEARCH_CLOSE": "Close search bar",
"WORKSPACE_SEARCH_NO_MATCHES": "No matching blocks",
"WORKSPACE_SEARCH_MATCH": "Match %1 of %2: %3"
}
9 changes: 8 additions & 1 deletion packages/blockly/msg/json/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,5 +669,12 @@
"FIELD_MULTILINEINPUT_NEW_LINE": "Label for the hint shown in the multiline input field editor indicating the key to insert a new line. Keep this message brief.",
"ZOOM_TO_FIT_ARIA_LABEL": "ARIA label for the zoom-to-fit button that zooms the workspace to fit all blocks.",
"MINIMAP_ARIA_LABEL": "ARIA label for the workspace minimap with instructions on keyboard use.",
"ARIA_LABEL_TRASH_EMPTY": "ARIA label for the trashcan when it contains no blocks and cannot be interacted with."
"ARIA_LABEL_TRASH_EMPTY": "ARIA label for the trashcan when it contains no blocks and cannot be interacted with.",
"WORKSPACE_SEARCH_PLACEHOLDER": "Placeholder text shown in the workspace search input.",
"WORKSPACE_SEARCH_INPUT_LABEL": "ARIA label for the workspace search text input, including keyboard usage.",
"WORKSPACE_SEARCH_FIND_NEXT": "ARIA label for the workspace search button that selects the next matching block.",
"WORKSPACE_SEARCH_FIND_PREVIOUS": "ARIA label for the workspace search button that selects the previous matching block.",
"WORKSPACE_SEARCH_CLOSE": "ARIA label for the button that closes the workspace search bar.",
"WORKSPACE_SEARCH_NO_MATCHES": "ARIA live region message announced when workspace search finds no matching blocks.",
"WORKSPACE_SEARCH_MATCH": "ARIA live region message announcing the currently highlighted workspace search match. \n\nParameters:\n* %1 - 1-based index of the current match\n* %2 - total number of matches\n* %3 - accessible label of the current block \n\nExamples:\n* 'Match 1 of 3: print, hello'"
}
25 changes: 24 additions & 1 deletion packages/blockly/msg/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2510,4 +2510,27 @@ Blockly.Msg.ZOOM_TO_FIT_ARIA_LABEL = 'Zoom to fit';
Blockly.Msg.MINIMAP_ARIA_LABEL = 'Workspace minimap. Use the arrow keys to pan the workspace.';
/** @type {string} */
/// ARIA label for the trashcan when it contains no blocks and cannot be interacted with.
Blockly.Msg.ARIA_LABEL_TRASH_EMPTY = 'Trash, currently empty';
Blockly.Msg.ARIA_LABEL_TRASH_EMPTY = 'Trash, currently empty';
/** @type {string} */
/// Placeholder text shown in the workspace search input.
Blockly.Msg.WORKSPACE_SEARCH_PLACEHOLDER = 'Search';
/** @type {string} */
/// ARIA label for the workspace search text input, including keyboard usage.
Blockly.Msg.WORKSPACE_SEARCH_INPUT_LABEL = 'Search workspace. Press Enter for the next match, Shift+Enter for the previous match. Press Escape to close search and focus the current match.';
/** @type {string} */
/// ARIA label for the workspace search button that selects the next matching block.
Blockly.Msg.WORKSPACE_SEARCH_FIND_NEXT = 'Find next';
/** @type {string} */
/// ARIA label for the workspace search button that selects the previous matching block.
Blockly.Msg.WORKSPACE_SEARCH_FIND_PREVIOUS = 'Find previous';
/** @type {string} */
/// ARIA label for the button that closes the workspace search bar.
Blockly.Msg.WORKSPACE_SEARCH_CLOSE = 'Close search bar';
/** @type {string} */
/// ARIA live region message announced when workspace search finds no matching blocks.
Blockly.Msg.WORKSPACE_SEARCH_NO_MATCHES = 'No matching blocks';
/** @type {string} */
/// ARIA live region message announcing the currently highlighted workspace search match.
/// \n\nParameters:\n* %1 - 1-based index of the current match\n* %2 - total number of matches\n* %3 - accessible label of the current block
/// \n\nExamples:\n* "Match 1 of 3: print, hello"
Blockly.Msg.WORKSPACE_SEARCH_MATCH = 'Match %1 of %2: %3';
2 changes: 1 addition & 1 deletion packages/plugins/workspace-search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ workspaceSearch.init();
<script src="./node_modules/@blockly/plugin-workspace-search/dist/index.js"></script>
```

To open workspace search use either command + f or control + f. To close the search bar hit escape or the 'x' in the top right corner.
To open workspace search use either command + f or control + f. To close the search bar, press Escape or click the 'x' in the top right corner. Escape focuses the current matching block, or the workspace if there is no match.

## API

Expand Down
1 change: 1 addition & 0 deletions packages/plugins/workspace-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"devDependencies": {
"@blockly/dev-scripts": "^13.1.0",
"@blockly/dev-tools": "^13.1.0",
"chai": "^6.2.2",
"jsdom": "^30.0.1",
"jsdom-global": "3.0.2",
"sinon": "^22.1.0",
Expand Down
72 changes: 62 additions & 10 deletions packages/plugins/workspace-search/src/workspace_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class WorkspaceSearch implements Blockly.IPositionable {
/**
* The placeholder text for the search bar input.
*/
private textInputPlaceholder = 'Search';
private textInputPlaceholder = Blockly.Msg['WORKSPACE_SEARCH_PLACEHOLDER'];

/**
* A list of blocks that came up in the search.
Expand Down Expand Up @@ -148,6 +148,7 @@ export class WorkspaceSearch implements Blockly.IPositionable {

this.htmlDiv = document.createElement('div');
Blockly.utils.dom.addClass(this.htmlDiv, 'blockly-ws-search');
Blockly.utils.aria.setRole(this.htmlDiv, Blockly.utils.aria.Role.SEARCH);

const searchContainer = document.createElement('div');
Blockly.utils.dom.addClass(searchContainer, 'blockly-ws-search-container');
Expand Down Expand Up @@ -239,8 +240,14 @@ export class WorkspaceSearch implements Blockly.IPositionable {
*/
protected createTextInput(): HTMLInputElement {
const textInput = document.createElement('input');
textInput.type = 'text';
textInput.type = 'search';
textInput.autocomplete = 'off';
textInput.setAttribute('placeholder', this.textInputPlaceholder);
Blockly.utils.aria.setState(
textInput,
Blockly.utils.aria.State.LABEL,
Blockly.Msg['WORKSPACE_SEARCH_INPUT_LABEL'],
);
return textInput;
}

Expand All @@ -250,7 +257,10 @@ export class WorkspaceSearch implements Blockly.IPositionable {
* @returns The next button.
*/
protected createNextBtn(): HTMLButtonElement {
return this.createBtn('blockly-ws-search-next-btn', 'Find next');
return this.createBtn(
'blockly-ws-search-next-btn',
Blockly.Msg['WORKSPACE_SEARCH_FIND_NEXT'],
);
}

/**
Expand All @@ -259,7 +269,10 @@ export class WorkspaceSearch implements Blockly.IPositionable {
* @returns The previous button.
*/
protected createPreviousBtn(): HTMLButtonElement {
return this.createBtn('blockly-ws-search-previous-btn', 'Find previous');
return this.createBtn(
'blockly-ws-search-previous-btn',
Blockly.Msg['WORKSPACE_SEARCH_FIND_PREVIOUS'],
);
}

/**
Expand All @@ -268,7 +281,10 @@ export class WorkspaceSearch implements Blockly.IPositionable {
* @returns A button for closing the search bar.
*/
protected createCloseBtn(): HTMLButtonElement {
return this.createBtn('blockly-ws-search-close-btn', 'Close search bar');
return this.createBtn(
'blockly-ws-search-close-btn',
Blockly.Msg['WORKSPACE_SEARCH_CLOSE'],
);
}

/**
Expand All @@ -283,7 +299,7 @@ export class WorkspaceSearch implements Blockly.IPositionable {
const btn = document.createElement('button');
Blockly.utils.dom.addClass(btn, className);
btn.type = 'button';
btn.setAttribute('aria-label', text);
Blockly.utils.aria.setState(btn, Blockly.utils.aria.State.LABEL, text);
return btn;
}

Expand Down Expand Up @@ -447,18 +463,20 @@ export class WorkspaceSearch implements Blockly.IPositionable {
this.highlightCurrentSelection(currentBlock);
this.workspace.centerOnBlock(currentBlock.id, false);
this.lastHighlighted = currentBlock;
this.announceCurrentMatch();
}

/**
* Opens the search bar.
*/
open() {
this.setVisible(true);
this.inputElement?.focus();
this.inputElement?.select();
if (this.searchText) {
this.searchAndHighlight(this.searchText);
if (this.inputElement) {
this.inputElement.value = this.searchText;
this.inputElement.focus();
this.inputElement.select();
}
this.searchAndHighlight(this.searchText);
}

/**
Expand Down Expand Up @@ -511,6 +529,9 @@ export class WorkspaceSearch implements Blockly.IPositionable {
currentIdx = currentIdx > -1 ? currentIdx : 0;
}
this.setCurrentBlock(currentIdx);
if (this.searchText && !this.blocks.length) {
this.announceNoMatches();
}
}

/**
Expand Down Expand Up @@ -600,6 +621,37 @@ export class WorkspaceSearch implements Blockly.IPositionable {
this.blocks = [];
}

/**
* Announces the currently highlighted match to screen readers.
*
* Focus stays in the search input while browsing results, so the highlighted
* block is announced via the live region rather than by moving focus.
*/
private announceCurrentMatch() {
const currentBlock = this.blocks[this.currentBlockIndex];
if (!currentBlock) {
return;
}
Blockly.utils.aria.announceDynamicAriaState(
Blockly.Msg['WORKSPACE_SEARCH_MATCH']
.replace('%1', String(this.currentBlockIndex + 1))
.replace('%2', String(this.blocks.length))
.replace(
'%3',
currentBlock.getAriaLabel(Blockly.utils.aria.Verbosity.TERSE),
),
);
}

/**
* Announces that the current search term matched no blocks.
*/
private announceNoMatches() {
Blockly.utils.aria.announceDynamicAriaState(
Blockly.Msg['WORKSPACE_SEARCH_NO_MATCHES'],
);
}

/**
* Adds "current selection" highlight to the provided block.
* Highlights the provided block as the "current selection".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* @author kozbial@google.com (Monica Kozbial)
*/

const assert = require('assert');
const Blockly = require('blockly');
const {assert} = require('chai');
const sinon = require('sinon');

const {WorkspaceSearch} = require('../src/index');
Expand Down Expand Up @@ -63,6 +63,24 @@ suite('WorkspaceSearch', function () {
}
}

suiteSetup(function () {
Blockly.defineBlocksWithJsonArray([
{
type: 'alpha_block',
message0: 'alpha',
},
{
type: 'beta_block',
message0: 'beta',
},
]);
});

suiteTeardown(function () {
delete Blockly.Blocks['alpha_block'];
delete Blockly.Blocks['beta_block'];
});

setup(function () {
this.jsdomCleanup = require('jsdom-global')(
'<!DOCTYPE html><div id="blocklyDiv"></div>',
Expand Down Expand Up @@ -119,6 +137,22 @@ suite('WorkspaceSearch', function () {
assert.equal(previousBtn.type, 'button');
assert.equal(closeBtn.type, 'button');
});

test('Buttons have accessible names', function () {
this.workspaceSearch.init();
const nextBtn = document.querySelector(
'button.blockly-ws-search-next-btn',
);
const previousBtn = document.querySelector(
'button.blockly-ws-search-previous-btn',
);
const closeBtn = document.querySelector(
'button.blockly-ws-search-close-btn',
);
assert.equal(nextBtn.getAttribute('aria-label'), 'Find next');
assert.equal(previousBtn.getAttribute('aria-label'), 'Find previous');
assert.equal(closeBtn.getAttribute('aria-label'), 'Close search bar');
});
});

suite('dispose()', function () {
Expand Down Expand Up @@ -466,19 +500,6 @@ suite('WorkspaceSearch', function () {
});

suite('focus', function () {
suiteSetup(function () {
Blockly.defineBlocksWithJsonArray([
{
type: 'alpha_block',
message0: 'alpha',
},
{
type: 'beta_block',
message0: 'beta',
},
]);
});

setup(function () {
this.alphaBlock = this.workspace.newBlock('alpha_block');
this.betaBlock = this.workspace.newBlock('beta_block');
Expand Down Expand Up @@ -530,4 +551,56 @@ suite('WorkspaceSearch', function () {
assertFocusedNodeType('beta_block');
});
});

suite('screen reader support', function () {
setup(function () {
this.alphaBlock = this.workspace.newBlock('alpha_block');
this.betaBlock = this.workspace.newBlock('beta_block');
this.workspaceSearch.init();
this.liveRegion = document.getElementById('blocklyAriaAnnounce');
});

test('search container is a search landmark', function () {
const search = document.querySelector('div.blockly-ws-search');
assert.equal(search.getAttribute('role'), 'search');
});

test('search input aria-label describes keyboard usage', function () {
const input = document.querySelector('.blockly-ws-search-input input');
const label = input.getAttribute('aria-label');
assert.include(label, 'Enter');
assert.include(label, 'Escape');
});

test('matching blocks are announced with the current match', function () {
this.workspaceSearch.searchAndHighlight('a');
this.clock.tick(11);

assert.include(this.liveRegion.textContent, 'Match 1 of 2');
assert.include(this.liveRegion.textContent, 'alpha');

this.workspaceSearch.next();
this.clock.tick(11);

assert.include(this.liveRegion.textContent, 'Match 2 of 2');
assert.include(this.liveRegion.textContent, 'beta');
});

test('no matches are announced', function () {
this.workspaceSearch.searchAndHighlight('c');
this.clock.tick(11);

assert.include(this.liveRegion.textContent, 'No matching blocks');
});

test('reopen restores the query and highlights matches', function () {
this.workspaceSearch.searchAndHighlight('alpha');
this.workspaceSearch.close();
this.workspaceSearch.open();

const input = document.querySelector('.blockly-ws-search-input input');
assert.equal(input.value, 'alpha');
assert.isTrue(isBlockHighlighted(this.alphaBlock));
});
});
});