Skip to content
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
2 changes: 1 addition & 1 deletion apps/web/src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function AppShell() {
{/* Skip to main content — must be the first focusable element */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded focus:shadow-lg focus:outline-none"
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded focus:shadow-lg"
>
Skip to main content
</a>
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/ConnectGitHubBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export function ConnectGitHubBanner() {
variant="ghost"
size="sm"
onClick={() => setDismissed(true)}
aria-label="Dismiss"
>
Dismiss
</Button>
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function MarkdownEditor({
required,
}: MarkdownEditorProps) {
const id = useId();
const errorId = `${id}-error`;
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [previewHtml, setPreviewHtml] = useState<string>('');
const [previewLoading, setPreviewLoading] = useState(false);
Expand Down Expand Up @@ -170,11 +171,14 @@ export function MarkdownEditor({
className="rounded-none border-0 focus-visible:ring-0 font-mono text-sm resize-y"
style={{ minHeight }}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? errorId : undefined}
/>
{/* Deliberately not a live region: the preview is the whole document
re-rendered on every debounce, so announcing it would read the
entire text back on each pause in typing. */}
<div
className="p-3 bg-background text-sm overflow-auto"
style={{ minHeight }}
aria-live="polite"
>
{previewError ? (
<p className="text-xs text-destructive">{previewError}</p>
Expand All @@ -192,7 +196,9 @@ export function MarkdownEditor({
</div>
<div className="flex items-center justify-between text-xs">
{error ? (
<span className="text-destructive">{error}</span>
<span id={errorId} className="text-destructive">
{error}
</span>
) : (
<span className="text-muted-foreground">Markdown · supports GFM</span>
)}
Expand Down
27 changes: 19 additions & 8 deletions apps/web/src/components/NetworkErrorBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@ import {
} from 'react';

interface NetworkErrorContextValue {
showError: (message?: string) => void;
/**
* Show the banner. Pass `retry` when the failed work can be re-issued; the
* button then reads "Retry" and runs it. Without one it reads "Dismiss".
*/
showError: (message?: string, retry?: () => void) => void;
clearError: () => void;
}

interface NetworkErrorState {
message: string;
retry?: () => void;
}

const NetworkErrorContext = createContext<NetworkErrorContextValue | null>(null);

export function NetworkErrorProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<NetworkErrorState | null>(null);

const showError = useCallback((message?: string) => {
setError(message ?? 'Something went wrong. We are looking at it.');
const showError = useCallback((message?: string, retry?: () => void) => {
setError({ message: message ?? 'Something went wrong. We are looking at it.', retry });
}, []);

const clearError = useCallback(() => {
Expand All @@ -32,13 +41,15 @@ export function NetworkErrorProvider({ children }: { children: ReactNode }) {
className="bg-destructive text-destructive-foreground px-4 py-2 text-sm flex items-center justify-between"
data-testid="network-error-banner"
>
<span>{error}</span>
<span>{error.message}</span>
<button
onClick={clearError}
onClick={() => {
error.retry?.();
clearError();
}}
className="ml-4 underline hover:no-underline"
aria-label="Dismiss error"
>
Retry
{error.retry ? 'Retry' : 'Dismiss'}
</button>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function Pagination({ page, totalPages, onPageChange, siblingCount = 1, c
variant={p === page ? 'default' : 'outline'}
size="sm"
onClick={() => onPageChange(p)}
aria-label={`Page ${p}`}
aria-current={p === page ? 'page' : undefined}
>
{p}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/PersonAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function PersonAvatar({ person, size = 32, asLink = true, className, titl
/>
) : (
<span
role="img"
title={title ?? person.fullName}
className={cn(
'inline-flex items-center justify-center rounded-full font-medium',
Expand All @@ -43,7 +44,7 @@ export function PersonAvatar({ person, size = 32, asLink = true, className, titl
if (!asLink || !person.slug || person.deactivated) return inner;

return (
<Link to={`/members/${person.slug}`} aria-label={person.fullName}>
<Link to={`/members/${person.slug}`}>
{inner}
</Link>
);
Expand Down
Loading