Skip to content

Commit 083329a

Browse files
authored
feat: Add support for conditional ephemeral focus. (#9051)
## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Needed for fixing RaspberryPiFoundation/blockly-samples#2514 and RaspberryPiFoundation/blockly-samples#2515. ### Proposed Changes Update `FieldInput` along with drop-down and widget divs to support disabling the automatic ephemeral focus functionality. ### Reason for Changes As mentioned in RaspberryPiFoundation/blockly-samples#2514 (comment) both RaspberryPiFoundation/blockly-samples#2514 and RaspberryPiFoundation/blockly-samples#2515 were caused by the custom fields leading to both drop-down and widget divs simultaneously taking ephemeral focus (and that's not currently allowed by `FocusManager`). This change updates both widget and drop-down divs with _optional_ parameters to conditionally disable automatic ephemeral focus so that `FieldInput` can, in turn, be customized with disabling automatic ephemeral focus for its inline editor. Being able to disable ephemeral focus for `FieldInput` allows the custom fields' own drop-down divs to take and manage ephemeral focus, instead, avoiding the duplicate scenario that led to the runtime failure. Note that the drop-down div change in this PR is completely optional, but it's added for consistency and to avoid future scenarios of breakage when trying to use both divs together (as a fix is required in Core without monkeypatching). It's worth noting that there could be a possibility for a more 'proper' fix in `FocusManager` by allowing multiple calls to `takeEphemeralFocus`, but it's unclear exactly how to solve this consistently (which is why it results in a hard failure today). The main issue is that the current focused node can change from underneath the manager (due to DOM focus changes), and the current focused element may also change. It's not clear if the first, last, or some other call to `takeEphemeralFocus` should take precedent or which node to return focus once ephemeral focus ends (in cases with multiple subsequent calls). ### Test Coverage No new tests were added, though common field cases were tested manually in core's simple playground and in the plugin-specific playgrounds (per the original regressions). The keyboard navigation plugin test environment was also verified to ensure that this didn't alter any existing behavior (it should be a no-op except for the two custom field plugins). Automated tests would be nice to add for all three classes, perhaps as part of #8915. ### Documentation The code documentation changes here should be sufficient. ### Additional Information These changes are being done directly to ease solving the above samples issues. See RaspberryPiFoundation/blockly-samples#2521 for the follow-up fixes to samples.
1 parent 4a2b743 commit 083329a

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

core/dropdowndiv.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,19 @@ function getScaledBboxOfField(field: Field): Rect {
268268
* @param field The field to position the dropdown against.
269269
* @param opt_onHide Optional callback for when the drop-down is hidden.
270270
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
271+
* @param manageEphemeralFocus Whether ephemeral focus should be managed
272+
* according to the drop-down div's lifetime. Note that if a false value is
273+
* passed in here then callers should manage ephemeral focus directly
274+
* otherwise focus may not properly restore when the widget closes. Defaults
275+
* to true.
271276
* @returns True if the menu rendered below block; false if above.
272277
*/
273278
function showPositionedByRect(
274279
bBox: Rect,
275280
field: Field,
276281
opt_onHide?: () => void,
277282
opt_secondaryYOffset?: number,
283+
manageEphemeralFocus: boolean = true,
278284
): boolean {
279285
// If we can fit it, render below the block.
280286
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
@@ -299,6 +305,7 @@ function showPositionedByRect(
299305
primaryY,
300306
secondaryX,
301307
secondaryY,
308+
manageEphemeralFocus,
302309
opt_onHide,
303310
);
304311
}
@@ -319,6 +326,8 @@ function showPositionedByRect(
319326
* @param secondaryX Secondary/alternative origin point x, in absolute px.
320327
* @param secondaryY Secondary/alternative origin point y, in absolute px.
321328
* @param opt_onHide Optional callback for when the drop-down is hidden.
329+
* @param manageEphemeralFocus Whether ephemeral focus should be managed
330+
* according to the widget div's lifetime.
322331
* @returns True if the menu rendered at the primary origin point.
323332
* @internal
324333
*/
@@ -329,6 +338,7 @@ export function show<T>(
329338
primaryY: number,
330339
secondaryX: number,
331340
secondaryY: number,
341+
manageEphemeralFocus: boolean,
332342
opt_onHide?: () => void,
333343
): boolean {
334344
owner = newOwner as Field;
@@ -342,7 +352,9 @@ export function show<T>(
342352
dom.addClass(div, renderedClassName);
343353
dom.addClass(div, themeClassName);
344354

345-
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
355+
if (manageEphemeralFocus) {
356+
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
357+
}
346358

347359
// When we change `translate` multiple times in close succession,
348360
// Chrome may choose to wait and apply them all at once.

core/field_input.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,16 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
352352
* undefined if triggered programmatically.
353353
* @param quietInput True if editor should be created without focus.
354354
* Defaults to false.
355+
* @param manageEphemeralFocus Whether ephemeral focus should be managed as
356+
* part of the editor's inline editor (when the inline editor is shown).
357+
* Callers must manage ephemeral focus themselves if this is false.
358+
* Defaults to true.
355359
*/
356-
protected override showEditor_(_e?: Event, quietInput = false) {
360+
protected override showEditor_(
361+
_e?: Event,
362+
quietInput = false,
363+
manageEphemeralFocus: boolean = true,
364+
) {
357365
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
358366
if (
359367
!quietInput &&
@@ -362,7 +370,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
362370
) {
363371
this.showPromptEditor();
364372
} else {
365-
this.showInlineEditor(quietInput);
373+
this.showInlineEditor(quietInput, manageEphemeralFocus);
366374
}
367375
}
368376

@@ -389,8 +397,10 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
389397
* Create and show a text input editor that sits directly over the text input.
390398
*
391399
* @param quietInput True if editor should be created without focus.
400+
* @param manageEphemeralFocus Whether ephemeral focus should be managed as
401+
* part of the field's inline editor (widget div).
392402
*/
393-
private showInlineEditor(quietInput: boolean) {
403+
private showInlineEditor(quietInput: boolean, manageEphemeralFocus: boolean) {
394404
const block = this.getSourceBlock();
395405
if (!block) {
396406
throw new UnattachedFieldError();
@@ -400,6 +410,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
400410
block.RTL,
401411
this.widgetDispose_.bind(this),
402412
this.workspace_,
413+
manageEphemeralFocus,
403414
);
404415
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
405416
this.isBeingEdited_ = true;

core/widgetdiv.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,18 @@ export function createDom() {
8484
* @param newDispose Optional cleanup function to be run when the widget is
8585
* closed.
8686
* @param workspace The workspace associated with the widget owner.
87+
* @param manageEphemeralFocus Whether ephemeral focus should be managed
88+
* according to the widget div's lifetime. Note that if a false value is
89+
* passed in here then callers should manage ephemeral focus directly
90+
* otherwise focus may not properly restore when the widget closes. Defaults
91+
* to true.
8792
*/
8893
export function show(
8994
newOwner: unknown,
9095
rtl: boolean,
9196
newDispose: () => void,
9297
workspace?: WorkspaceSvg | null,
98+
manageEphemeralFocus: boolean = true,
9399
) {
94100
hide();
95101
owner = newOwner;
@@ -114,7 +120,9 @@ export function show(
114120
if (themeClassName) {
115121
dom.addClass(div, themeClassName);
116122
}
117-
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
123+
if (manageEphemeralFocus) {
124+
returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div);
125+
}
118126
}
119127

120128
/**

0 commit comments

Comments
 (0)