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][SR Tree] Add screen reader tree to interactive graph editor #2062

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

nishasy
Copy link
Contributor

@nishasy nishasy commented Dec 27, 2024

Summary:

To make it easier for content authors (and us devs while we're still
working on the screen reader experience) to build the screen reader
experience for an interactive graph without constantly having to
turn on the screen reader, add a screen reader tree directly into
the editor.

To do this, I used querySelectorAll() to get the mafs container
and all its children, and read their aria-label and
aria-describedby attributes.

After building an object with the elements, roles, and attributes,
display a tree showing these in the editor.

image

Issue: https://khanacademy.atlassian.net/browse/LEMS-2714

Test plan:

yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-sr-tree.test.tsx

Storybook

@nishasy nishasy self-assigned this Dec 27, 2024
Copy link
Contributor

github-actions bot commented Dec 27, 2024

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (b3e2e88) and published it to npm. You
can install it using the tag PR2062.

Example:

yarn add @khanacademy/perseus@PR2062

If you are working in Khan Academy's webapp, you can run:

./dev/tools/bump_perseus_version.sh -t PR2062

Copy link
Contributor

github-actions bot commented Dec 27, 2024

Size Change: +979 B (+0.08%)

Total Size: 1.28 MB

Filename Size Change
packages/perseus-editor/dist/es/index.js 689 kB +978 B (+0.14%)
packages/perseus/dist/es/index.js 419 kB +1 B (0%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 4.27 kB
packages/math-input/dist/es/index.js 78 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-core/dist/es/index.js 1.48 kB
packages/perseus-linter/dist/es/index.js 22.2 kB
packages/perseus/dist/es/strings.js 5.03 kB
packages/pure-markdown/dist/es/index.js 3.67 kB
packages/simple-markdown/dist/es/index.js 12.5 kB

compressed-size-action

@nishasy nishasy marked this pull request as ready for review December 27, 2024 02:06
};

// Exported for testing
export function fetchAriaLabels(container?: Element): AttributeMap[] {
Copy link
Member

Choose a reason for hiding this comment

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

This function is returning more than ARIA labels, so how about naming it something like getAccessibilityAttributes?

Suggested change
export function fetchAriaLabels(container?: Element): AttributeMap[] {
export function getAccessibilityAttributes(container?: Element): AttributeMap[] {

Comment on lines 20 to 29
// Arrange
const container = render(
<div>
<div aria-label="label1" />
<div aria-label="label2" />
</div>,
);

// Act
const result = fetchAriaLabels(container.container);
Copy link
Member

Choose a reason for hiding this comment

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

container.container is confusing. I think the thing returned by render is just an object containing the actual container? So maybe destructuring would be appropriate here.

Suggested change
// Arrange
const container = render(
<div>
<div aria-label="label1" />
<div aria-label="label2" />
</div>,
);
// Act
const result = fetchAriaLabels(container.container);
// Arrange
const {container} = render(
<div>
<div aria-label="label1" />
<div aria-label="label2" />
</div>,
);
// Act
const result = fetchAriaLabels(container);

if (attribute.name === "aria-describedby") {
// Aria-description is a space-separated list of ids.
// Use the ids to get the actual description strings.
const descriptions = attribute.value.split(" ");
Copy link
Member

Choose a reason for hiding this comment

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

What if there are multiple spaces between description IDs? It might be good to use a regex here:

Suggested change
const descriptions = attribute.value.split(" ");
const descriptions = attribute.value.split(/ +/);

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'll update it to regex so it's more deliberate, but I tried testing it with " " just for fun, and it looks like it still passes 🤔

const descriptions = attribute.value.split(" ");
for (const description of descriptions) {
const descriptionString =
document.getElementById(description)?.textContent;
Copy link
Member

Choose a reason for hiding this comment

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

Question: why textContent and not innerText here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

textContent includes hidden text, whereas innerText does not. All the descriptions that the aria-describedbys are referring to are within hidden text, so we have to use textContent for them to show up.

I'll add a comment here and a test for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... if I use innerText instead, the test passes even though the descriptions stop showing up in the tree in the editor 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

innerText textContent
image image

// followed by an unordered list of its aria attributes.
return (
<ol style={{listStyle: "revert", marginLeft: 8}}>
{elementArias.map((ariaString, index) => (
Copy link
Member

Choose a reason for hiding this comment

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

The elements of elementArias aren't strings, so I'd prefer a different name here:

Suggested change
{elementArias.map((ariaString, index) => (
{elementArias.map((aria, index) => (

const elementAttributes: Array<Attribute> = [];

const attributes = element.attributes;
for (const attribute of attributes) {
Copy link
Member

Choose a reason for hiding this comment

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

Why use a loop to get the attributes here? It seems like it might be simpler to look up the specific attributes we care about, e.g.

const ariaLabel = element.getAttribute("aria-label");

Then we'd have more direct control over the order in which the attributes are displayed.

I don't think an element can have multiple attributes with the same name, so this preserves behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah!! I'm so silly.

I tried that, but then my holiday brain thought that aria-describedby can be added multiple times, rather than it being one attribute that's space-separated strings 🤦🏽‍♀️

],
},
]);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test still passes if I change the code to use innerText instead of textContent, which is super weird. That's not how it behaves in practice. Not sure what I'm doing wrong. 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants