Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR] Linear graph - add grab handle description and aria lives #2025

Merged
merged 4 commits into from
Dec 19, 2024
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
5 changes: 5 additions & 0 deletions .changeset/cyan-bees-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

[SR] Linear graph - add grab handle description and aria lives
19 changes: 19 additions & 0 deletions packages/perseus/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ export type PerseusStrings = {
yIntercept: string;
}) => string;
srLinearGraphOriginIntercept: string;
srLinearGrabHandle: ({
point1X,
point1Y,
point2X,
point2Y,
}: {
point1X: string;
point1Y: string;
point2X: string;
point2Y: string;
}) => string;
srAngleSideAtCoordinates: ({
point,
side,
Expand Down Expand Up @@ -515,6 +526,12 @@ export const strings: {
"Screenreader-only description of the line's intercept when the intercept is the graph's origin.",
message: "The line crosses the x and y axes at the graph's origin.",
},
srLinearGrabHandle: {
context:
"Screenreader-only label on the grab handle for the line on a linear graph.",
message:
"Line from %(point1X)s comma %(point1Y)s to %(point2X)s comma %(point2Y)s.",
},
srAngleSideAtCoordinates: {
context:
"Screenreader-accessible description of the side / vertex of an angle graph",
Expand Down Expand Up @@ -733,6 +750,8 @@ export const mockStrings: PerseusStrings = {
`The line crosses the X-axis at ${xIntercept} comma 0 and the Y-axis at 0 comma ${yIntercept}.`,
srLinearGraphOriginIntercept:
"The line crosses the x and y axes at the graph's origin.",
srLinearGrabHandle: ({point1X, point1Y, point2X, point2Y}) =>
`Line from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`,
srAngleSideAtCoordinates: ({point, side, x, y}) =>
`Point ${point}, ${side} at ${x} comma ${y}`,
srAngleVertexAtCoordinatesWithAngleMeasure: ({x, y, angleMeasure}) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ exports[`Rendering Does NOT render extensions of line when option is disabled 1`
>
<g
aria-label="Point 1 at -1 comma -1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
tabindex="0"
/>
<g
aria-live="off"
class="movable-line"
data-testid="movable-line"
role="button"
style="cursor: grab;"
tabindex="0"
>
Expand Down Expand Up @@ -61,7 +63,7 @@ exports[`Rendering Does NOT render extensions of line when option is disabled 1`
</g>
<g
aria-label="Point 2 at 1 comma 1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
Expand Down Expand Up @@ -156,15 +158,17 @@ exports[`Rendering Does NOT render extensions of line when option is not provide
>
<g
aria-label="Point 1 at -1 comma -1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
tabindex="0"
/>
<g
aria-live="off"
class="movable-line"
data-testid="movable-line"
role="button"
style="cursor: grab;"
tabindex="0"
>
Expand Down Expand Up @@ -201,7 +205,7 @@ exports[`Rendering Does NOT render extensions of line when option is not provide
</g>
<g
aria-label="Point 2 at 1 comma 1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
Expand Down Expand Up @@ -296,15 +300,17 @@ exports[`Rendering Does render extensions of line when option is enabled 1`] = `
>
<g
aria-label="Point 1 at -1 comma -1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
tabindex="0"
/>
<g
aria-live="off"
class="movable-line"
data-testid="movable-line"
role="button"
style="cursor: grab;"
tabindex="0"
>
Expand Down Expand Up @@ -395,7 +401,7 @@ exports[`Rendering Does render extensions of line when option is enabled 1`] = `
</g>
<g
aria-label="Point 2 at 1 comma 1"
aria-live="polite"
aria-live="off"
class="movable-point__focusable-handle"
data-testid="movable-point__focusable-handle"
role="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {SVGLine} from "./svg-line";
import {useControlPoint} from "./use-control-point";
import {Vector} from "./vector";

import type {AriaLive} from "../../types";
import type {Interval} from "mafs";

type Props = {
points: Readonly<[vec.Vector2, vec.Vector2]>;
ariaLabels?: {
point1AriaLabel?: string;
point2AriaLabel?: string;
grabHandleAriaLabel?: string;
};
// Extra graph information to be read by screen readers
ariaDescribedBy?: string;
Expand All @@ -35,15 +37,25 @@ type Props = {

export const MovableLine = (props: Props) => {
const {
points: [start, end],
ariaLabels,
ariaDescribedBy,
onMoveLine = () => {},
onMovePoint = () => {},
color,
points: [start, end],
extend,
onMoveLine = () => {},
onMovePoint = () => {},
} = props;

// Aria live states for (0) point 1, (1) point 2, and (2) grab handle.
// When moving an element, set its aria live to "polite" and the others
// to "off". Otherwise, other connected elements that move at the same
// time might override the currently focused element's aria live.
const [ariaLives, setAriaLives] = React.useState<Array<AriaLive>>([
"off",
"off",
"off",
]);

// We use separate focusableHandle elements, instead of letting the movable
// points themselves be focusable, to allow the tab order of the points to
// be different from the rendering order. We had to solve for the following
Expand All @@ -59,28 +71,42 @@ export const MovableLine = (props: Props) => {
useControlPoint({
ariaLabel: ariaLabels?.point1AriaLabel,
ariaDescribedBy: ariaDescribedBy,
ariaLive: ariaLives[0],
point: start,
sequenceNumber: 1,
color,
onMove: (p) => onMovePoint(0, p),
onMove: (p) => {
setAriaLives(["polite", "off", "off"]);
onMovePoint(0, p);
},
});
const {visiblePoint: visiblePoint2, focusableHandle: focusableHandle2} =
useControlPoint({
ariaLabel: ariaLabels?.point2AriaLabel,
ariaDescribedBy: ariaDescribedBy,
ariaLive: ariaLives[1],
point: end,
sequenceNumber: 2,
color,
onMove: (p) => onMovePoint(1, p),
onMove: (p) => {
setAriaLives(["off", "polite", "off"]);
onMovePoint(1, p);
},
});

const line = (
<Line
ariaLabel={ariaLabels?.grabHandleAriaLabel}
ariaDescribedBy={ariaDescribedBy}
ariaLive={ariaLives[2]}
start={start}
end={end}
stroke={color}
extend={extend}
onMove={onMoveLine}
onMove={(delta) => {
setAriaLives(["off", "off", "polite"]);
onMoveLine(delta);
}}
/>
);

Expand All @@ -100,19 +126,31 @@ const defaultStroke = "var(--movable-line-stroke-color)";
type LineProps = {
start: vec.Vector2;
end: vec.Vector2;
onMove: (delta: vec.Vector2) => unknown;
stroke?: string | undefined;
ariaLabel?: string;
ariaDescribedBy?: string;
ariaLive?: AriaLive;
/* Extends the line to the edge of the graph with an arrow */
extend?:
| undefined
| {
start: boolean;
end: boolean;
};
stroke?: string | undefined;
onMove: (delta: vec.Vector2) => unknown;
};

const Line = (props: LineProps) => {
const {start, end, onMove, extend, stroke = defaultStroke} = props;
const {
start,
end,
ariaLabel,
ariaDescribedBy,
ariaLive,
extend,
stroke = defaultStroke,
onMove,
} = props;

const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
const {
Expand Down Expand Up @@ -150,9 +188,16 @@ const Line = (props: LineProps) => {
<g
ref={line}
tabIndex={disableKeyboardInteraction ? -1 : 0}
aria-label={ariaLabel}
aria-describedby={ariaDescribedBy}
aria-live={ariaLive}
className="movable-line"
data-testid="movable-line"
style={{cursor: dragging ? "grabbing" : "grab"}}
// Indicate that this element is interactive.
// As a bonus, giving this group a non-group role makes
// the screen reader skip over its empty children.
role="button"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-read the whole graph section of the SRUX doc earlier and noticed I missed this

An appropriate semantic element or role for the graph as a whole is figure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💀 I missed that too.

I think this line is still button since it's referring to the grab handle, but I think I'll have to add figure to the parent layer.

>
{/**
* This transparent line creates a nice big click/touch target.
Expand Down
Loading
Loading