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 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 .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
74 changes: 74 additions & 0 deletions site/src/components/Abbr/Abbr.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { type PropsWithChildren } from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { Abbr } from "./Abbr";

// Just here to make the abbreviated part more obvious in the component library
const Underline = ({ children }: PropsWithChildren) => (
<span css={{ textDecoration: "underline dotted" }}>{children}</span>
);

const meta: Meta<typeof Abbr> = {
title: "components/Abbr",
component: Abbr,
decorators: [
(Story) => (
<>
<p>Try the following text out in a screen reader!</p>
<Story />
</>
),
],
};

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

export const InlinedShorthand: Story = {
args: {
pronunciation: "shorthand",
children: "ms",
title: "milliseconds",
},
decorators: [
(Story) => (
<p css={{ maxWidth: "40em" }}>
The physical pain of getting bonked on the head with a cartoon mallet
lasts precisely 593{" "}
<Underline>
<Story />
</Underline>
. The emotional turmoil and complete embarrassment lasts forever.
</p>
),
],
};

export const Acronym: Story = {
args: {
pronunciation: "acronym",
children: "NASA",
title: "National Aeronautics and Space Administration",
},
decorators: [
(Story) => (
<Underline>
<Story />
</Underline>
),
],
};

export const Initialism: Story = {
args: {
pronunciation: "initialism",
children: "CLI",
title: "Command-Line Interface",
},
decorators: [
(Story) => (
<Underline>
<Story />
</Underline>
),
],
};
97 changes: 97 additions & 0 deletions site/src/components/Abbr/Abbr.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen } from "@testing-library/react";
import { Abbr, type Pronunciation } from "./Abbr";

type AbbreviationData = {
abbreviation: string;
title: string;
expectedLabel: string;
};

type AssertionInput = AbbreviationData & {
pronunciation: Pronunciation;
};

function assertAccessibleLabel({
abbreviation,
title,
expectedLabel,
pronunciation,
}: AssertionInput) {
const { unmount } = render(
<Abbr title={title} pronunciation={pronunciation}>
{abbreviation}
</Abbr>,
);

screen.getByLabelText(expectedLabel, { selector: "abbr" });
unmount();
}

describe(Abbr.name, () => {
it("Has an aria-label that equals the title if the abbreviation is shorthand", () => {
const sampleShorthands: AbbreviationData[] = [
{
abbreviation: "ms",
title: "milliseconds",
expectedLabel: "milliseconds",
},
{
abbreviation: "g",
title: "grams",
expectedLabel: "grams",
},
];

for (const shorthand of sampleShorthands) {
assertAccessibleLabel({ ...shorthand, pronunciation: "shorthand" });
}
});

it("Has an aria label with title and 'flattened' pronunciation if abbreviation is acronym", () => {
const sampleAcronyms: AbbreviationData[] = [
{
abbreviation: "NASA",
title: "National Aeronautics and Space Administration",
expectedLabel: "Nasa (National Aeronautics and Space Administration)",
},
{
abbreviation: "AWOL",
title: "Absent without Official Leave",
expectedLabel: "Awol (Absent without Official Leave)",
},
{
abbreviation: "YOLO",
title: "You Only Live Once",
expectedLabel: "Yolo (You Only Live Once)",
},
];

for (const acronym of sampleAcronyms) {
assertAccessibleLabel({ ...acronym, pronunciation: "acronym" });
}
});

it("Has an aria label with title and initialized pronunciation if abbreviation is initialism", () => {
const sampleInitialisms: AbbreviationData[] = [
{
abbreviation: "FBI",
title: "Federal Bureau of Investigation",
expectedLabel: "F.B.I. (Federal Bureau of Investigation)",
},
{
abbreviation: "YMCA",
title: "Young Men's Christian Association",
expectedLabel: "Y.M.C.A. (Young Men's Christian Association)",
},
{
abbreviation: "CLI",
title: "Command-Line Interface",
expectedLabel: "C.L.I. (Command-Line Interface)",
},
];

for (const initialism of sampleInitialisms) {
assertAccessibleLabel({ ...initialism, pronunciation: "initialism" });
}
});
});
66 changes: 66 additions & 0 deletions site/src/components/Abbr/Abbr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type FC, type HTMLAttributes } from "react";

export type Pronunciation = "shorthand" | "acronym" | "initialism";

type AbbrProps = HTMLAttributes<HTMLElement> & {
children: string;
title: string;
pronunciation?: Pronunciation;
};

/**
Copy link
Member

Choose a reason for hiding this comment

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

There are a lot of comments in here for a relatively small component. Are there any we can shorten?

* A more sophisticated version of the native <abbr> element.
*
* Features:
* - Better type-safety (requiring you to include certain properties)
* - All built-in HTML styling is stripped away by default
* - Better integration with screen readers (like exposing the title prop to
* them), with more options for influencing how they pronounce text
*/
export const Abbr: FC<AbbrProps> = ({
children,
title,
pronunciation = "shorthand",
...delegatedProps
}) => {
return (
<abbr
// Title attributes usually aren't natively available to screen readers;
// always have to supplement with aria-label
title={title}
aria-label={getAccessibleLabel(children, title, pronunciation)}
css={{
textDecoration: "inherit",
letterSpacing: children === children.toUpperCase() ? "0.02em" : "0",
}}
{...delegatedProps}
>
<span aria-hidden>{children}</span>
</abbr>
);
};

function getAccessibleLabel(
abbreviation: string,
title: string,
pronunciation: Pronunciation,
): string {
if (pronunciation === "initialism") {
return `${initializeText(abbreviation)} (${title})`;
}

if (pronunciation === "acronym") {
return `${flattenPronunciation(abbreviation)} (${title})`;
}

return title;
}

function initializeText(text: string): string {
return text.trim().toUpperCase().replaceAll(/\B/g, ".") + ".";
}

function flattenPronunciation(text: string): string {
const trimmed = text.trim();
return (trimmed[0] ?? "").toUpperCase() + trimmed.slice(1).toLowerCase();
}
Loading