Merge pull request #8798 from google/develop-v12-merge

chore: Merge develop into rc/v12.0.0
This commit is contained in:
Aaron Dodson
2025-03-12 13:29:12 -07:00
committed by GitHub
25 changed files with 780 additions and 455 deletions

View File

@@ -176,7 +176,7 @@ export function disconnectUiEffect(block: BlockSvg) {
}
// Start the animation.
wobblingBlock = block;
disconnectUiStep(block, magnitude, new Date());
disconnectUiStep(block, magnitude, new Date(), 0);
}
/**
@@ -184,22 +184,30 @@ export function disconnectUiEffect(block: BlockSvg) {
*
* @param block Block to animate.
* @param magnitude Maximum degrees skew (reversed for RTL).
* @param start Date of animation's start.
* @param start Date of animation's start for deciding when to stop.
* @param step Which step of the animation we're on.
*/
function disconnectUiStep(block: BlockSvg, magnitude: number, start: Date) {
function disconnectUiStep(
block: BlockSvg,
magnitude: number,
start: Date,
step: number,
) {
const DURATION = 200; // Milliseconds.
const WIGGLES = 3; // Half oscillations.
const ms = new Date().getTime() - start.getTime();
const percent = ms / DURATION;
const WIGGLES = [0.66, 1, 0.66, 0, -0.66, -1, -0.66, 0]; // Single cycle
let skew = '';
if (percent <= 1) {
const val = Math.round(
Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude,
);
if (start.getTime() + DURATION > new Date().getTime()) {
const val = Math.round(WIGGLES[step % WIGGLES.length] * magnitude);
skew = `skewX(${val})`;
disconnectPid = setTimeout(disconnectUiStep, 10, block, magnitude, start);
disconnectPid = setTimeout(
disconnectUiStep,
15,
block,
magnitude,
start,
step + 1,
);
}
block

View File

@@ -16,7 +16,7 @@ import {Bubble} from './bubble.js';
* A bubble that displays non-editable text. Used by the warning icon.
*/
export class TextBubble extends Bubble {
private paragraph: SVGTextElement;
private paragraph: SVGGElement;
constructor(
private text: string,
@@ -49,43 +49,52 @@ export class TextBubble extends Bubble {
*/
private stringToSvg(text: string, container: SVGGElement) {
const paragraph = this.createParagraph(container);
const spans = this.createSpans(paragraph, text);
const fragments = this.createTextFragments(paragraph, text);
if (this.workspace.RTL)
this.rightAlignSpans(paragraph.getBBox().width, spans);
this.rightAlignTextFragments(paragraph.getBBox().width, fragments);
return paragraph;
}
/** Creates the paragraph container for this bubble's view's spans. */
private createParagraph(container: SVGGElement): SVGTextElement {
/** Creates the paragraph container for this bubble's view's text fragments. */
private createParagraph(container: SVGGElement): SVGGElement {
return dom.createSvgElement(
Svg.TEXT,
Svg.G,
{
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
'y': Bubble.BORDER_WIDTH,
'transform': `translate(0,${Bubble.BORDER_WIDTH})`,
'style': `direction: ${this.workspace.RTL ? 'rtl' : 'ltr'}`,
},
container,
);
}
/** Creates the spans visualizing the text of this bubble. */
private createSpans(parent: SVGTextElement, text: string): SVGTSpanElement[] {
/** Creates the text fragments visualizing the text of this bubble. */
private createTextFragments(
parent: SVGGElement,
text: string,
): SVGTextElement[] {
let lineNum = 1;
return text.split('\n').map((line) => {
const tspan = dom.createSvgElement(
Svg.TSPAN,
{'dy': '1em', 'x': Bubble.BORDER_WIDTH},
const fragment = dom.createSvgElement(
Svg.TEXT,
{'y': `${lineNum}em`, 'x': Bubble.BORDER_WIDTH},
parent,
);
const textNode = document.createTextNode(line);
tspan.appendChild(textNode);
return tspan;
fragment.appendChild(textNode);
lineNum += 1;
return fragment;
});
}
/** Right aligns the given spans. */
private rightAlignSpans(maxWidth: number, spans: SVGTSpanElement[]) {
for (const span of spans) {
span.setAttribute('text-anchor', 'end');
span.setAttribute('x', `${maxWidth + Bubble.BORDER_WIDTH}`);
/** Right aligns the given text fragments. */
private rightAlignTextFragments(
maxWidth: number,
fragments: SVGTextElement[],
) {
for (const text of fragments) {
text.setAttribute('text-anchor', 'start');
text.setAttribute('x', `${maxWidth + Bubble.BORDER_WIDTH}`);
}
}

View File

@@ -213,8 +213,14 @@ export class RenderedWorkspaceComment
private startGesture(e: PointerEvent) {
const gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleCommentStart(e, this);
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
if (browserEvents.isTargetInput(e)) {
// If the text area was the focus, don't allow this event to bubble up
// and steal focus away from the editor/comment.
e.stopPropagation();
} else {
gesture.handleCommentStart(e, this);
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
}
common.setSelected(this);
}
}

View File

@@ -321,6 +321,7 @@ input[type=number] {
.blocklyScrollbarBackground {
opacity: 0;
pointer-events: none;
}
.blocklyScrollbarHandle {

View File

@@ -30,6 +30,7 @@ import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import * as utilsString from './utils/string.js';
import * as style from './utils/style.js';
import {Svg} from './utils/svg.js';
/**
@@ -302,6 +303,11 @@ export class FieldDropdown extends Field<string> {
if (this.selectedMenuItem) {
this.menu_!.setHighlighted(this.selectedMenuItem);
style.scrollIntoContainerView(
this.selectedMenuItem.getElement()!,
dropDownDiv.getContentDiv(),
true,
);
}
this.applyColour();

View File

@@ -77,6 +77,16 @@ export function inject(
});
browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown);
browserEvents.conditionalBind(
dropDownDiv.getContentDiv(),
'keydown',
null,
onKeyDown,
);
const widgetContainer = WidgetDiv.getDiv();
if (widgetContainer) {
browserEvents.conditionalBind(widgetContainer, 'keydown', null, onKeyDown);
}
return workspace;
}

View File

@@ -29,8 +29,8 @@ export class Menu {
private readonly menuItems: Array<MenuItem | MenuSeparator> = [];
/**
* Coordinates of the mousedown event that caused this menu to open. Used to
* prevent the consequent mouseup event due to a simple click from
* Coordinates of the pointerdown event that caused this menu to open. Used to
* prevent the consequent pointerup event due to a simple click from
* activating a menu item immediately.
*/
openingCoords: Coordinate | null = null;
@@ -41,17 +41,17 @@ export class Menu {
*/
private highlightedItem: MenuItem | null = null;
/** Mouse over event data. */
private mouseOverHandler: browserEvents.Data | null = null;
/** Pointer over event data. */
private pointerMoveHandler: browserEvents.Data | null = null;
/** Click event data. */
private clickHandler: browserEvents.Data | null = null;
/** Mouse enter event data. */
private mouseEnterHandler: browserEvents.Data | null = null;
/** Pointer enter event data. */
private pointerEnterHandler: browserEvents.Data | null = null;
/** Mouse leave event data. */
private mouseLeaveHandler: browserEvents.Data | null = null;
/** Pointer leave event data. */
private pointerLeaveHandler: browserEvents.Data | null = null;
/** Key down event data. */
private onKeyDownHandler: browserEvents.Data | null = null;
@@ -97,11 +97,11 @@ export class Menu {
}
// Add event handlers.
this.mouseOverHandler = browserEvents.conditionalBind(
this.pointerMoveHandler = browserEvents.conditionalBind(
element,
'pointerover',
'pointermove',
this,
this.handleMouseOver,
this.handlePointerMove,
true,
);
this.clickHandler = browserEvents.conditionalBind(
@@ -111,18 +111,18 @@ export class Menu {
this.handleClick,
true,
);
this.mouseEnterHandler = browserEvents.conditionalBind(
this.pointerEnterHandler = browserEvents.conditionalBind(
element,
'pointerenter',
this,
this.handleMouseEnter,
this.handlePointerEnter,
true,
);
this.mouseLeaveHandler = browserEvents.conditionalBind(
this.pointerLeaveHandler = browserEvents.conditionalBind(
element,
'pointerleave',
this,
this.handleMouseLeave,
this.handlePointerLeave,
true,
);
this.onKeyDownHandler = browserEvents.conditionalBind(
@@ -179,21 +179,21 @@ export class Menu {
/** Dispose of this menu. */
dispose() {
// Remove event handlers.
if (this.mouseOverHandler) {
browserEvents.unbind(this.mouseOverHandler);
this.mouseOverHandler = null;
if (this.pointerMoveHandler) {
browserEvents.unbind(this.pointerMoveHandler);
this.pointerMoveHandler = null;
}
if (this.clickHandler) {
browserEvents.unbind(this.clickHandler);
this.clickHandler = null;
}
if (this.mouseEnterHandler) {
browserEvents.unbind(this.mouseEnterHandler);
this.mouseEnterHandler = null;
if (this.pointerEnterHandler) {
browserEvents.unbind(this.pointerEnterHandler);
this.pointerEnterHandler = null;
}
if (this.mouseLeaveHandler) {
browserEvents.unbind(this.mouseLeaveHandler);
this.mouseLeaveHandler = null;
if (this.pointerLeaveHandler) {
browserEvents.unbind(this.pointerLeaveHandler);
this.pointerLeaveHandler = null;
}
if (this.onKeyDownHandler) {
browserEvents.unbind(this.onKeyDownHandler);
@@ -257,11 +257,13 @@ export class Menu {
this.highlightedItem = item;
// Bring the highlighted item into view. This has no effect if the menu is
// not scrollable.
const el = this.getElement();
if (el) {
aria.setState(el, aria.State.ACTIVEDESCENDANT, item.getId());
}
item.getElement()?.scrollIntoView();
const menuElement = this.getElement();
const scrollingParent = menuElement?.parentElement;
const menuItemElement = item.getElement();
if (!scrollingParent || !menuItemElement) return;
style.scrollIntoContainerView(menuItemElement, scrollingParent);
aria.setState(menuElement, aria.State.ACTIVEDESCENDANT, item.getId());
}
}
@@ -321,14 +323,26 @@ export class Menu {
}
}
// Mouse events.
// Pointer events.
/**
* Handles mouseover events. Highlight menuitems as the user hovers over them.
* Handles pointermove events. Highlight menu items as the user hovers over
* them.
*
* @param e Mouse event to handle.
* @param e Pointer event to handle.
*/
private handleMouseOver(e: PointerEvent) {
private handlePointerMove(e: PointerEvent) {
// Check whether the pointer actually did move. Move events are triggered if
// the element underneath the pointer moves, even if the pointer itself has
// remained stationary. In the case where the pointer is hovering over
// the menu but the user is navigating through the list of items via the
// keyboard and causing items off the end of the menu to scroll into view,
// a pointermove event would be triggered due to the pointer now being over
// a new child, but we don't want to highlight the item that's now under the
// pointer.
const delta = Math.max(Math.abs(e.movementX), Math.abs(e.movementY));
if (delta === 0) return;
const menuItem = this.getMenuItem(e.target as Element);
if (menuItem) {
@@ -354,11 +368,11 @@ export class Menu {
if (oldCoords && typeof e.clientX === 'number') {
const newCoords = new Coordinate(e.clientX, e.clientY);
if (Coordinate.distance(oldCoords, newCoords) < 1) {
// This menu was opened by a mousedown and we're handling the consequent
// click event. The coords haven't changed, meaning this was the same
// opening event. Don't do the usual behavior because the menu just
// popped up under the mouse and the user didn't mean to activate this
// item.
// This menu was opened by a pointerdown and we're handling the
// consequent click event. The coords haven't changed, meaning this was
// the same opening event. Don't do the usual behavior because the menu
// just popped up under the pointer and the user didn't mean to activate
// this item.
return;
}
}
@@ -370,22 +384,21 @@ export class Menu {
}
/**
* Handles mouse enter events. Focus the element.
* Handles pointer enter events. Focus the element.
*
* @param _e Mouse event to handle.
* @param _e Pointer event to handle.
*/
private handleMouseEnter(_e: PointerEvent) {
private handlePointerEnter(_e: PointerEvent) {
this.focus();
}
/**
* Handles mouse leave events. Blur and clear highlight.
* Handles pointer leave events by clearing the active highlight.
*
* @param _e Mouse event to handle.
* @param _e Pointer event to handle.
*/
private handleMouseLeave(_e: PointerEvent) {
private handlePointerLeave(_e: PointerEvent) {
if (this.getElement()) {
this.blur();
this.setHighlighted(null);
}
}

View File

@@ -533,6 +533,21 @@ export class RenderedConnection extends Connection {
childBlock.updateDisabled();
childBlock.queueRender();
// If either block being connected was selected, visually un- and reselect
// it. This has the effect of moving the selection path to the end of the
// list of child nodes in the DOM. Since SVG z-order is determined by node
// order in the DOM, this works around an issue where the selection outline
// path could be partially obscured by a new block inserted after it in the
// DOM.
const selection = common.getSelected();
const selectedBlock =
(selection === parentBlock && parentBlock) ||
(selection === childBlock && childBlock);
if (selectedBlock) {
selectedBlock.removeSelect();
selectedBlock.addSelect();
}
// The input the child block is connected to (if any).
const parentInput = parentBlock.getInputWithBlock(childBlock);
if (parentInput) {

View File

@@ -876,11 +876,11 @@ export class ConstantProvider extends BaseConstantProvider {
`}`,
// Widget and Dropdown Div
`${selector}.blocklyWidgetDiv .goog-menuitem,`,
`${selector}.blocklyDropDownDiv .goog-menuitem {`,
`${selector}.blocklyWidgetDiv .blocklyMenuItem,`,
`${selector}.blocklyDropDownDiv .blocklyMenuItem {`,
`font-family: ${this.FIELD_TEXT_FONTFAMILY};`,
`}`,
`${selector}.blocklyDropDownDiv .goog-menuitem-content {`,
`${selector}.blocklyDropDownDiv .blocklyMenuItemContent {`,
`color: #fff;`,
`}`,

View File

@@ -7,7 +7,6 @@
// Former goog.module ID: Blockly.utils.style
import {Coordinate} from './coordinate.js';
import * as deprecation from './deprecation.js';
import {Rect} from './rect.js';
import {Size} from './size.js';
@@ -59,7 +58,6 @@ function getSizeInternal(element: Element): Size {
* @returns Object with width/height properties.
*/
function getSizeWithDisplay(element: Element): Size {
deprecation.warn(`Blockly.utils.style.getSizeWithDisplay()`, 'v11.2', 'v13');
const offsetWidth = (element as HTMLElement).offsetWidth;
const offsetHeight = (element as HTMLElement).offsetHeight;
return new Size(offsetWidth, offsetHeight);
@@ -132,7 +130,6 @@ export function getViewportPageOffset(): Coordinate {
* @returns The computed border widths.
*/
export function getBorderBox(element: Element): Rect {
deprecation.warn(`Blockly.utils.style.getBorderBox()`, 'v11.2', 'v13');
const left = parseFloat(getComputedStyle(element, 'borderLeftWidth'));
const right = parseFloat(getComputedStyle(element, 'borderRightWidth'));
const top = parseFloat(getComputedStyle(element, 'borderTopWidth'));
@@ -159,12 +156,6 @@ export function scrollIntoContainerView(
container: Element,
opt_center?: boolean,
) {
deprecation.warn(
`Blockly.utils.style.scrollIntoContainerView()`,
'v11.2',
'v13',
'the native Element.scrollIntoView()',
);
const offset = getContainerOffsetToScrollInto(element, container, opt_center);
container.scrollLeft = offset.x;
container.scrollTop = offset.y;
@@ -189,11 +180,6 @@ export function getContainerOffsetToScrollInto(
container: Element,
opt_center?: boolean,
): Coordinate {
deprecation.warn(
`Blockly.utils.style.getContainerOffsetToScrollInto()`,
'v11.2',
'v13',
);
// Absolute position of the element's border's top left corner.
const elementPos = getPageOffset(element);
// Absolute position of the container's border's top left corner.