1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-04-17 10:28:46 -04:00

fix: improve accessibility across components

- Add aria-hidden to decorative SVG icons
- Add title attributes to iframe embeds (CodePen, Gist, YouTube)
- Add aria-labels to comment form textareas
- Use proper button element for Markdown help popover trigger
- Use proper ellipsis character in placeholders

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 21:38:39 -05:00
parent cd57a9c4dd
commit 013311a618
6 changed files with 29 additions and 11 deletions

View File

@@ -101,7 +101,7 @@ const Page = async () => {
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-2 hover:no-underline"
>
<StarIcon className="inline-block size-4 shrink-0" />
<StarIcon className="inline-block size-4 shrink-0" aria-hidden="true" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
</a>
)}
@@ -114,7 +114,7 @@ const Page = async () => {
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-2 hover:no-underline"
>
<GitForkIcon className="inline-block size-4" />
<GitForkIcon className="inline-block size-4" aria-hidden="true" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
</a>
)}

View File

@@ -73,16 +73,19 @@ const CommentTextarea = ({
setContent,
isPending,
placeholder,
ariaLabel,
}: {
content: string;
setContent: (value: string) => void;
isPending: boolean;
placeholder: string;
ariaLabel: string;
}) => (
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder={placeholder}
aria-label={ariaLabel}
className="min-h-[4lh] w-full"
disabled={isPending}
/>
@@ -131,11 +134,14 @@ const MarkdownHelp = () => (
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" />
<span className="max-md:hidden">Basic&nbsp;</span>
<Popover>
<PopoverTrigger>
<span className="text-primary decoration-primary/40 cursor-pointer font-semibold no-underline decoration-2 underline-offset-4 hover:underline">
<PopoverTrigger asChild>
<button
type="button"
className="text-primary decoration-primary/40 cursor-pointer font-semibold no-underline decoration-2 underline-offset-4 hover:underline"
>
<span>Markdown</span>
<span className="max-md:hidden">&nbsp;syntax</span>
</span>
</button>
</PopoverTrigger>
<PopoverContent align="start">
<p className="text-sm leading-loose">
@@ -216,7 +222,8 @@ const NewCommentForm = ({ slug }: { slug: string }) => {
content={content}
setContent={setContent}
isPending={isPending}
placeholder="Write your thoughts..."
placeholder="Write your thoughts"
ariaLabel="Write a comment"
/>
<div className="flex justify-between gap-4">
@@ -277,7 +284,8 @@ const ReplyForm = ({
content={content}
setContent={setContent}
isPending={isPending}
placeholder="Reply to this comment..."
placeholder="Reply to this comment"
ariaLabel="Write a reply"
/>
<div className="flex justify-end gap-2">
@@ -338,7 +346,8 @@ const EditCommentForm = ({
content={content}
setContent={setContent}
isPending={isPending}
placeholder="Edit your comment..."
placeholder="Edit your comment"
ariaLabel="Edit your comment"
/>
<div className="flex justify-end gap-2">

View File

@@ -8,6 +8,7 @@ export const Win95Icon = ({ className }: { className?: string }) => (
strokeWidth="0"
viewBox="0 0 24 24"
className={className}
aria-hidden="true"
>
<path d="M5.712 1.596l-.756.068-.238.55.734-.017zm1.39.927l-.978.137-.326.807.96-.12.345-.824zM4.89 3.535l-.72.05-.24.567.721-.017zm3.724.309l-1.287.068-.394.96 1.27-.052zm1.87.566l-1.579.069-.566 1.357 1.596-.088.548-1.338zm-4.188.037l-.977.153-.343.806.976-.12zm6.144.668l-1.87.135-.637 1.527 1.87-.154zm2.925.219c-.11 0-.222 0-.334.002l-.767 1.85c1.394-.03 2.52.089 3.373.38l-1.748 4.201c-.955-.304-2.082-.444-3.36-.394l-.54 1.305a8.762 8.762 0 0 1 3.365.396l-1.663 4.014c-1.257-.27-2.382-.395-3.387-.344l-.782 1.887c3.363-.446 6.348.822 9.009 3.773L24 9.23c-2.325-2.575-5.2-3.88-8.637-3.896zm-.644.002l-2.024.12-.687 1.68 2.025-.19zm-10.603.05l-.719.036-.224.566h.703l.24-.601zm3.69.397l-1.287.069-.395.959 1.27-.05zM5.54 6.3l-.994.154-.344.807.98-.121zm4.137.066l-1.58.069L7.53 7.77l1.596-.085.55-1.32zm1.955.688l-1.87.135-.636 1.527 1.887-.154zm2.282.19l-2.01.136-.7 1.682 2.04-.19.67-1.63zm-10.57.066l-.739.035-.238.564h.72l.257-.6zm3.705.293l-1.303.085-.394.96 1.287-.034zm11.839.255a6.718 6.718 0 0 1 2.777 1.717l-1.75 4.237c-.617-.584-1.15-.961-1.611-1.149l-1.201-.498zM4.733 8.22l-.976.154-.344.807.961-.12.36-.841zm4.186 0l-1.594.052-.549 1.354L8.37 9.54zm1.957.668L8.99 9.04l-.619 1.508 1.87-.135.636-1.527zm2.247.275l-2.007.12-.703 1.665 2.042-.156zM2.52 9.267l-.718.033-.24.549.718-.016zm3.725.273l-1.289.07-.41.96 1.287-.03.412-1zm1.87.6l-1.596.05-.55 1.356 1.598-.084.547-1.322zm-4.186.037l-.979.136-.324.805.96-.119zm6.14.633l-1.87.154-.653 1.527 1.906-.154zm2.267.275l-2.026.12-.686 1.663 2.025-.172zm-10.569.031l-.739.037-.238.565.72-.016zm3.673.362l-1.289.068-.41.978 1.305-.05zm-2.285.533l-.976.154-.326.805.96-.12.342-.84zm4.153.07l-1.596.066-.565 1.356 1.612-.084zm1.957.666l-1.889.154-.617 1.526 1.886-.15zm2.28.223l-2.025.12-.685 1.665 2.041-.172.67-1.613zm-10.584.05l-.738.053L0 13.64l.72-.02.24-.6zm3.705.31l-1.285.07-.395.976 1.287-.05.393-.997zm11.923.07c1.08.29 2.024.821 2.814 1.613l-1.715 4.183c-.892-.754-1.82-1.32-2.814-1.664l1.715-4.133zm-10.036.515L4.956 14l-.549 1.32 1.578-.066.567-1.338zm-4.184.014l-.996.156-.309.79.961-.106zm6.14.67l-1.904.154-.617 1.527 1.89-.154.632-1.527zm2.231.324l-2.025.123-.686 1.682 2.026-.174zm-6.863.328l-1.3.068-.397.98 1.285-.054zm1.871.584l-1.578.068-.566 1.334 1.595-.064zm1.953.701l-1.867.137-.635 1.51 1.87-.137zm2.23.31l-2.005.122-.703 1.68 2.04-.19.67-1.61z" />
</svg>
@@ -21,6 +22,7 @@ export const MarkdownIcon = ({ className }: { className?: string }) => (
strokeWidth="0"
viewBox="0 0 24 24"
className={className}
aria-hidden="true"
>
<path d="M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z" />
</svg>
@@ -34,6 +36,7 @@ export const GitHubIcon = ({ className }: { className?: string }) => (
strokeWidth="0"
viewBox="0 0 24 24"
className={className}
aria-hidden="true"
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
@@ -47,6 +50,7 @@ export const NextjsIcon = ({ className }: { className?: string }) => (
strokeWidth="0"
viewBox="0 0 24 24"
className={className}
aria-hidden="true"
>
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
</svg>

View File

@@ -6,6 +6,7 @@ const CodePen = ({
defaultTab = "html",
preview = true,
editable = false,
title = "CodePen embed",
className,
...rest
}: {
@@ -14,6 +15,7 @@ const CodePen = ({
defaultTab?: string;
preview?: boolean;
editable?: boolean;
title?: string;
} & React.ComponentProps<"iframe">) => {
return (
<iframe
@@ -22,6 +24,7 @@ const CodePen = ({
preview: `${!!preview}`,
editable: `${!!editable}`,
})}`}
title={title}
className={cn("h-[500px] w-full overflow-hidden border-none", className)}
{...rest}
/>

View File

@@ -4,9 +4,10 @@ import { cn } from "@/lib/utils";
const Gist = async ({
id,
file,
title,
className,
...rest
}: { id: string; file?: string } & React.ComponentProps<"iframe">) => {
}: { id: string; file?: string; title?: string } & React.ComponentProps<"iframe">) => {
"use cache";
cacheLife("max");
cacheTag("gist", `gist-${id}${file ? `-${file}` : ""}`);
@@ -44,6 +45,7 @@ const Gist = async ({
scrolling="no"
id={iframeId}
srcDoc={iframeHtml}
title={title || `GitHub Gist ${id}${file ? ` - ${file}` : ""}`}
className={cn("overflow-hidden border-none", className)}
{...rest}
suppressHydrationWarning

View File

@@ -4,8 +4,8 @@ import YouTubeEmbed from "react-lite-youtube-embed";
// lite-youtube-embed CSS is imported in app/global.css to save a request
const YouTube = ({ ...rest }: Omit<React.ComponentProps<typeof YouTubeEmbed>, "title">) => {
return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />;
const YouTube = ({ title = "YouTube video", ...rest }: React.ComponentProps<typeof YouTubeEmbed>) => {
return <YouTubeEmbed cookie={false} containerElement="div" title={title} {...rest} />;
};
export { YouTube };