Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
*.hot-update.js
*.log
dist
dist-vite
build

Thumbs.db
Expand Down
13 changes: 11 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
24 changes: 22 additions & 2 deletions packages/cx/src/charts/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -210,7 +223,7 @@ export class Legend extends HtmlElement {
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
{this.renderShape(e, shape)}
{this.renderShape(e, shape, shapeRole, shapeAriaLabel)}
<div className={entryTextClass}>{e.displayText || e.name}</div>
{this.showValues && (
<div className={valueClasses} style={valueStyle}>
Expand All @@ -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}`]:
Expand All @@ -244,6 +259,11 @@ export class Legend extends HtmlElement {
return (
<svg
className={this.CSS.element(this.baseClass, "svg")}
role={role ?? (ariaLabel ? "img" : undefined)}
aria-label={ariaLabel}
// Mutually exclusive with the above: `aria-hidden` removes the element from the
// accessibility tree, which is also what a tagged PDF is built from.
aria-hidden={role == null && !ariaLabel ? true : undefined}
style={{
width: `${this.svgSize}px`,
height: `${this.svgSize}px`,
Expand Down
25 changes: 24 additions & 1 deletion packages/cx/src/svg/Svg.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** @jsxImportSource react */
import { StringProp } from "../ui/Prop";
import { RenderingContext } from "../ui/RenderingContext";
import { ResizeManager } from "../ui/ResizeManager";
import { VDOM, Widget } from "../ui/Widget";
Expand All @@ -18,6 +19,15 @@ interface SvgInstance extends BoundedObjectInstance {
}

export interface SvgConfig extends BoundedObjectConfig {
/** ARIA role for the `svg` element. Defaults to `img` when `ariaLabel` is set. */
role?: StringProp;

/**
* Accessible name for the `svg` element. Setting it announces the drawing as a single graphic
* instead of as its individual shapes, which is also how a tagged PDF export tags it.
*/
ariaLabel?: StringProp;

/** Set to `true` to automatically calculate width based on the measured height and `aspectRatio`. */
autoWidth?: boolean;

Expand Down Expand Up @@ -53,6 +63,16 @@ export class Svg extends BoundedObject<SvgConfig, SvgInstance> {
super(config);
}

declareData(...args: any[]) {
return super.declareData(
{
role: undefined,
ariaLabel: undefined,
},
...args,
);
}

initState(context: RenderingContext, instance: SvgInstance) {
const size = {
width: 0,
Expand Down Expand Up @@ -147,6 +167,9 @@ class SvgComponent extends VDOM.Component<SvgComponentProps> {
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];
Expand Down Expand Up @@ -181,7 +204,7 @@ class SvgComponent extends VDOM.Component<SvgComponentProps> {
{...eventHandlers}
>
{size.width > 0 && size.height > 0 && (
<svg>
<svg role={role} aria-label={data.ariaLabel}>
<defs>{defs}</defs>
{children}
</svg>
Expand Down
15 changes: 9 additions & 6 deletions packages/cx/src/widgets/form/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextFieldConfig, "trim"> {
export interface TextAreaConfig extends TextFieldConfig {
/** Specifies the number of visible lines. */
rows?: NumberProp;

Expand Down Expand Up @@ -69,8 +70,6 @@ export class TextArea extends TextField<TextAreaConfig> {

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 {
Expand Down Expand Up @@ -205,8 +204,12 @@ class Input extends VDOM.Component<InputProps, InputState> {
}

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;
}
}

Expand Down
Loading