mirror of
https://github.com/google/blockly.git
synced 2026-01-04 07:30:08 +01:00
## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #9301 Fixes #9312 Fixes #9313 Fixes part of #9304 ### Proposed Changes This introduces a variety of specific changes to resolve several issues for screen reader work, including introducing fundamental support for field labeling. Specifically: - Field labels have been simplified to only use their custom defined ARIA name otherwise they are null (and thus should be ignored for readout purposes) which wraps up the remaining high-level work for #9301 (#9450 tracks more specific follow-up work to improve upon what's been established at this point). The PR also introduces an ARIA override for number inputs in math blocks so that the readout is correct for them. - Bubble labeling is more explicit now which is useful for mutators (#9312), warnings, and comments. The general improvement for bubbles wraps up the remaining work for #9313 as well since the core issue was resolved in #9351. By default a bubble has no ARIA label. - #9304 is partly being addressed here with the change to field images: they are no longer being added to the accessibility node tree unless they are actually navigable (that is, clickable). Part of #9304's goal is to remove extraneous nodes. - Finally, a typo was fixed for 'replaceable blocks' since these were not reading out correctly. This was noticed in passing and isn't directly related to the other issues. ### Reason for Changes This PR is largely being used as a basis for one particularly significant issue: #9301. Field labeling has undergone several iterations over the past few months and the team seems comfortable sticking with a "do as little as possible" approach when determining the label, thus justifying the need for expecting more specific customization (i.e. #9450). To this end it's important to be clear that getting fields to a good state is not actually "done" but the need to track it as a large incomplete thing has ended. Note that one important part of #9301 was updating field plugins to be accessible--this largely seems unnecessary as-is as it will be completely dependent on the needs of future user tests. The long-term plan will need to account for making all fields in `blockly-samples` accessible (per #9307). Some of the terminology used here (e.g. for bubbles) will likely need to change after user testing, but it's important to establish that _something_ correct is communicated even if the terminology may require scaffolding and/or refinement. It's important to note that while non-clickable field images are no longer in the node graph, their ARIA presence still exists as part of the fluent block labeling solution. That is, `FieldImage`'s alt text is used as part of constructing a fluent block label (sometimes to confusing effect--see #9452). ### Test Coverage No tests needed since these are experimental changes and do not change existing test behaviors. ### Documentation No documentation changes are needed for these experimental changes. ### Additional Information None.
301 lines
8.7 KiB
TypeScript
301 lines
8.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2023 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {CommentEditor} from '../comments/comment_editor.js';
|
|
import * as Css from '../css.js';
|
|
import {getFocusManager} from '../focus_manager.js';
|
|
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
|
import type {IHasBubble} from '../interfaces/i_has_bubble.js';
|
|
import * as touch from '../touch.js';
|
|
import {browserEvents} from '../utils.js';
|
|
import {Coordinate} from '../utils/coordinate.js';
|
|
import * as dom from '../utils/dom.js';
|
|
import * as drag from '../utils/drag.js';
|
|
import {Rect} from '../utils/rect.js';
|
|
import {Size} from '../utils/size.js';
|
|
import {Svg} from '../utils/svg.js';
|
|
import {WorkspaceSvg} from '../workspace_svg.js';
|
|
import {Bubble} from './bubble.js';
|
|
|
|
/**
|
|
* A bubble that displays editable text. It can also be resized by the user.
|
|
* Used by the comment icon.
|
|
*/
|
|
export class TextInputBubble extends Bubble {
|
|
/** The group containing the lines indicating the bubble is resizable. */
|
|
private resizeGroup: SVGGElement;
|
|
|
|
/**
|
|
* Event data associated with the listener for pointer up events on the
|
|
* resize group.
|
|
*/
|
|
private resizePointerUpListener: browserEvents.Data | null = null;
|
|
|
|
/**
|
|
* Event data associated with the listener for pointer move events on the
|
|
* resize group.
|
|
*/
|
|
private resizePointerMoveListener: browserEvents.Data | null = null;
|
|
|
|
/** Functions listening for changes to the size of this bubble. */
|
|
private sizeChangeListeners: (() => void)[] = [];
|
|
|
|
/** Functions listening for changes to the location of this bubble. */
|
|
private locationChangeListeners: (() => void)[] = [];
|
|
|
|
/** The default size of this bubble, including borders. */
|
|
private readonly DEFAULT_SIZE = new Size(
|
|
160 + Bubble.DOUBLE_BORDER,
|
|
80 + Bubble.DOUBLE_BORDER,
|
|
);
|
|
|
|
/** The minimum size of this bubble, including borders. */
|
|
private readonly MIN_SIZE = new Size(
|
|
45 + Bubble.DOUBLE_BORDER,
|
|
20 + Bubble.DOUBLE_BORDER,
|
|
);
|
|
|
|
private editable = true;
|
|
|
|
/** View responsible for supporting text editing. */
|
|
private editor: CommentEditor;
|
|
|
|
/**
|
|
* @param workspace The workspace this bubble belongs to.
|
|
* @param anchor The anchor location of the thing this bubble is attached to.
|
|
* The tail of the bubble will point to this location.
|
|
* @param ownerRect An optional rect we don't want the bubble to overlap with
|
|
* when automatically positioning.
|
|
* @param owner The object that owns/hosts this bubble.
|
|
*/
|
|
constructor(
|
|
public readonly workspace: WorkspaceSvg,
|
|
protected anchor: Coordinate,
|
|
protected ownerRect?: Rect,
|
|
protected owner?: IHasBubble & IFocusableNode,
|
|
) {
|
|
super(workspace, anchor, ownerRect, undefined, owner);
|
|
dom.addClass(this.svgRoot, 'blocklyTextInputBubble');
|
|
this.editor = new CommentEditor(workspace, this.id, () => {
|
|
getFocusManager().focusNode(this);
|
|
});
|
|
this.contentContainer.appendChild(this.editor.getDom());
|
|
this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace);
|
|
this.setSize(this.DEFAULT_SIZE, true);
|
|
}
|
|
|
|
protected override getAriaLabel(): string | null {
|
|
return 'Comment Bubble';
|
|
}
|
|
|
|
/** @returns the text of this bubble. */
|
|
getText(): string {
|
|
return this.editor.getText();
|
|
}
|
|
|
|
/** Sets the text of this bubble. Calls change listeners. */
|
|
setText(text: string) {
|
|
this.editor.setText(text);
|
|
}
|
|
|
|
/** Sets whether or not the text in the bubble is editable. */
|
|
setEditable(editable: boolean) {
|
|
this.editable = editable;
|
|
this.editor.setEditable(editable);
|
|
}
|
|
|
|
/** Returns whether or not the text in the bubble is editable. */
|
|
isEditable(): boolean {
|
|
return this.editable;
|
|
}
|
|
|
|
/** Adds a change listener to be notified when this bubble's text changes. */
|
|
addTextChangeListener(listener: () => void) {
|
|
this.editor.addTextChangeListener(listener);
|
|
}
|
|
|
|
/** Adds a change listener to be notified when this bubble's size changes. */
|
|
addSizeChangeListener(listener: () => void) {
|
|
this.sizeChangeListeners.push(listener);
|
|
}
|
|
|
|
/** Adds a change listener to be notified when this bubble's location changes. */
|
|
addLocationChangeListener(listener: () => void) {
|
|
this.locationChangeListeners.push(listener);
|
|
}
|
|
|
|
/** Creates the resize handler elements and binds events to them. */
|
|
private createResizeHandle(
|
|
container: SVGGElement,
|
|
workspace: WorkspaceSvg,
|
|
): SVGGElement {
|
|
const resizeHandle = dom.createSvgElement(
|
|
Svg.IMAGE,
|
|
{
|
|
'class': 'blocklyResizeHandle',
|
|
'href': `${workspace.options.pathToMedia}resize-handle.svg`,
|
|
},
|
|
container,
|
|
);
|
|
|
|
browserEvents.conditionalBind(
|
|
resizeHandle,
|
|
'pointerdown',
|
|
this,
|
|
this.onResizePointerDown,
|
|
);
|
|
|
|
return resizeHandle;
|
|
}
|
|
|
|
/**
|
|
* Sets the size of this bubble, including the border.
|
|
*
|
|
* @param size Sets the size of this bubble, including the border.
|
|
* @param relayout If true, reposition the bubble from scratch so that it is
|
|
* optimally visible. If false, reposition it so it maintains the same
|
|
* position relative to the anchor.
|
|
*/
|
|
setSize(size: Size, relayout = false) {
|
|
size.width = Math.max(size.width, this.MIN_SIZE.width);
|
|
size.height = Math.max(size.height, this.MIN_SIZE.height);
|
|
|
|
const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER;
|
|
const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER;
|
|
this.editor.updateSize(
|
|
new Size(widthMinusBorder, heightMinusBorder),
|
|
new Size(0, 0),
|
|
);
|
|
this.editor.getDom().setAttribute('x', `${Bubble.DOUBLE_BORDER / 2}`);
|
|
this.editor.getDom().setAttribute('y', `${Bubble.DOUBLE_BORDER / 2}`);
|
|
|
|
this.resizeGroup.setAttribute('y', `${heightMinusBorder}`);
|
|
if (this.workspace.RTL) {
|
|
this.resizeGroup.setAttribute('x', `${-Bubble.DOUBLE_BORDER}`);
|
|
} else {
|
|
this.resizeGroup.setAttribute('x', `${widthMinusBorder}`);
|
|
}
|
|
|
|
super.setSize(size, relayout);
|
|
this.onSizeChange();
|
|
}
|
|
|
|
/** @returns the size of this bubble. */
|
|
getSize(): Size {
|
|
// Overridden to be public.
|
|
return super.getSize();
|
|
}
|
|
|
|
override moveDuringDrag(newLoc: Coordinate) {
|
|
super.moveDuringDrag(newLoc);
|
|
this.onLocationChange();
|
|
}
|
|
|
|
override setPositionRelativeToAnchor(left: number, top: number) {
|
|
super.setPositionRelativeToAnchor(left, top);
|
|
this.onLocationChange();
|
|
}
|
|
|
|
protected override positionByRect(rect = new Rect(0, 0, 0, 0)) {
|
|
super.positionByRect(rect);
|
|
this.onLocationChange();
|
|
}
|
|
|
|
/** Handles mouse down events on the resize target. */
|
|
private onResizePointerDown(e: PointerEvent) {
|
|
this.bringToFront();
|
|
if (browserEvents.isRightButton(e)) {
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
drag.start(
|
|
this.workspace,
|
|
e,
|
|
new Coordinate(
|
|
this.workspace.RTL ? -this.getSize().width : this.getSize().width,
|
|
this.getSize().height,
|
|
),
|
|
);
|
|
|
|
this.resizePointerUpListener = browserEvents.conditionalBind(
|
|
document,
|
|
'pointerup',
|
|
this,
|
|
this.onResizePointerUp,
|
|
);
|
|
this.resizePointerMoveListener = browserEvents.conditionalBind(
|
|
document,
|
|
'pointermove',
|
|
this,
|
|
this.onResizePointerMove,
|
|
);
|
|
this.workspace.hideChaff();
|
|
// This event has been handled. No need to bubble up to the document.
|
|
e.stopPropagation();
|
|
}
|
|
|
|
/** Handles pointer up events on the resize target. */
|
|
private onResizePointerUp(_e: PointerEvent) {
|
|
touch.clearTouchIdentifier();
|
|
if (this.resizePointerUpListener) {
|
|
browserEvents.unbind(this.resizePointerUpListener);
|
|
this.resizePointerUpListener = null;
|
|
}
|
|
if (this.resizePointerMoveListener) {
|
|
browserEvents.unbind(this.resizePointerMoveListener);
|
|
this.resizePointerMoveListener = null;
|
|
}
|
|
}
|
|
|
|
/** Handles pointer move events on the resize target. */
|
|
private onResizePointerMove(e: PointerEvent) {
|
|
const delta = drag.move(this.workspace, e);
|
|
this.setSize(
|
|
new Size(this.workspace.RTL ? -delta.x : delta.x, delta.y),
|
|
false,
|
|
);
|
|
this.onSizeChange();
|
|
}
|
|
|
|
/** Handles a size change event for the text area. Calls event listeners. */
|
|
private onSizeChange() {
|
|
for (const listener of this.sizeChangeListeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
/** Handles a location change event for the text area. Calls event listeners. */
|
|
private onLocationChange() {
|
|
for (const listener of this.locationChangeListeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the text editor component of this bubble.
|
|
*
|
|
* @internal
|
|
*/
|
|
getEditor() {
|
|
return this.editor;
|
|
}
|
|
}
|
|
|
|
Css.register(`
|
|
.blocklyTextInputBubble .blocklyTextarea {
|
|
background-color: var(--commentFillColour);
|
|
border: 0;
|
|
box-sizing: border-box;
|
|
display: block;
|
|
outline: 0;
|
|
padding: 5px;
|
|
resize: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
`);
|