Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: make ProxyMenu more accessible to screen readers #11312

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

Merged
merged 18 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
chore: add stories and clean up tests
  • Loading branch information
Parkreiner committed Dec 21, 2023
commit 1402691e67c21f47eface6d6ca8cf77bbbc6cc00
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"idtoken",
"Iflag",
"incpatch",
"initialisms",
"ipnstate",
"isatty",
"Jobf",
Expand Down
52 changes: 52 additions & 0 deletions site/src/components/Abbr/Abbr.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Abbr } from "./Abbr";

const meta: Meta<typeof Abbr> = {
title: "components/Abbr",
component: Abbr,
decorators: [
(Story) => (
// Just here to make the abbreviated text part more obvious
<p css={{ textDecoration: "dotted" }}>
<Story />
</p>
),
],
};

export default meta;
type Story = StoryObj<typeof Abbr>;

export const Abbreviation: Story = {
args: {
initialism: false,
children: "NASA",
expandedText: "National Aeronautics and Space Administration",
},
};

export const Initialism: Story = {
args: {
initialism: true,
children: "CLI",
expandedText: "Command-Line Interface",
},
};

export const InlinedAbbreviation: Story = {
args: {
initialism: false,
children: "ms",
expandedText: "milliseconds",
},
decorators: [
(Story) => (
<p>
The physical pain of getting bonked on the head with a cartoon mallet
lasts precisely 593
<Story />. The emotional turmoil and complete embarrassment lasts
forever.
</p>
),
],
};
34 changes: 22 additions & 12 deletions site/src/components/Abbr/Abbr.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { render, screen } from "@testing-library/react";
import { Abbr } from "./Abbr";
Copy link
Member

Choose a reason for hiding this comment

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

🌠


