From 2bbb3aa1fcc1cc2df1a75bfbdefa42ab56182872 Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Mon, 17 Apr 2023 10:06:44 -0700 Subject: [PATCH] fix: do not hide all chaff when resizing (#6916) --- core/bump_objects.ts | 22 ++++++++++++---------- core/field.ts | 22 ++++++++++++++++++++++ core/field_input.ts | 26 +++++++++++++++++++++++++- core/inject.ts | 7 ++++++- core/widgetdiv.ts | 23 +++++++++++++++++++++++ core/workspace_svg.ts | 17 ++++++++++++++--- 6 files changed, 102 insertions(+), 15 deletions(-) diff --git a/core/bump_objects.ts b/core/bump_objects.ts index 7396b7a0e..00b5e3f11 100644 --- a/core/bump_objects.ts +++ b/core/bump_objects.ts @@ -26,22 +26,24 @@ import type {WorkspaceSvg} from './workspace_svg.js'; * Bumps the given object that has passed out of bounds. * * @param workspace The workspace containing the object. - * @param scrollMetrics Scroll metrics - * in workspace coordinates. + * @param bounds The region to bump an object into. For example, pass + * ScrollMetrics to bump a block into the scrollable region of the + * workspace, or pass ViewMetrics to bump a block into the visible region of + * the workspace. This should be specified in workspace coordinates. * @param object The object to bump. - * @returns True if block was bumped. + * @returns True if object was bumped. */ function bumpObjectIntoBounds( - workspace: WorkspaceSvg, scrollMetrics: ContainerRegion, + workspace: WorkspaceSvg, bounds: ContainerRegion, object: IBoundedElement): boolean { // Compute new top/left position for object. const objectMetrics = object.getBoundingRectangle(); const height = objectMetrics.bottom - objectMetrics.top; const width = objectMetrics.right - objectMetrics.left; - const topClamp = scrollMetrics.top; - const scrollMetricsBottom = scrollMetrics.top + scrollMetrics.height; - const bottomClamp = scrollMetricsBottom - height; + const topClamp = bounds.top; + const boundsBottom = bounds.top + bounds.height; + const bottomClamp = boundsBottom - height; // If the object is taller than the workspace we want to // top-align the block const newYPosition = @@ -50,9 +52,9 @@ function bumpObjectIntoBounds( // Note: Even in RTL mode the "anchor" of the object is the // top-left corner of the object. - let leftClamp = scrollMetrics.left; - const scrollMetricsRight = scrollMetrics.left + scrollMetrics.width; - let rightClamp = scrollMetricsRight - width; + let leftClamp = bounds.left; + const boundsRight = bounds.left + bounds.width; + let rightClamp = boundsRight - width; if (workspace.RTL) { // If the object is wider than the workspace and we're in RTL // mode we want to right-align the block, which means setting diff --git a/core/field.ts b/core/field.ts index eda991fe4..5821b7938 100644 --- a/core/field.ts +++ b/core/field.ts @@ -729,6 +729,28 @@ export abstract class Field implements IASTNodeLocationSvg, protected showEditor_(_e?: Event): void {} // NOP + /** + * A developer hook to reposition the WidgetDiv during a window resize. You + * need to define this hook if your field has a WidgetDiv that needs to + * reposition itself when the window is resized. For example, text input + * fields define this hook so that the input WidgetDiv can reposition itself + * on a window resize event. This is especially important when modal inputs + * have been disabled, as Android devices will fire a window resize event when + * the soft keyboard opens. + * + * If you want the WidgetDiv to hide itself instead of repositioning, return + * false. This is the default behavior. + * + * DropdownDivs already handle their own positioning logic, so you do not need + * to override this function if your field only has a DropdownDiv. + * + * @returns True if the field should be repositioned, + * false if the WidgetDiv should hide itself instead. + */ + repositionForWindowResize(): boolean { + return false; + } + /** * Updates the size of the field based on the text. * diff --git a/core/field_input.ts b/core/field_input.ts index ba86b0449..1fba97b96 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -15,7 +15,8 @@ goog.declareModuleId('Blockly.FieldInput'); // Unused import preserved for side-effects. Remove if unneeded. import './events/events_block_change.js'; -import type {BlockSvg} from './block_svg.js'; +import {BlockSvg} from './block_svg.js'; +import * as bumpObjects from './bump_objects.js'; import * as browserEvents from './browser_events.js'; import * as dialog from './dialog.js'; import * as dom from './utils/dom.js'; @@ -505,6 +506,29 @@ export abstract class FieldInput extends Field { div!.style.top = xy.y + 'px'; } + /** + * Handles repositioning the WidgetDiv used for input fields when the + * workspace is resized. Will bump the block into the viewport and update the + * position of the field if necessary. + * + * @returns True for rendered workspaces, as we never want to hide the widget + * div. + */ + override repositionForWindowResize(): boolean { + const block = this.getSourceBlock(); + // This shouldn't be possible. We should never have a WidgetDiv if not using + // rendered blocks. + if (!(block instanceof BlockSvg)) return false; + + bumpObjects.bumpIntoBounds( + this.workspace_!, + this.workspace_!.getMetricsManager().getViewMetrics(true), block); + + this.resizeEditor_(); + + return true; + } + /** * Returns whether or not the field is tab navigable. * diff --git a/core/inject.ts b/core/inject.ts index 69026a897..e6d97b118 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -213,7 +213,12 @@ function init(mainWorkspace: WorkspaceSvg) { const workspaceResizeHandler = browserEvents.conditionalBind(window, 'resize', null, function() { - mainWorkspace.hideChaff(true); + // Don't hide all the chaff. Leave the dropdown and widget divs open if + // possible. + Tooltip.hide(); + mainWorkspace.hideComponents(true); + dropDownDiv.repositionForWindowResize(); + WidgetDiv.repositionForWindowResize(); common.svgResize(mainWorkspace); bumpObjects.bumpTopObjectsIntoBounds(mainWorkspace); }); diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index 0c5f1615d..87f4c4e19 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -9,6 +9,7 @@ goog.declareModuleId('Blockly.WidgetDiv'); import * as common from './common.js'; import * as dom from './utils/dom.js'; +import type {Field} from './field.js'; import type {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -237,3 +238,25 @@ function calculateY( return anchorBBox.bottom; } } + +/** + * Determine if the owner is a field for purposes of repositioning. + * We can't simply check `instanceof Field` as that would introduce a circular + * dependency. + */ +function isRepositionable(item: any): item is Field { + return !!item?.repositionForWindowResize; +} + +/** + * Reposition the widget div if the owner of it says to. + * If the owner isn't a field, just give up and hide it. + */ +export function repositionForWindowResize(): void { + if (!isRepositionable(owner) || !owner.repositionForWindowResize()) { + // If the owner is not a Field, or if the owner returns false from the + // reposition method, we should hide the widget div. Otherwise, we'll assume + // the owner handled any needed resize. + hide(); + } +} diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 7ac747c14..c5a07ab04 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2511,14 +2511,25 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * Close tooltips, context menus, dropdown selections, etc. * - * @param opt_onlyClosePopups Whether only popups should be closed. + * @param onlyClosePopups Whether only popups should be closed. Defaults to + * false. */ - hideChaff(opt_onlyClosePopups?: boolean) { + hideChaff(onlyClosePopups = false) { Tooltip.hide(); WidgetDiv.hide(); dropDownDiv.hideWithoutAnimation(); - const onlyClosePopups = !!opt_onlyClosePopups; + this.hideComponents(onlyClosePopups); + } + + /** + * Hide any autohideable components (like flyout, trashcan, and any + * user-registered components). + * + * @param onlyClosePopups Whether only popups should be closed. Defaults to + * false. + */ + hideComponents(onlyClosePopups = false) { const autoHideables = this.getComponentManager().getComponents( ComponentManager.Capability.AUTOHIDEABLE, true); autoHideables.forEach(