From 4e08a237aa7835794ac7fa90fadc61a69e506388 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 17:32:52 +0000 Subject: [PATCH 01/16] wip: commit progress on latency update --- site/src/components/Abbr/Abbr.test.tsx | 81 +++++++++++++++++++ site/src/components/Abbr/Abbr.tsx | 56 +++++++++++++ .../Dashboard/Navbar/NavbarView.tsx | 60 +++++++++----- .../ProxyStatusLatency/ProxyStatusLatency.tsx | 32 +++++--- 4 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 site/src/components/Abbr/Abbr.test.tsx create mode 100644 site/src/components/Abbr/Abbr.tsx diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx new file mode 100644 index 0000000000000..07639b1022c0b --- /dev/null +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from "@testing-library/react"; +import { Abbr } from "./Abbr"; + +type AbbrEntry = { + shortText: string; + fullText: string; + augmented?: string; +}; + +describe(Abbr.name, () => { + it("Does not change visual output compared if text is not initialism", () => { + const sampleText: AbbrEntry[] = [ + { + shortText: "NASA", + fullText: "National Aeronautics and Space Administration", + }, + { + shortText: "POTUS", + fullText: "President of the United States", + }, + { + shortText: "AWOL", + fullText: "Absent without Official Leave", + }, + { + shortText: "Laser", + fullText: "Light Amplification by Stimulated Emission of Radiation", + }, + { + shortText: "YOLO", + fullText: "You Only Live Once", + }, + ]; + + for (const { shortText, fullText } of sampleText) { + const { unmount } = render({shortText}); + + const element = screen.getByTestId("abbr"); + const matcher = new RegExp(`^${shortText}$`); + expect(element).toHaveTextContent(matcher); + + unmount(); + } + }); + + it("Augments pronunciation for screen readers if text is an initialism (but does not change visual output)", () => { + const sampleText: AbbrEntry[] = [ + { + shortText: "FBI", + fullText: "Federal Bureau of Investigations", + augmented: "F.B.I.", + }, + { + shortText: "YMCA", + fullText: "Young Men's Christian Association", + augmented: "Y.M.C.A.", + }, + { + shortText: "tbh", + fullText: "To be honest", + augmented: "T.B.H.", + }, + ]; + + for (const { shortText, fullText, augmented } of sampleText) { + const { unmount } = render( + + {shortText} + , + ); + + const visuallyHidden = screen.getByTestId("visually-hidden"); + expect(visuallyHidden).toHaveTextContent(augmented ?? ""); + + const visualContent = screen.getByTestId("visual-only"); + expect(visualContent).toHaveTextContent(shortText); + + unmount(); + } + }); +}); diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx new file mode 100644 index 0000000000000..7b2e067f93cb9 --- /dev/null +++ b/site/src/components/Abbr/Abbr.tsx @@ -0,0 +1,56 @@ +/** + * @file A more sophisticated version of the native element. + * + * 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 + */ +import { visuallyHidden } from "@mui/utils"; +import { type FC } from "react"; + +type AbbrProps = { + title: string; + children: string; + + initialism?: boolean; + className?: string; +}; + +export const Abbr: FC = ({ + title, + className, + children, + initialism = false, +}) => { + return ( + // Have to use test IDs instead of roles because traditional elements + // have weird edge cases and aren't that accessible, so abbreviated roles + // usually aren't available in testing libraries + + {initialism ? ( + // Helps make sure that screen readers read initialisms correctly + // without it affecting the visual output for sighted users + <> + + {initializeText(children)} + + + + {children} + + + ) : ( + children + )} + + ); +}; + +function initializeText(text: string): string { + return text.trim().toUpperCase().replaceAll(/\B/g, ".") + "."; +} diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index 8b37d41ba1d53..0b849f3cf79d0 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -18,6 +18,8 @@ import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLat import { CoderIcon } from "components/Icons/CoderIcon"; import { usePermissions } from "hooks/usePermissions"; import { UserDropdown } from "./UserDropdown/UserDropdown"; +import { visuallyHidden } from "@mui/utils"; +import { Abbr } from "components/Abbr/Abbr"; export const USERS_LINK = `/users?filter=${encodeURIComponent( "status:active", @@ -214,25 +216,22 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { const isLoadingLatencies = Object.keys(latencies).length === 0; const isLoading = proxyContextValue.isLoading || isLoadingLatencies; const permissions = usePermissions(); + const proxyLatencyLoading = (proxy: TypesGen.Region): boolean => { if (!refetchDate) { // Only show loading if the user manually requested a refetch return false; } - const latency = latencies?.[proxy.id]; // Only show a loading spinner if: - // - A latency exists. This means the latency was fetched at some point, so the - // loader *should* be resolved. + // - A latency exists. This means the latency was fetched at some point, so + // the loader *should* be resolved. // - The proxy is healthy. If it is not, the loader might never resolve. - // - The latency reported is older than the refetch date. This means the latency - // is stale and we should show a loading spinner until the new latency is - // fetched. - if (proxy.healthy && latency && latency.at < refetchDate) { - return true; - } - - return false; + // - The latency reported is older than the refetch date. This means the + // latency is stale and we should show a loading spinner until the new + // latency is fetched. + const latency = latencies[proxy.id]; + return proxy.healthy && latency !== undefined && latency.at < refetchDate; }; if (isLoading) { @@ -257,12 +256,18 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { "& .MuiSvgIcon-root": { fontSize: 14 }, }} > + + Latency for {selectedProxy?.display_name ?? "your region"} + + {selectedProxy ? (
= ({ proxyContextValue }) => { }} />
+ = ({ proxyContextValue }) => { "Select Proxy" )} +
= ({ proxyContextValue }) => { }} >

= ({ proxyContextValue }) => { > Select a region nearest to you

+

= ({ proxyContextValue }) => { }} > Workspace proxies improve terminal and web app connections to - workspaces. This does not apply to CLI connections. A region must be - manually selected, otherwise the default primary region will be - used. + workspaces. This does not apply to{" "} + + CLI + {" "} + connections. A region must be manually selected, otherwise the + default primary region will be used.

+ + {proxyContextValue.proxies ?.sort((a, b) => { const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity; @@ -329,6 +348,9 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { }) .map((proxy) => ( { if (!proxy.healthy) { displayError("Please select a healthy workspace proxy."); @@ -339,9 +361,6 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { proxyContextValue.setProxy(proxy); closeMenu(); }} - key={proxy.id} - selected={proxy.id === selectedProxy?.id} - css={{ fontSize: 14 }} >
= ({ proxyContextValue }) => { }} />
+ {proxy.display_name} + = ({ proxyContextValue }) => {
))} + + {Boolean(permissions.editWorkspaceProxies) && ( = ({ proxyContextValue }) => { Proxy settings )} + { diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx index 6776cf06e1f55..a6466ac5e7228 100644 --- a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx +++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx @@ -4,6 +4,7 @@ import Tooltip from "@mui/material/Tooltip"; import { type FC } from "react"; import { getLatencyColor } from "utils/latency"; import CircularProgress from "@mui/material/CircularProgress"; +import { visuallyHidden } from "@mui/utils"; interface ProxyStatusLatencyProps { latency?: number; @@ -33,22 +34,31 @@ export const ProxyStatusLatency: FC = ({ } if (!latency) { + const notAvailableText = "Latency not available"; return ( - - + + <> + {notAvailableText} + + + ); } return ( -
- {latency.toFixed(0)}ms -
+

+ Latency: + {latency.toFixed(0)} + + ms + +

); }; From 1402691e67c21f47eface6d6ca8cf77bbbc6cc00 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 19:57:01 +0000 Subject: [PATCH 02/16] chore: add stories and clean up tests --- .vscode/settings.json | 1 + site/src/components/Abbr/Abbr.stories.tsx | 52 +++++++++++++++++++ site/src/components/Abbr/Abbr.test.tsx | 34 +++++++----- site/src/components/Abbr/Abbr.tsx | 23 +++++--- .../Dashboard/Navbar/NavbarView.tsx | 2 +- .../ProxyStatusLatency/ProxyStatusLatency.tsx | 5 +- 6 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 site/src/components/Abbr/Abbr.stories.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index bcbdb7baeb9fa..3a5b401e1d992 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,7 @@ "idtoken", "Iflag", "incpatch", + "initialisms", "ipnstate", "isatty", "Jobf", diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx new file mode 100644 index 0000000000000..388cd13df0719 --- /dev/null +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Abbr } from "./Abbr"; + +const meta: Meta = { + title: "components/Abbr", + component: Abbr, + decorators: [ + (Story) => ( + // Just here to make the abbreviated text part more obvious +

+ +

+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +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) => ( +

+ The physical pain of getting bonked on the head with a cartoon mallet + lasts precisely 593 + . The emotional turmoil and complete embarrassment lasts + forever. +

+ ), + ], +}; diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index 07639b1022c0b..0ab96c497d7a0 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -1,15 +1,18 @@ import { render, screen } from "@testing-library/react"; import { Abbr } from "./Abbr"; -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 if text is not initialism", () => { - const sampleText: AbbrEntry[] = [ + const sampleText: Abbreviation[] = [ { shortText: "NASA", fullText: "National Aeronautics and Space Administration", @@ -33,7 +36,9 @@ describe(Abbr.name, () => { ]; for (const { shortText, fullText } of sampleText) { - const { unmount } = render({shortText}); + const { unmount } = render( + {shortText}, + ); const element = screen.getByTestId("abbr"); const matcher = new RegExp(`^${shortText}$`); @@ -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( - + {shortText} , ); const visuallyHidden = screen.getByTestId("visually-hidden"); - expect(visuallyHidden).toHaveTextContent(augmented ?? ""); + expect(visuallyHidden).toHaveTextContent(augmented); const visualContent = screen.getByTestId("visual-only"); expect(visualContent).toHaveTextContent(shortText); diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 7b2e067f93cb9..84b2be17b7d54 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -4,23 +4,27 @@ * 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 + expandedText: string; + initialism?: boolean; className?: string; }; export const Abbr: FC = ({ - title, - className, children, + expandedText, + className, initialism = false, }) => { return ( @@ -28,7 +32,10 @@ export const Abbr: FC = ({ // have weird edge cases and aren't that accessible, so abbreviated roles // usually aren't available in testing libraries @@ -37,6 +44,10 @@ export const Abbr: FC = ({ // without it affecting the visual output for sighted users <> + {/* + * Once speakAs: "spell-out" has more browser support, that CSS + * property can be swapped in and clean up this code a lot + */} {initializeText(children)} @@ -45,7 +56,7 @@ export const Abbr: FC = ({ ) : ( - children + {children} )} ); diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index 0b849f3cf79d0..c7c6665f4d640 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -330,7 +330,7 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { > Workspace proxies improve terminal and web app connections to workspaces. This does not apply to{" "} - + CLI {" "} connections. A region must be manually selected, otherwise the diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx index a6466ac5e7228..f239b7d411914 100644 --- a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx +++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx @@ -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; @@ -56,9 +57,7 @@ export const ProxyStatusLatency: FC = ({

Latency: {latency.toFixed(0)} - - ms - + ms

); }; From f0031449b5f31d65393a8a22a1f3c73772c7de48 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 20:01:34 +0000 Subject: [PATCH 03/16] refactor: clean up code --- site/src/components/Abbr/Abbr.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index 0ab96c497d7a0..15999584437cd 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -72,7 +72,7 @@ describe(Abbr.name, () => { }, ]; - for (const { shortText, fullText, spelledOut: augmented } of sampleText) { + for (const { shortText, fullText, spelledOut } of sampleText) { const { unmount } = render( {shortText} @@ -80,7 +80,7 @@ describe(Abbr.name, () => { ); const visuallyHidden = screen.getByTestId("visually-hidden"); - expect(visuallyHidden).toHaveTextContent(augmented); + expect(visuallyHidden).toHaveTextContent(spelledOut); const visualContent = screen.getByTestId("visual-only"); expect(visualContent).toHaveTextContent(shortText); From 5f58877a1401cacd700d48b30f34807abdbb19d6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 20:03:12 +0000 Subject: [PATCH 04/16] fix: make sure headers aren't treated as interactive elements --- site/src/components/Dashboard/Navbar/NavbarView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index c7c6665f4d640..a2367b8d5fc0c 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -309,6 +309,7 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { >

Date: Thu, 21 Dec 2023 20:12:13 +0000 Subject: [PATCH 05/16] refactor: clean up tests --- site/src/components/Abbr/Abbr.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index 15999584437cd..8cb46737a5850 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -41,9 +41,7 @@ describe(Abbr.name, () => { ); const element = screen.getByTestId("abbr"); - const matcher = new RegExp(`^${shortText}$`); - expect(element).toHaveTextContent(matcher); - + expect(element).toHaveTextContent(shortText); unmount(); } }); From b7d05de3f3fc4e34fb0405f5dc26e267dafd9366 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 20:26:42 +0000 Subject: [PATCH 06/16] fix: clean up stories --- site/src/components/Abbr/Abbr.stories.tsx | 12 ++++++++---- site/src/components/Abbr/Abbr.tsx | 13 ++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx index 388cd13df0719..8bf6fb994d939 100644 --- a/site/src/components/Abbr/Abbr.stories.tsx +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -6,10 +6,14 @@ const meta: Meta = { component: Abbr, decorators: [ (Story) => ( - // Just here to make the abbreviated text part more obvious -

- -

+ <> +

Try the following text out in a screen reader!

+ + {/* Just here to make the abbreviated text part more obvious */} +

+ +

+ ), ], }; diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 84b2be17b7d54..7203dfd0ac0ff 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -36,8 +36,15 @@ export const Abbr: FC = ({ // 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" + css={[ + { + textDecoration: "inherit", + // Rare case where this should be ems, not rems + letterSpacing: isAllUppercase(children) ? "0.05em" : "0", + }, + className, + ]} > {initialism ? ( // Helps make sure that screen readers read initialisms correctly @@ -65,3 +72,7 @@ export const Abbr: FC = ({ function initializeText(text: string): string { return text.trim().toUpperCase().replaceAll(/\B/g, ".") + "."; } + +function isAllUppercase(text: string): boolean { + return text === text.toUpperCase(); +} From c836057e2646082d2968442ca608635fdfb1fd77 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 20:28:01 +0000 Subject: [PATCH 07/16] docs: add clarifying comment --- site/src/components/Abbr/Abbr.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 7203dfd0ac0ff..2520c068907cc 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -48,7 +48,8 @@ export const Abbr: FC = ({ > {initialism ? ( // Helps make sure that screen readers read initialisms correctly - // without it affecting the visual output for sighted users + // without it affecting the visual output for sighted users (e.g., + // making sure "CLI" isn't read out as "klee") <> {/* From 4af5cecfd07599cfa0f50735852ff0789cc2b5dc Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 21 Dec 2023 20:40:41 +0000 Subject: [PATCH 08/16] fix: update stories again --- site/src/components/Abbr/Abbr.stories.tsx | 36 +++++++++++++++++------ site/src/components/Abbr/Abbr.tsx | 2 +- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx index 8bf6fb994d939..b09c17809078f 100644 --- a/site/src/components/Abbr/Abbr.stories.tsx +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -1,6 +1,12 @@ +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) => ( + {children} +); + const meta: Meta = { title: "components/Abbr", component: Abbr, @@ -8,11 +14,7 @@ const meta: Meta = { (Story) => ( <>

Try the following text out in a screen reader!

- - {/* Just here to make the abbreviated text part more obvious */} -

- -

+ ), ], @@ -27,6 +29,13 @@ export const Abbreviation: Story = { children: "NASA", expandedText: "National Aeronautics and Space Administration", }, + decorators: [ + (Story) => ( + + + + ), + ], }; export const Initialism: Story = { @@ -35,6 +44,13 @@ export const Initialism: Story = { children: "CLI", expandedText: "Command-Line Interface", }, + decorators: [ + (Story) => ( + + + + ), + ], }; export const InlinedAbbreviation: Story = { @@ -45,11 +61,13 @@ export const InlinedAbbreviation: Story = { }, decorators: [ (Story) => ( -

+

The physical pain of getting bonked on the head with a cartoon mallet - lasts precisely 593 - . The emotional turmoil and complete embarrassment lasts - forever. + lasts precisely 593{" "} + + + + . The emotional turmoil and complete embarrassment lasts forever.

), ], diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 2520c068907cc..8305c3b705008 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -41,7 +41,7 @@ export const Abbr: FC = ({ { textDecoration: "inherit", // Rare case where this should be ems, not rems - letterSpacing: isAllUppercase(children) ? "0.05em" : "0", + letterSpacing: isAllUppercase(children) ? "0.02em" : "0", }, className, ]} From 9a05ee1531b89a992a2063e3f929f25632fac42e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 22 Dec 2023 14:31:55 +0000 Subject: [PATCH 09/16] fix: clean up/extend prop definitions --- site/src/components/Abbr/Abbr.test.tsx | 2 +- site/src/components/Abbr/Abbr.tsx | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index 8cb46737a5850..ed375ad06a2ac 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -11,7 +11,7 @@ type Initialism = Abbreviation & { }; describe(Abbr.name, () => { - it("Does not change visual output compared if text is not initialism", () => { + it("Does not change semantics compared if text is not initialism", () => { const sampleText: Abbreviation[] = [ { shortText: "NASA", diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 8305c3b705008..124e24d387e55 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -8,24 +8,22 @@ * how they read out initialisms. */ import { visuallyHidden } from "@mui/utils"; -import { type FC } from "react"; - -type AbbrProps = { - children: string; +import { HTMLAttributes, type FC } from "react"; +// There isn't a dedicated HTMLAbbrElement type; have to go with base type +type AbbrProps = Exclude, "title" | "children"> & { // 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 expandedText: string; - + children: string; initialism?: boolean; - className?: string; }; export const Abbr: FC = ({ children, expandedText, - className, initialism = false, + ...delegatedProps }) => { return ( // Have to use test IDs instead of roles because traditional elements @@ -37,14 +35,12 @@ export const Abbr: FC = ({ // letting sighted users hover over the abbreviation to see expanded text title={expandedText} data-testid="abbr" - css={[ - { - textDecoration: "inherit", - // Rare case where this should be ems, not rems - letterSpacing: isAllUppercase(children) ? "0.02em" : "0", - }, - className, - ]} + css={{ + textDecoration: "inherit", + // Rare case where this should be ems, not rems + letterSpacing: isAllUppercase(children) ? "0.02em" : "0", + }} + {...delegatedProps} > {initialism ? ( // Helps make sure that screen readers read initialisms correctly From cdff5fbdb2d562f8128a51fb9c5abe80ecf37f47 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 22 Dec 2023 14:36:31 +0000 Subject: [PATCH 10/16] refactor: quick cleanup --- site/src/components/Abbr/Abbr.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 124e24d387e55..7d2f25c04f883 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -3,12 +3,12 @@ * * Features: * - Better type-safety (requiring you to include certain properties) - * - All default styling is stripped away by default + * - All built-in HTML styling is stripped away by default * - Better integration with screen readers, with more options for controlling * how they read out initialisms. */ import { visuallyHidden } from "@mui/utils"; -import { HTMLAttributes, type FC } from "react"; +import { type FC, type HTMLAttributes } from "react"; // There isn't a dedicated HTMLAbbrElement type; have to go with base type type AbbrProps = Exclude, "title" | "children"> & { @@ -47,14 +47,13 @@ export const Abbr: FC = ({ // without it affecting the visual output for sighted users (e.g., // making sure "CLI" isn't read out as "klee") <> + {/* + * Once speakAs: "spell-out" has more browser support, that CSS + * property can be swapped in and clean up this code a lot + */} - {/* - * Once speakAs: "spell-out" has more browser support, that CSS - * property can be swapped in and clean up this code a lot - */} {initializeText(children)} - {children} From fd643ae41828e743a51669e421d8290b4f701cd5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 3 Jan 2024 14:15:14 +0000 Subject: [PATCH 11/16] fix: apply Kira's feedback --- site/src/components/Abbr/Abbr.stories.tsx | 12 ++--- site/src/components/Abbr/Abbr.test.tsx | 6 +-- site/src/components/Abbr/Abbr.tsx | 52 +++++++++---------- .../Dashboard/Navbar/NavbarView.tsx | 2 +- .../ProxyStatusLatency/ProxyStatusLatency.tsx | 4 +- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx index b09c17809078f..63efee755d818 100644 --- a/site/src/components/Abbr/Abbr.stories.tsx +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -25,9 +25,9 @@ type Story = StoryObj; export const Abbreviation: Story = { args: { - initialism: false, + pronunciation: "acronym", children: "NASA", - expandedText: "National Aeronautics and Space Administration", + title: "National Aeronautics and Space Administration", }, decorators: [ (Story) => ( @@ -40,9 +40,9 @@ export const Abbreviation: Story = { export const Initialism: Story = { args: { - initialism: true, + pronunciation: "initialism", children: "CLI", - expandedText: "Command-Line Interface", + title: "Command-Line Interface", }, decorators: [ (Story) => ( @@ -55,9 +55,9 @@ export const Initialism: Story = { export const InlinedAbbreviation: Story = { args: { - initialism: false, + pronunciation: "acronym", children: "ms", - expandedText: "milliseconds", + title: "milliseconds", }, decorators: [ (Story) => ( diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index ed375ad06a2ac..ecb5a57ebcb54 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -36,9 +36,7 @@ describe(Abbr.name, () => { ]; for (const { shortText, fullText } of sampleText) { - const { unmount } = render( - {shortText}, - ); + const { unmount } = render({shortText}); const element = screen.getByTestId("abbr"); expect(element).toHaveTextContent(shortText); @@ -72,7 +70,7 @@ describe(Abbr.name, () => { for (const { shortText, fullText, spelledOut } of sampleText) { const { unmount } = render( - + {shortText} , ); diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 7d2f25c04f883..ee66f5e68bf01 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -4,62 +4,55 @@ * 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, with more options for controlling - * how they read out initialisms. + * - Better integration with screen readers (making the title prop available), + * with more options for controlling how they read out initialisms */ import { visuallyHidden } from "@mui/utils"; import { type FC, type HTMLAttributes } from "react"; -// There isn't a dedicated HTMLAbbrElement type; have to go with base type -type AbbrProps = Exclude, "title" | "children"> & { - // 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 - expandedText: string; +type AbbrProps = HTMLAttributes & { children: string; - initialism?: boolean; + title: string; + pronunciation?: "shorthand" | "acronym" | "initialism"; }; export const Abbr: FC = ({ children, - expandedText, - initialism = false, + title, + pronunciation = "shorthand", ...delegatedProps }) => { return ( - // Have to use test IDs instead of roles because traditional elements - // have weird edge cases and aren't that accessible, so abbreviated roles - // usually aren't available in testing libraries - {initialism ? ( - // Helps make sure that screen readers read initialisms correctly - // without it affecting the visual output for sighted users (e.g., - // making sure "CLI" isn't read out as "klee") + {pronunciation === "shorthand" ? ( + <>{children} + ) : ( + // Helps make sure that screen readers read initialisms/acronyms + // correctly without it affecting the visual output for sighted users + // (e.g., Mac VoiceOver reads "CLI" as "klee" by default) <> - {/* - * Once speakAs: "spell-out" has more browser support, that CSS - * property can be swapped in and clean up this code a lot - */} + {/*Can be simplified once CSS "spell-out" has more browser support*/} - {initializeText(children)} + {pronunciation === "initialism" + ? initializeText(children) + : flattenPronunciation(children)} + {children} - ) : ( - {children} )} ); @@ -69,6 +62,11 @@ 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(); +} + function isAllUppercase(text: string): boolean { return text === text.toUpperCase(); } diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index a2367b8d5fc0c..66c7c7fc22f17 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -331,7 +331,7 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { > Workspace proxies improve terminal and web app connections to workspaces. This does not apply to{" "} - + CLI {" "} connections. A region must be manually selected, otherwise the diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx index f239b7d411914..9a1a55af7cacb 100644 --- a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx +++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx @@ -57,7 +57,9 @@ export const ProxyStatusLatency: FC = ({

Latency: {latency.toFixed(0)} - ms + + ms +

); }; From 8853c5a2258ab07425662af97ebccdd1baa6746a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 7 Jan 2024 16:21:43 +0000 Subject: [PATCH 12/16] refactor: clean up abbr markup to account for pronunciation --- site/src/components/Abbr/Abbr.tsx | 42 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index ee66f5e68bf01..a06c2a1eb6aab 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -28,45 +28,41 @@ export const Abbr: FC = ({ // still have to inject text manually. Main value of titles here is // letting sighted users hover over the abbreviation to see the full term title={title} - data-testid="abbr" + data-testid="abbr-root" css={{ textDecoration: "inherit", letterSpacing: isAllUppercase(children) ? "0.02em" : "0", }} {...delegatedProps} > - {pronunciation === "shorthand" ? ( - <>{children} - ) : ( - // Helps make sure that screen readers read initialisms/acronyms - // correctly without it affecting the visual output for sighted users - // (e.g., Mac VoiceOver reads "CLI" as "klee" by default) - <> - {/*Can be simplified once CSS "spell-out" has more browser support*/} - - {pronunciation === "initialism" - ? initializeText(children) - : flattenPronunciation(children)} - + {/* + * Helps make sure that screen readers read initialisms/acronyms (e.g., + * making sure Mac VoiceOver doesn't read "CLI" as "klee") + * + * Can be simplified once CSS "spell-out" has more browser support + */} + + {pronunciation === "shorthand" && title} + {pronunciation === "acronym" && flattenPronunciation(children)} + {pronunciation === "initialism" && initializeText(children)} + - - {children} - - - )} + + {children} +
); }; -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(); } +function initializeText(text: string): string { + return text.trim().toUpperCase().replaceAll(/\B/g, ".") + "."; +} + function isAllUppercase(text: string): boolean { return text === text.toUpperCase(); } From 318903163c91ada2c95882289ba8183bb86c4785 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 7 Jan 2024 16:28:44 +0000 Subject: [PATCH 13/16] fix: more cleanup --- site/src/components/Abbr/Abbr.test.tsx | 18 +++++++++--------- site/src/components/Abbr/Abbr.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index ecb5a57ebcb54..a1de5f1a036f7 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -7,7 +7,7 @@ type Abbreviation = { }; type Initialism = Abbreviation & { - spelledOut: string; + initializedForm: string; }; describe(Abbr.name, () => { @@ -38,7 +38,7 @@ describe(Abbr.name, () => { for (const { shortText, fullText } of sampleText) { const { unmount } = render({shortText}); - const element = screen.getByTestId("abbr"); + const element = screen.getByTestId("abbr-root"); expect(element).toHaveTextContent(shortText); unmount(); } @@ -49,34 +49,34 @@ describe(Abbr.name, () => { { shortText: "FBI", fullText: "Federal Bureau of Investigation", - spelledOut: "F.B.I.", + initializedForm: "F.B.I.", }, { shortText: "YMCA", fullText: "Young Men's Christian Association", - spelledOut: "Y.M.C.A.", + initializedForm: "Y.M.C.A.", }, { shortText: "tbh", fullText: "To be honest", - spelledOut: "T.B.H.", + initializedForm: "T.B.H.", }, { shortText: "CLI", fullText: "Command-Line Interface", - spelledOut: "C.L.I.", + initializedForm: "C.L.I.", }, ]; - for (const { shortText, fullText, spelledOut } of sampleText) { + for (const { shortText, fullText, initializedForm } of sampleText) { const { unmount } = render( {shortText} , ); - const visuallyHidden = screen.getByTestId("visually-hidden"); - expect(visuallyHidden).toHaveTextContent(spelledOut); + const visuallyHidden = screen.getByTestId("abbr-screen-readers"); + expect(visuallyHidden).toHaveTextContent(initializedForm); const visualContent = screen.getByTestId("visual-only"); expect(visualContent).toHaveTextContent(shortText); diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index a06c2a1eb6aab..3fb41d6c15667 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -5,7 +5,7 @@ * - Better type-safety (requiring you to include certain properties) * - All built-in HTML styling is stripped away by default * - Better integration with screen readers (making the title prop available), - * with more options for controlling how they read out initialisms + * with more options for influencing how they read out initialisms */ import { visuallyHidden } from "@mui/utils"; import { type FC, type HTMLAttributes } from "react"; From 8f3820817b319bddba5dbc110974e1abcbc658ff Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 7 Jan 2024 16:56:46 +0000 Subject: [PATCH 14/16] fix: refine screen reader output for VoiceOver --- site/src/components/Abbr/Abbr.tsx | 48 +++++++++---------- .../ProxyStatusLatency/ProxyStatusLatency.tsx | 4 +- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index 3fb41d6c15667..ebcf54374e580 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -1,13 +1,3 @@ -/** - * @file A more sophisticated version of the native 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 (making the title prop available), - * with more options for influencing how they read out initialisms - */ -import { visuallyHidden } from "@mui/utils"; import { type FC, type HTMLAttributes } from "react"; type AbbrProps = HTMLAttributes & { @@ -16,18 +6,36 @@ type AbbrProps = HTMLAttributes & { pronunciation?: "shorthand" | "acronym" | "initialism"; }; +/** + * A more sophisticated version of the native 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 = ({ children, - title, + title: visualTitle, pronunciation = "shorthand", ...delegatedProps }) => { + let screenReaderTitle: string; + if (pronunciation === "initialism") { + screenReaderTitle = `${visualTitle} (${initializeText(children)})`; + } else if (pronunciation === "acronym") { + screenReaderTitle = `${visualTitle} (${flattenPronunciation(children)})`; + } else { + screenReaderTitle = visualTitle; + } + return ( = ({ }} {...delegatedProps} > - {/* - * Helps make sure that screen readers read initialisms/acronyms (e.g., - * making sure Mac VoiceOver doesn't read "CLI" as "klee") - * - * Can be simplified once CSS "spell-out" has more browser support - */} - - {pronunciation === "shorthand" && title} - {pronunciation === "acronym" && flattenPronunciation(children)} - {pronunciation === "initialism" && initializeText(children)} - - {children} diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx index 9a1a55af7cacb..f139314f05748 100644 --- a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx +++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx @@ -57,9 +57,7 @@ export const ProxyStatusLatency: FC = ({

Latency: {latency.toFixed(0)} - - ms - + ms

); }; From d5340994232bb6e77615d13c07d6565c967c4dc7 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 7 Jan 2024 17:28:45 +0000 Subject: [PATCH 15/16] refactor: clean up and redefine tests --- site/src/components/Abbr/Abbr.test.tsx | 118 ++++++++++++++----------- site/src/components/Abbr/Abbr.tsx | 19 ++-- 2 files changed, 73 insertions(+), 64 deletions(-) diff --git a/site/src/components/Abbr/Abbr.test.tsx b/site/src/components/Abbr/Abbr.test.tsx index a1de5f1a036f7..58e37287f6011 100644 --- a/site/src/components/Abbr/Abbr.test.tsx +++ b/site/src/components/Abbr/Abbr.test.tsx @@ -1,87 +1,97 @@ import { render, screen } from "@testing-library/react"; -import { Abbr } from "./Abbr"; +import { Abbr, type Pronunciation } from "./Abbr"; -type Abbreviation = { - shortText: string; - fullText: string; +type AbbreviationData = { + abbreviation: string; + title: string; + expectedLabel: string; }; -type Initialism = Abbreviation & { - initializedForm: string; +type AssertionInput = AbbreviationData & { + pronunciation: Pronunciation; }; +function assertAccessibleLabel({ + abbreviation, + title, + expectedLabel, + pronunciation, +}: AssertionInput) { + const { unmount } = render( + + {abbreviation} + , + ); + + screen.getByLabelText(expectedLabel, { selector: "abbr" }); + unmount(); +} + describe(Abbr.name, () => { - it("Does not change semantics compared if text is not initialism", () => { - const sampleText: Abbreviation[] = [ + it("Has an aria-label that equals the title if the abbreviation is shorthand", () => { + const sampleShorthands: AbbreviationData[] = [ { - shortText: "NASA", - fullText: "National Aeronautics and Space Administration", + abbreviation: "ms", + title: "milliseconds", + expectedLabel: "milliseconds", }, { - shortText: "POTUS", - fullText: "President of the United States", + 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[] = [ { - shortText: "AWOL", - fullText: "Absent without Official Leave", + abbreviation: "NASA", + title: "National Aeronautics and Space Administration", + expectedLabel: "Nasa (National Aeronautics and Space Administration)", }, { - shortText: "Laser", - fullText: "Light Amplification by Stimulated Emission of Radiation", + abbreviation: "AWOL", + title: "Absent without Official Leave", + expectedLabel: "Awol (Absent without Official Leave)", }, { - shortText: "YOLO", - fullText: "You Only Live Once", + abbreviation: "YOLO", + title: "You Only Live Once", + expectedLabel: "Yolo (You Only Live Once)", }, ]; - for (const { shortText, fullText } of sampleText) { - const { unmount } = render({shortText}); - - const element = screen.getByTestId("abbr-root"); - expect(element).toHaveTextContent(shortText); - unmount(); + for (const acronym of sampleAcronyms) { + assertAccessibleLabel({ ...acronym, pronunciation: "acronym" }); } }); - it("Augments pronunciation for screen readers if text is an initialism (but does not change visual output)", () => { - const sampleText: Initialism[] = [ - { - shortText: "FBI", - fullText: "Federal Bureau of Investigation", - initializedForm: "F.B.I.", - }, + it("Has an aria label with title and initialized pronunciation if abbreviation is initialism", () => { + const sampleInitialisms: AbbreviationData[] = [ { - shortText: "YMCA", - fullText: "Young Men's Christian Association", - initializedForm: "Y.M.C.A.", + abbreviation: "FBI", + title: "Federal Bureau of Investigation", + expectedLabel: "F.B.I. (Federal Bureau of Investigation)", }, { - shortText: "tbh", - fullText: "To be honest", - initializedForm: "T.B.H.", + abbreviation: "YMCA", + title: "Young Men's Christian Association", + expectedLabel: "Y.M.C.A. (Young Men's Christian Association)", }, { - shortText: "CLI", - fullText: "Command-Line Interface", - initializedForm: "C.L.I.", + abbreviation: "CLI", + title: "Command-Line Interface", + expectedLabel: "C.L.I. (Command-Line Interface)", }, ]; - for (const { shortText, fullText, initializedForm } of sampleText) { - const { unmount } = render( - - {shortText} - , - ); - - const visuallyHidden = screen.getByTestId("abbr-screen-readers"); - expect(visuallyHidden).toHaveTextContent(initializedForm); - - const visualContent = screen.getByTestId("visual-only"); - expect(visualContent).toHaveTextContent(shortText); - - unmount(); + for (const initialism of sampleInitialisms) { + assertAccessibleLabel({ ...initialism, pronunciation: "initialism" }); } }); }); diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index ebcf54374e580..fc5d1b108f4d0 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -1,9 +1,11 @@ import { type FC, type HTMLAttributes } from "react"; +export type Pronunciation = "shorthand" | "acronym" | "initialism"; + type AbbrProps = HTMLAttributes & { children: string; title: string; - pronunciation?: "shorthand" | "acronym" | "initialism"; + pronunciation?: Pronunciation; }; /** @@ -21,13 +23,13 @@ export const Abbr: FC = ({ pronunciation = "shorthand", ...delegatedProps }) => { - let screenReaderTitle: string; + let screenReaderLabel: string; if (pronunciation === "initialism") { - screenReaderTitle = `${visualTitle} (${initializeText(children)})`; + screenReaderLabel = `${initializeText(children)} (${visualTitle})`; } else if (pronunciation === "acronym") { - screenReaderTitle = `${visualTitle} (${flattenPronunciation(children)})`; + screenReaderLabel = `${flattenPronunciation(children)} (${visualTitle})`; } else { - screenReaderTitle = visualTitle; + screenReaderLabel = visualTitle; } return ( @@ -35,17 +37,14 @@ export const Abbr: FC = ({ // Title attributes usually aren't natively available to screen readers; // always have to supplement with aria-label title={visualTitle} - aria-label={screenReaderTitle} - data-testid="abbr-root" + aria-label={screenReaderLabel} css={{ textDecoration: "inherit", letterSpacing: isAllUppercase(children) ? "0.02em" : "0", }} {...delegatedProps} > - - {children} - + {children} ); }; From 03b2ac103d194b3d6c18818b0767b5a1ed6ce1c7 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 7 Jan 2024 20:02:47 +0000 Subject: [PATCH 16/16] feature: add finishing touches --- site/src/components/Abbr/Abbr.stories.tsx | 42 +++++++++++------------ site/src/components/Abbr/Abbr.tsx | 39 +++++++++++---------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx index 63efee755d818..b47546dcb05ce 100644 --- a/site/src/components/Abbr/Abbr.stories.tsx +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -23,7 +23,27 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Abbreviation: Story = { +export const InlinedShorthand: Story = { + args: { + pronunciation: "shorthand", + children: "ms", + title: "milliseconds", + }, + decorators: [ + (Story) => ( +

+ The physical pain of getting bonked on the head with a cartoon mallet + lasts precisely 593{" "} + + + + . The emotional turmoil and complete embarrassment lasts forever. +

+ ), + ], +}; + +export const Acronym: Story = { args: { pronunciation: "acronym", children: "NASA", @@ -52,23 +72,3 @@ export const Initialism: Story = { ), ], }; - -export const InlinedAbbreviation: Story = { - args: { - pronunciation: "acronym", - children: "ms", - title: "milliseconds", - }, - decorators: [ - (Story) => ( -

- The physical pain of getting bonked on the head with a cartoon mallet - lasts precisely 593{" "} - - - - . The emotional turmoil and complete embarrassment lasts forever. -

- ), - ], -}; diff --git a/site/src/components/Abbr/Abbr.tsx b/site/src/components/Abbr/Abbr.tsx index fc5d1b108f4d0..9fba0618a57cf 100644 --- a/site/src/components/Abbr/Abbr.tsx +++ b/site/src/components/Abbr/Abbr.tsx @@ -19,28 +19,19 @@ type AbbrProps = HTMLAttributes & { */ export const Abbr: FC = ({ children, - title: visualTitle, + title, pronunciation = "shorthand", ...delegatedProps }) => { - let screenReaderLabel: string; - if (pronunciation === "initialism") { - screenReaderLabel = `${initializeText(children)} (${visualTitle})`; - } else if (pronunciation === "acronym") { - screenReaderLabel = `${flattenPronunciation(children)} (${visualTitle})`; - } else { - screenReaderLabel = visualTitle; - } - return ( @@ -49,15 +40,27 @@ export const Abbr: FC = ({ ); }; -function flattenPronunciation(text: string): string { - const trimmed = text.trim(); - return (trimmed[0] ?? "").toUpperCase() + trimmed.slice(1).toLowerCase(); +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 isAllUppercase(text: string): boolean { - return text === text.toUpperCase(); +function flattenPronunciation(text: string): string { + const trimmed = text.trim(); + return (trimmed[0] ?? "").toUpperCase() + trimmed.slice(1).toLowerCase(); }