Skip to content

Commit a758e32

Browse files
authored
feat: Allow passing HTML elements to display in toasts. (#588)
* feat: Allow passing HTML elements to display in toasts. * chore: Satisfy the linter and clarify comments. * chore: Add a test for HTML toasts.
1 parent 29e4b03 commit a758e32

File tree

3 files changed

+95
-0
lines changed

3 files changed

+95
-0
lines changed

src/html_toast.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as Blockly from 'blockly/core';
8+
9+
/**
10+
* Configuration options for toasts.
11+
*/
12+
interface HtmlToastOptions extends Blockly.ToastOptions {
13+
element?: HTMLElement;
14+
}
15+
16+
/**
17+
* Custom toast implementation that supports HTML elements in toast messages.
18+
*
19+
* After registering, call
20+
`Blockly.dialog.toast(workspace, {element: <html element>, message: <text>});`
21+
* to display an HTML-based toast.
22+
*/
23+
class HtmlToast extends Blockly.Toast {
24+
/**
25+
* Creates the body of the toast for display.
26+
*
27+
* @param workspace The workspace the toast will be displayed on.
28+
* @param options Configuration options for toast appearance/behavior.
29+
* @returns The body for the toast.
30+
*/
31+
protected static override createDom(
32+
workspace: Blockly.WorkspaceSvg,
33+
options: HtmlToastOptions,
34+
) {
35+
const dom = super.createDom(workspace, options);
36+
const contents = dom.querySelector('div');
37+
if (
38+
contents &&
39+
'element' in options &&
40+
options.element instanceof HTMLElement
41+
) {
42+
contents.innerHTML = '';
43+
contents.appendChild(options.element);
44+
}
45+
return dom;
46+
}
47+
}
48+
49+
/**
50+
* Registers HtmlToast as the default toast implementation for Blockly.
51+
*/
52+
export function registerHtmlToast() {
53+
Blockly.dialog.setToast(HtmlToast.show.bind(HtmlToast));
54+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as Blockly from 'blockly/core';
88
import {NavigationController} from './navigation_controller';
99
import {enableBlocksOnDrag} from './disabled_blocks';
10+
import {registerHtmlToast} from './html_toast';
1011

1112
/** Plugin for keyboard navigation. */
1213
export class KeyboardNavigation {
@@ -82,6 +83,8 @@ export class KeyboardNavigation {
8283
});
8384
workspace.getSvgGroup().appendChild(this.workspaceFocusRing);
8485
this.resizeWorkspaceRings();
86+
87+
registerHtmlToast();
8588
}
8689

8790
private resizeWorkspaceRings() {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as chai from 'chai';
8+
import * as Blockly from 'blockly/core';
9+
import {PAUSE_TIME, testFileLocations, testSetup} from './test_setup.js';
10+
11+
suite('HTML toasts', function () {
12+
setup(async function () {
13+
this.browser = await testSetup(testFileLocations.BASE);
14+
await this.browser.pause(PAUSE_TIME);
15+
});
16+
17+
test('Can be displayed', async function () {
18+
const equal = await this.browser.execute(() => {
19+
const element = document.createElement('div');
20+
element.id = 'testToast';
21+
element.innerHTML = 'This is a <b>test</b>';
22+
23+
const options = {
24+
element,
25+
message: 'Placeholder',
26+
};
27+
Blockly.dialog.toast(
28+
Blockly.getMainWorkspace() as Blockly.WorkspaceSvg,
29+
options,
30+
);
31+
32+
// Ensure that the element displayed in the toast is the one we specified.
33+
return document.querySelector('.blocklyToast #testToast') === element;
34+
});
35+
36+
chai.assert.isTrue(equal);
37+
});
38+
});

0 commit comments

Comments
 (0)