feat: update links for improved accessibility

This commit is contained in:
Amruth Pillai
2026-05-26 13:09:30 +02:00
parent dd1e37e579
commit 8da780c868
43 changed files with 952 additions and 189 deletions
+2
View File
@@ -11,6 +11,7 @@
},
"scripts": {
"shadcn": "shadcn",
"doctor": "react-doctor",
"typecheck": "tsgo --noEmit",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
@@ -43,6 +44,7 @@
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260526.1",
"postcss": "^8.5.15",
"react-doctor": "^0.2.6",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
+2 -2
View File
@@ -32,8 +32,8 @@ describe("Badge", () => {
});
it("supports a custom render function", () => {
render(<Badge render={(props) => <a {...props} href="/x" />}>link</Badge>);
const anchor = screen.getByRole("link", { name: "link" });
render(<Badge render={(props) => <a {...props} href="/x" aria-label="View profile" />}>View profile</Badge>);
const anchor = screen.getByRole("link", { name: "View profile" });
expect(anchor).toBeInTheDocument();
});
});
+2 -1
View File
@@ -18,9 +18,10 @@ type FormItemProps = React.ComponentProps<"div"> & { hasError?: boolean };
function FormItem({ className, hasError = false, ...props }: FormItemProps) {
const id = React.useId();
const contextValue = React.useMemo<FormItemContextValue>(() => ({ id, hasError }), [hasError, id]);
return (
<FormItemContext.Provider value={{ id, hasError }}>
<FormItemContext.Provider value={contextValue}>
<div data-slot="form-item" className={cn("grid gap-1.5", className)} {...props} />
</FormItemContext.Provider>
);
@@ -49,7 +49,6 @@ function InputGroupAddon({
className={cn(inputGroupAddonVariants({ align }), className)}
onKeyDown={(e) => {
if (!(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
// Only respond to Space or Enter
if (e.key !== " " && e.key !== "Enter") return;
if (!(e.target as HTMLElement).closest("button")) {
e.preventDefault();
+1
View File
@@ -268,6 +268,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
return (
<button
type="button"
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
+3 -1
View File
@@ -61,8 +61,10 @@ export function ConfirmDialogProvider({ children }: { children: React.ReactNode
setState((prev) => ({ ...prev, open: false, resolve: null }));
}, [state.resolve]);
const contextValue = React.useMemo<ConfirmContextType>(() => ({ confirm }), [confirm]);
return (
<ConfirmContext.Provider value={{ confirm }}>
<ConfirmContext.Provider value={contextValue}>
{children}
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
interface CommonControlledStateProps<T> {
value?: T;
@@ -13,19 +13,17 @@ export function useControlledState<T, Rest extends unknown[] = []>(
props: UseControlledStateProps<T, Rest>,
): readonly [T, (next: T, ...args: Rest) => void] {
const { value, defaultValue, onChange } = props;
const isControlled = value !== undefined;
const [state, setInternalState] = useState<T>(value !== undefined ? value : (defaultValue as T));
useEffect(() => {
if (value !== undefined) setInternalState(value);
}, [value]);
const [internalState, setInternalState] = useState<T>(value !== undefined ? value : (defaultValue as T));
const state = isControlled ? (value as T) : internalState;
const setState = useCallback(
(next: T, ...args: Rest) => {
setInternalState(next);
if (!isControlled) setInternalState(next);
onChange?.(next, ...args);
},
[onChange],
[isControlled, onChange],
);
return [state, setState] as const;
+17 -12
View File
@@ -1,20 +1,25 @@
import { useEffect, useState } from "react";
import { useRef, useState, useSyncExternalStore } from "react";
const MOBILE_BREAKPOINT = 768;
const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
const [mediaQueryList] = useState(() => (typeof window === "undefined" ? null : window.matchMedia(MOBILE_QUERY)));
const latestMatchesRef = useRef(mediaQueryList?.matches ?? false);
useEffect(() => {
const mql = window.matchMedia(MOBILE_QUERY);
const onChange = (e: MediaQueryListEvent) => {
setIsMobile(e.matches);
};
mql.addEventListener("change", onChange);
setIsMobile(mql.matches);
return () => mql.removeEventListener("change", onChange);
}, []);
return useSyncExternalStore(
(onStoreChange) => {
if (!mediaQueryList) return () => {};
return !!isMobile;
const handleChange = (event: MediaQueryListEvent | { matches: boolean }) => {
latestMatchesRef.current = event.matches;
onStoreChange();
};
mediaQueryList.addEventListener("change", handleChange);
return () => mediaQueryList.removeEventListener("change", handleChange);
},
() => latestMatchesRef.current,
() => false,
);
}
+3 -1
View File
@@ -96,8 +96,10 @@ export function PromptDialogProvider({ children }: { children: React.ReactNode }
[handleConfirm],
);
const contextValue = React.useMemo<PromptContextType>(() => ({ prompt }), [prompt]);
return (
<PromptContext.Provider value={{ prompt }}>
<PromptContext.Provider value={contextValue}>
{children}
<AlertDialog open={state.open} onOpenChange={(open) => !open && handleCancel()}>