type AbbrEntry = {
type Abbreviation = {
shortText: string;
fullText: string;
augmented?: string;
};

type Initialism = Abbreviation & {
spelledOut: string;
};

describe(Abbr.name, () => {
it("Does not change visual output compared <abbr> if text is not initialism", () => {
const sampleText: AbbrEntry[] = [
const sampleText: Abbreviation[] = [
{
shortText: "NASA",
fullText: "National Aeronautics and Space Administration",
Expand All @@ -33,7 +36,9 @@ describe(Abbr.name, () => {
];

for (const { shortText, fullText } of sampleText) {
const { unmount } = render(<Abbr title={fullText}>{shortText}</Abbr>);
const { unmount } = render(
<Abbr expandedText={fullText}>{shortText}</Abbr>,
);

const element = screen.getByTestId("abbr");
const matcher = new RegExp(`^${shortText}$`);
Expand All @@ -44,33 +49,38 @@ describe(Abbr.name, () => {
});

it("Augments pronunciation for screen readers if text is an initialism (but does not change visual output)", () => {
const sampleText: AbbrEntry[] = [
const sampleText: Initialism[] = [
{
shortText: "FBI",
fullText: "Federal Bureau of Investigations",
augmented: "F.B.I.",
fullText: "Federal Bureau of Investigation",
spelledOut: "F.B.I.",
},
{
shortText: "YMCA",
fullText: "Young Men's Christian Association",
augmented: "Y.M.C.A.",
spelledOut: "Y.M.C.A.",
},
{
shortText: "tbh",
fullText: "To be honest",
augmented: "T.B.H.",
spelledOut: "T.B.H.",
},
{
shortText: "CLI",
fullText: "Command-Line Interface",
spelledOut: "C.L.I.",
},
];

for (const { shortText, fullText, augmented } of sampleText) {
for (const { shortText, fullText, spelledOut: augmented } of sampleText) {
const { unmount } = render(
<Abbr title={fullText} initialism>
<Abbr initialism expandedText={fullText}>
{shortText}
</Abbr>,
);

const visuallyHidden = screen.getByTestId("visually-hidden");
expect(visuallyHidden).toHaveTextContent(augmented ?? "");
expect(visuallyHidden).toHaveTextContent(augmented);

const visualContent = screen.getByTestId("visual-only");
expect(visualContent).toHaveTextContent(shortText);
Expand Down
23 changes: 17 additions & 6 deletions site/src/components/Abbr/Abbr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,38 @@
* Features:
* - Better type-safety (requiring you to include certain properties)
* - All default styling is stripped away by default
* - Better control over how screen readers read the text
* - Better integration with screen readers, with more options for controlling
* how they read out initialisms.
*/
import { visuallyHidden } from "@mui/utils";
import { type FC } from "react";

type AbbrProps = {
title: string;
children: string;

// Not calling this "title" to make it clear that it doesn't have the same
// issues as the native title attribute as far as screen reader support
Copy link
Member

Choose a reason for hiding this comment

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

I don't know that this rename lends a lot of clarity, tbh - I might just use "title" so there's less cognitive overhead

Copy link
Member Author

@Parkreiner Parkreiner Jan 3, 2024

Choose a reason for hiding this comment

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

Yeah, I can see that. As I've been doing research, it seems that title is usually next to useless for screen readers, but reframing it like "the API is 99% the same as the native <abbr>, and we fixed the title prop" would make things more clear for other folks

expandedText: string;

initialism?: boolean;
Copy link
Member

@Kira-Pilot Kira-Pilot Jan 2, 2024

Choose a reason for hiding this comment

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

NIT: what do you think about the var names "tooltipText" (instead of "expandedText") and "hasAcronym" (instead of "initialism")? Expanded makes me think there is a dropdown or accordion and I had to google initialism.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, I think I've done a bad job of adding enough context:

  • I can see if there are some better alternatives for expandedText, but with toolTipText, it feels like it's putting much more emphasis on one type of user (sighted users who can see the tooltip), vs accommodating both them and screen-reader users. If a user's been blind all their life, they'll have no idea what a tooltip even is. Even in the WCAG spec, they call this "the expanded form of abbreviations"
  • The initialism prop is there to make sure that initialisms are pronounced like initialisms instead of just general acronyms/abbreviations. Like, NASA is an acronym, but you wouldn't want to pronounce it like an initialism, because then it'd be read "N-A-S-A"

Copy link
Member

Choose a reason for hiding this comment

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

Ah, thanks for those distinctions; makes sense. I like the proposal you came up with, i.e.
pronunciation?: "shorthand" | "acronym" | "initialism";

className?: string;
};

export const Abbr: FC<AbbrProps> = ({
title,
className,
children,
expandedText,
className,
initialism = false,
}) => {
return (
// Have to use test IDs instead of roles because traditional <abbr> elements
// have weird edge cases and aren't that accessible, so abbreviated roles
// usually aren't available in testing libraries
<abbr
title={title}
// Title attributes usually aren't natively available to screen readers;
// still have to inject text manually. Main value of titles here is
// letting sighted users hover over the abbreviation to see expanded text
title={expandedText}
css={[{ textDecoration: "inherit" }, className]}
data-testid="abbr"
>
Expand All @@ -37,6 +44,10 @@ export const Abbr: FC<AbbrProps> = ({
// without it affecting the visual output for sighted users
<>
<span css={{ ...visuallyHidden }} data-testid="visually-hidden">
{/*
* Once speakAs: "spell-out" has more browser support, that CSS
* property can be swapped in and clean up this code a lot
*/}
{initializeText(children)}
</span>

Expand All @@ -45,7 +56,7 @@ export const Abbr: FC<AbbrProps> = ({
</span>
</>
) : (
children
<span aria-label={expandedText}>{children}</span>
)}
</abbr>
);
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/Dashboard/Navbar/NavbarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
>
Workspace proxies improve terminal and web app connections to
workspaces. This does not apply to{" "}
<Abbr title="Command-Line Interface" initialism>
<Abbr initialism expandedText="Command-Line Interface">
CLI
</Abbr>{" "}
connections. A region must be manually selected, otherwise the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type FC } from "react";
import { getLatencyColor } from "utils/latency";
import CircularProgress from "@mui/material/CircularProgress";
import { visuallyHidden } from "@mui/utils";
import { Abbr } from "components/Abbr/Abbr";

interface ProxyStatusLatencyProps {
latency?: number;
Expand Down Expand Up @@ -56,9 +57,7 @@ export const ProxyStatusLatency: FC<ProxyStatusLatencyProps> = ({
<p css={{ color, fontSize: 13, margin: "0 0 0 auto" }}>
<span css={{ ...visuallyHidden }}>Latency: </span>
{latency.toFixed(0)}
<abbr title="milliseconds" css={{ textDecoration: "none" }}>
ms
</abbr>
<Abbr expandedText="milliseconds">ms</Abbr>
</p>
);
};