Skip to content

Commit 75efba9

Browse files
authored
fix: Fix bug that prevented keyboard navigation in flyouts. (#8687)
* fix: Fix bug that prevented keyboard navigation in flyouts. * refactor: Add an `isFocusable()` method to FlyoutItem.
1 parent 80a6d85 commit 75efba9

12 files changed

Lines changed: 203 additions & 81 deletions

core/block_flyout_inflater.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as common from './common.js';
1010
import {MANUALLY_DISABLED} from './constants.js';
1111
import type {Abstract as AbstractEvent} from './events/events_abstract.js';
1212
import {EventType} from './events/type.js';
13-
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
13+
import {FlyoutItem} from './flyout_item.js';
1414
import type {IFlyout} from './interfaces/i_flyout.js';
1515
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
1616
import * as registry from './registry.js';
@@ -27,6 +27,8 @@ import * as Xml from './xml.js';
2727
const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
2828
'WORKSPACE_AT_BLOCK_CAPACITY';
2929

30+
const BLOCK_TYPE = 'block';
31+
3032
/**
3133
* Class responsible for creating blocks for flyouts.
3234
*/
@@ -51,7 +53,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
5153
* @param flyoutWorkspace The workspace to create the block on.
5254
* @returns A newly created block.
5355
*/
54-
load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
56+
load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem {
5557
this.setFlyoutWorkspace(flyoutWorkspace);
5658
this.flyout = flyoutWorkspace.targetWorkspace?.getFlyout() ?? undefined;
5759
const block = this.createBlock(state as BlockInfo, flyoutWorkspace);
@@ -70,7 +72,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
7072
block.getDescendants(false).forEach((b) => (b.isInFlyout = true));
7173
this.addBlockListeners(block);
7274

73-
return block;
75+
return new FlyoutItem(block, BLOCK_TYPE, true);
7476
}
7577

7678
/**
@@ -114,7 +116,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
114116
* @param defaultGap The default spacing for flyout items.
115117
* @returns The amount of space that should follow this block.
116118
*/
117-
gapForElement(state: object, defaultGap: number): number {
119+
gapForItem(state: object, defaultGap: number): number {
118120
const blockState = state as BlockInfo;
119121
let gap;
120122
if (blockState['gap']) {
@@ -134,9 +136,10 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
134136
/**
135137
* Disposes of the given block.
136138
*
137-
* @param element The flyout block to dispose of.
139+
* @param item The flyout block to dispose of.
138140
*/
139-
disposeElement(element: IBoundedElement): void {
141+
disposeItem(item: FlyoutItem): void {
142+
const element = item.getElement();
140143
if (!(element instanceof BlockSvg)) return;
141144
this.removeListeners(element.id);
142145
element.dispose(false, false);
@@ -257,6 +260,19 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
257260
}
258261
});
259262
}
263+
264+
/**
265+
* Returns the type of items this inflater is responsible for creating.
266+
*
267+
* @returns An identifier for the type of items this inflater creates.
268+
*/
269+
getType() {
270+
return BLOCK_TYPE;
271+
}
260272
}
261273

262-
registry.register(registry.Type.FLYOUT_INFLATER, 'block', BlockFlyoutInflater);
274+
registry.register(
275+
registry.Type.FLYOUT_INFLATER,
276+
BLOCK_TYPE,
277+
BlockFlyoutInflater,
278+
);

core/blockly.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ import {
9999
FieldVariableFromJsonConfig,
100100
FieldVariableValidator,
101101
} from './field_variable.js';
102-
import {Flyout, FlyoutItem} from './flyout_base.js';
102+
import {Flyout} from './flyout_base.js';
103103
import {FlyoutButton} from './flyout_button.js';
104104
import {HorizontalFlyout} from './flyout_horizontal.js';
105+
import {FlyoutItem} from './flyout_item.js';
105106
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
106107
import {FlyoutSeparator} from './flyout_separator.js';
107108
import {VerticalFlyout} from './flyout_vertical.js';

core/button_flyout_inflater.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
*/
66

77
import {FlyoutButton} from './flyout_button.js';
8-
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
8+
import {FlyoutItem} from './flyout_item.js';
99
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
1010
import * as registry from './registry.js';
1111
import {ButtonOrLabelInfo} from './utils/toolbox.js';
1212
import type {WorkspaceSvg} from './workspace_svg.js';
1313

14+
const BUTTON_TYPE = 'button';
15+
1416
/**
1517
* Class responsible for creating buttons for flyouts.
1618
*/
@@ -22,15 +24,16 @@ export class ButtonFlyoutInflater implements IFlyoutInflater {
2224
* @param flyoutWorkspace The workspace to create the button on.
2325
* @returns A newly created FlyoutButton.
2426
*/
25-
load(state: object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
27+
load(state: object, flyoutWorkspace: WorkspaceSvg): FlyoutItem {
2628
const button = new FlyoutButton(
2729
flyoutWorkspace,
2830
flyoutWorkspace.targetWorkspace!,
2931
state as ButtonOrLabelInfo,
3032
false,
3133
);
3234
button.show();
33-
return button;
35+
36+
return new FlyoutItem(button, BUTTON_TYPE, true);
3437
}
3538

3639
/**
@@ -40,24 +43,34 @@ export class ButtonFlyoutInflater implements IFlyoutInflater {
4043
* @param defaultGap The default spacing for flyout items.
4144
* @returns The amount of space that should follow this button.
4245
*/
43-
gapForElement(state: object, defaultGap: number): number {
46+
gapForItem(state: object, defaultGap: number): number {
4447
return defaultGap;
4548
}
4649

4750
/**
4851
* Disposes of the given button.
4952
*
50-
* @param element The flyout button to dispose of.
53+
* @param item The flyout button to dispose of.
5154
*/
52-
disposeElement(element: IBoundedElement): void {
55+
disposeItem(item: FlyoutItem): void {
56+
const element = item.getElement();
5357
if (element instanceof FlyoutButton) {
5458
element.dispose();
5559
}
5660
}
61+
62+
/**
63+
* Returns the type of items this inflater is responsible for creating.
64+
*
65+
* @returns An identifier for the type of items this inflater creates.
66+
*/
67+
getType() {
68+
return BUTTON_TYPE;
69+
}
5770
}
5871

5972
registry.register(
6073
registry.Type.FLYOUT_INFLATER,
61-
'button',
74+
BUTTON_TYPE,
6275
ButtonFlyoutInflater,
6376
);

core/flyout_base.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ import {DeleteArea} from './delete_area.js';
1818
import type {Abstract as AbstractEvent} from './events/events_abstract.js';
1919
import {EventType} from './events/type.js';
2020
import * as eventUtils from './events/utils.js';
21+
import {FlyoutItem} from './flyout_item.js';
2122
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
2223
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
2324
import {IAutoHideable} from './interfaces/i_autohideable.js';
24-
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
2525
import type {IFlyout} from './interfaces/i_flyout.js';
2626
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
2727
import type {Options} from './options.js';
2828
import * as registry from './registry.js';
2929
import * as renderManagement from './render_management.js';
3030
import {ScrollbarPair} from './scrollbar_pair.js';
31+
import {SEPARATOR_TYPE} from './separator_flyout_inflater.js';
3132
import * as blocks from './serialization/blocks.js';
3233
import {Coordinate} from './utils/coordinate.js';
3334
import * as dom from './utils/dom.js';
@@ -677,20 +678,19 @@ export abstract class Flyout
677678
const type = info['kind'].toLowerCase();
678679
const inflater = this.getInflaterForType(type);
679680
if (inflater) {
680-
const element = inflater.load(info, this.getWorkspace());
681-
contents.push({
682-
type,
683-
element,
684-
});
685-
const gap = inflater.gapForElement(info, defaultGap);
681+
contents.push(inflater.load(info, this.getWorkspace()));
682+
const gap = inflater.gapForItem(info, defaultGap);
686683
if (gap) {
687-
contents.push({
688-
type: 'sep',
689-
element: new FlyoutSeparator(
690-
gap,
691-
this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y,
684+
contents.push(
685+
new FlyoutItem(
686+
new FlyoutSeparator(
687+
gap,
688+
this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y,
689+
),
690+
SEPARATOR_TYPE,
691+
false,
692692
),
693-
});
693+
);
694694
}
695695
}
696696
}
@@ -711,9 +711,12 @@ export abstract class Flyout
711711
*/
712712
protected normalizeSeparators(contents: FlyoutItem[]): FlyoutItem[] {
713713
for (let i = contents.length - 1; i > 0; i--) {
714-
const elementType = contents[i].type.toLowerCase();
715-
const previousElementType = contents[i - 1].type.toLowerCase();
716-
if (elementType === 'sep' && previousElementType === 'sep') {
714+
const elementType = contents[i].getType().toLowerCase();
715+
const previousElementType = contents[i - 1].getType().toLowerCase();
716+
if (
717+
elementType === SEPARATOR_TYPE &&
718+
previousElementType === SEPARATOR_TYPE
719+
) {
717720
// Remove previousElement from the array, shifting the current element
718721
// forward as a result. This preserves the behavior where explicit
719722
// separator elements override the value of prior implicit (or explicit)
@@ -752,9 +755,9 @@ export abstract class Flyout
752755
* Delete elements from a previous showing of the flyout.
753756
*/
754757
private clearOldBlocks() {
755-
this.getContents().forEach((element) => {
756-
const inflater = this.getInflaterForType(element.type);
757-
inflater?.disposeElement(element.element);
758+
this.getContents().forEach((item) => {
759+
const inflater = this.getInflaterForType(item.getType());
760+
inflater?.disposeItem(item);
758761
});
759762

760763
// Clear potential variables from the previous showing.
@@ -959,11 +962,3 @@ export abstract class Flyout
959962
return null;
960963
}
961964
}
962-
963-
/**
964-
* A flyout content item.
965-
*/
966-
export interface FlyoutItem {
967-
type: string;
968-
element: IBoundedElement;
969-
}

core/flyout_horizontal.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
import * as browserEvents from './browser_events.js';
1515
import * as dropDownDiv from './dropdowndiv.js';
16-
import {Flyout, FlyoutItem} from './flyout_base.js';
16+
import {Flyout} from './flyout_base.js';
17+
import type {FlyoutItem} from './flyout_item.js';
1718
import type {Options} from './options.js';
1819
import * as registry from './registry.js';
1920
import {Scrollbar} from './scrollbar.js';
@@ -263,10 +264,10 @@ export class HorizontalFlyout extends Flyout {
263264
}
264265

265266
for (const item of contents) {
266-
const rect = item.element.getBoundingRectangle();
267+
const rect = item.getElement().getBoundingRectangle();
267268
const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX;
268-
item.element.moveBy(moveX, cursorY);
269-
cursorX += item.element.getBoundingRectangle().getWidth();
269+
item.getElement().moveBy(moveX, cursorY);
270+
cursorX += item.getElement().getBoundingRectangle().getWidth();
270271
}
271272
}
272273

@@ -336,7 +337,7 @@ export class HorizontalFlyout extends Flyout {
336337
let flyoutHeight = this.getContents().reduce((maxHeightSoFar, item) => {
337338
return Math.max(
338339
maxHeightSoFar,
339-
item.element.getBoundingRectangle().getHeight(),
340+
item.getElement().getBoundingRectangle().getHeight(),
340341
);
341342
}, 0);
342343
flyoutHeight += this.MARGIN * 1.5;

core/flyout_item.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
2+
3+
/**
4+
* Representation of an item displayed in a flyout.
5+
*/
6+
export class FlyoutItem {
7+
/**
8+
* Creates a new FlyoutItem.
9+
*
10+
* @param element The element that will be displayed in the flyout.
11+
* @param type The type of element. Should correspond to the type of the
12+
* flyout inflater that created this object.
13+
* @param focusable True if the element should be allowed to be focused by
14+
* e.g. keyboard navigation in the flyout.
15+
*/
16+
constructor(
17+
private element: IBoundedElement,
18+
private type: string,
19+
private focusable: boolean,
20+
) {}
21+
22+
/**
23+
* Returns the element displayed in the flyout.
24+
*/
25+
getElement() {
26+
return this.element;
27+
}
28+
29+
/**
30+
* Returns the type of flyout element this item represents.
31+
*/
32+
getType() {
33+
return this.type;
34+
}
35+
36+
/**
37+
* Returns whether or not the flyout element can receive focus.
38+
*/
39+
isFocusable() {
40+
return this.focusable;
41+
}
42+
}

core/flyout_vertical.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
import * as browserEvents from './browser_events.js';
1515
import * as dropDownDiv from './dropdowndiv.js';
16-
import {Flyout, FlyoutItem} from './flyout_base.js';
16+
import {Flyout} from './flyout_base.js';
17+
import type {FlyoutItem} from './flyout_item.js';
1718
import type {Options} from './options.js';
1819
import * as registry from './registry.js';
1920
import {Scrollbar} from './scrollbar.js';
@@ -229,8 +230,8 @@ export class VerticalFlyout extends Flyout {
229230
let cursorY = margin;
230231

231232
for (const item of contents) {
232-
item.element.moveBy(cursorX, cursorY);
233-
cursorY += item.element.getBoundingRectangle().getHeight();
233+
item.getElement().moveBy(cursorX, cursorY);
234+
cursorY += item.getElement().getBoundingRectangle().getHeight();
234235
}
235236
}
236237

@@ -301,7 +302,7 @@ export class VerticalFlyout extends Flyout {
301302
let flyoutWidth = this.getContents().reduce((maxWidthSoFar, item) => {
302303
return Math.max(
303304
maxWidthSoFar,
304-
item.element.getBoundingRectangle().getWidth(),
305+
item.getElement().getBoundingRectangle().getWidth(),
305306
);
306307
}, 0);
307308
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
@@ -312,13 +313,13 @@ export class VerticalFlyout extends Flyout {
312313
if (this.RTL) {
313314
// With the flyoutWidth known, right-align the flyout contents.
314315
for (const item of this.getContents()) {
315-
const oldX = item.element.getBoundingRectangle().left;
316+
const oldX = item.getElement().getBoundingRectangle().left;
316317
const newX =
317318
flyoutWidth / this.workspace_.scale -
318-
item.element.getBoundingRectangle().getWidth() -
319+
item.getElement().getBoundingRectangle().getWidth() -
319320
this.MARGIN -
320321
this.tabWidth_;
321-
item.element.moveBy(newX - oldX, 0);
322+
item.getElement().moveBy(newX - oldX, 0);
322323
}
323324
}
324325

core/interfaces/i_flyout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Former goog.module ID: Blockly.IFlyout
88

99
import type {BlockSvg} from '../block_svg.js';
10-
import {FlyoutItem} from '../flyout_base.js';
10+
import type {FlyoutItem} from '../flyout_item.js';
1111
import type {Coordinate} from '../utils/coordinate.js';
1212
import type {Svg} from '../utils/svg.js';
1313
import type {FlyoutDefinition} from '../utils/toolbox.js';

0 commit comments

Comments
 (0)