diff --git a/.gitignore b/.gitignore index 8aca6d7b3..684e62b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules *.hot-update.js *.log dist +dist-vite build Thumbs.db diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e2a0fe4e..5aad0eda6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,12 +28,21 @@ "statusBar.background": "#017ce6", "statusBar.foreground": "#e7e7e7", "statusBarItem.hoverBackground": "#1c95fe", - "statusBarItem.remoteBackground": "#017ce6", + "statusBarItem.remoteBackground": "#ffa0d3", "statusBarItem.remoteForeground": "#e7e7e7", "titleBar.activeBackground": "#017ce6", "titleBar.activeForeground": "#e7e7e7", "titleBar.inactiveBackground": "#017ce699", - "titleBar.inactiveForeground": "#e7e7e799" + "titleBar.inactiveForeground": "#e7e7e799", + "activityBarTop.activeBackground": "#1c95fe", + "activityBarTop.background": "#1c95fe", + "activityBarTop.foreground": "#e7e7e7", + "activityBarTop.inactiveForeground": "#e7e7e799", + "commandCenter.foreground": "#e7e7e7", + "statusBar.debuggingBackground": "#017ce6", + "statusBar.debuggingForeground": "#e7e7e7", + "activityBar.activeBorder": "#e7e7e7", + "activityBarTop.activeBorder": "#e7e7e7" }, "peacock.color": "#017ce6" } diff --git a/packages/cx/src/charts/Legend.tsx b/packages/cx/src/charts/Legend.tsx index 6eecec463..aeee88401 100644 --- a/packages/cx/src/charts/Legend.tsx +++ b/packages/cx/src/charts/Legend.tsx @@ -41,6 +41,15 @@ export interface LegendConfig extends HtmlElementConfig { /** Default shape to use for all entries. */ shape?: StringProp; + /** ARIA role for each entry's shape `svg`. Defaults to `img` when `shapeAriaLabel` is set. */ + shapeRole?: StringProp; + + /** + * Accessible name for each entry's shape `svg`. Unnamed shapes are hidden from assistive + * technology, since the entry text already says what they mean. + */ + shapeAriaLabel?: StringProp; + /** Style applied to each entry. */ entryStyle?: StyleProp; @@ -97,6 +106,8 @@ export class Legend extends HtmlElement { declareData(...args: any[]): void { super.declareData(...args, { shape: undefined, + shapeRole: undefined, + shapeAriaLabel: undefined, entryStyle: { structured: true }, entryClass: { structured: true }, valueStyle: { structured: true }, @@ -124,6 +135,8 @@ export class Legend extends HtmlElement { case "shapeSize": case "svgSize": case "shape": + case "shapeRole": + case "shapeAriaLabel": case "entryStyle": case "entryClass": case "valueStyle": @@ -177,7 +190,7 @@ export class Legend extends HtmlElement { instance.legends[this.name] && instance.legends[this.name].entries, list: React.ReactNode; - let { entryClass, entryStyle, shape, valueClass, valueStyle } = + let { entryClass, entryStyle, shape, shapeRole, shapeAriaLabel, valueClass, valueStyle } = instance.data; let valueFormatter = Format.parse(this.valueFormat); @@ -210,7 +223,7 @@ export class Legend extends HtmlElement { onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} > - {this.renderShape(e, shape)} + {this.renderShape(e, shape, shapeRole, shapeAriaLabel)}
{e.displayText || e.name}
{this.showValues && (
@@ -229,6 +242,8 @@ export class Legend extends HtmlElement { renderShape( entry: LegendEntryData, legendEntriesShape: string | null | undefined, + role?: string, + ariaLabel?: string, ): React.ReactNode { const className = this.CSS.element(this.baseClass, "shape", { [`color-${entry.colorIndex}`]: @@ -244,6 +259,11 @@ export class Legend extends HtmlElement { return ( { super(config); } + declareData(...args: any[]) { + return super.declareData( + { + role: undefined, + ariaLabel: undefined, + }, + ...args, + ); + } + initState(context: RenderingContext, instance: SvgInstance) { const size = { width: 0, @@ -147,6 +167,9 @@ class SvgComponent extends VDOM.Component { const { instance, data, size, children, eventHandlers } = this.props; const { widget } = instance; + // An unnamed drawing is left alone: `role="img"` would hide its text and put nothing in its place. + const role = data.role ?? (data.ariaLabel ? "img" : undefined); + const defs: any[] = []; for (const k in (instance as any).clipRects) { let cr = (instance as any).clipRects[k]; @@ -181,7 +204,7 @@ class SvgComponent extends VDOM.Component { {...eventHandlers} > {size.width > 0 && size.height > 0 && ( - + {defs} {children} diff --git a/packages/cx/src/widgets/form/TextArea.tsx b/packages/cx/src/widgets/form/TextArea.tsx index f2feb525a..fb2298158 100644 --- a/packages/cx/src/widgets/form/TextArea.tsx +++ b/packages/cx/src/widgets/form/TextArea.tsx @@ -16,13 +16,14 @@ import { stopPropagation } from "../../util/eventCallbacks"; import { KeyCode } from "../../util/KeyCode"; import { autoFocus } from "../autoFocus"; import { getActiveElement } from "../../util/getActiveElement"; +import { isString } from "../../util/isString"; import { NumberProp } from "../../ui/Prop"; /** - * `trim` is intentionally omitted: `TextArea` renders its own input which commits the raw value, - * so leading and trailing whitespace is always preserved. + * `trim` applies to the commit that ends the edit, not to intermediate `input` commits. Whitespace + * inside the text is never touched. */ -export interface TextAreaConfig extends Omit { +export interface TextAreaConfig extends TextFieldConfig { /** Specifies the number of visible lines. */ rows?: NumberProp; @@ -69,8 +70,6 @@ export class TextArea extends TextField { TextArea.prototype.baseClass = "textarea"; TextArea.prototype.reactOn = "blur"; -// `trim` is not supported by TextArea, so a global `TextField.prototype.trim = true` must not leak into it. -TextArea.prototype.trim = false; TextArea.prototype.suppressErrorsUntilVisited = true; interface InputProps { @@ -205,8 +204,12 @@ class Input extends VDOM.Component { } if (instance.widget.reactOn.indexOf(change) != -1) { - let value = inputValue || widget.emptyValue; + // Trimming on `input` would strip whitespace the user is still typing, so a trailing + // newline could never be entered. + let text = change != "input" && data.trim && isString(inputValue) ? inputValue.trim() : inputValue; + let value = text || widget.emptyValue; instance.set("value", value); + if (this.input && text != inputValue) this.input.value = text; } }