60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
|
|
import { Alert, Box, Button, CircularProgress, Typography } from "@mui/material";
|
|
import { visuallyHidden } from "@mui/utils";
|
|
|
|
import type { ViewResourceError } from "../hooks/useViewResource";
|
|
import { useI18n } from "../i18n/I18nProvider";
|
|
|
|
type Props = {
|
|
loading?: boolean;
|
|
error?: ViewResourceError | null;
|
|
title: string;
|
|
description?: string;
|
|
retryLabel?: string;
|
|
onRetry?: () => void | Promise<void>;
|
|
compact?: boolean;
|
|
};
|
|
|
|
export default function ViewStateNotice({ loading = false, error = null, title, description, retryLabel, onRetry, compact = false }: Props) {
|
|
const { t } = useI18n();
|
|
const resolvedRetryLabel = retryLabel ?? t("retry");
|
|
const [observedLoading, setObservedLoading] = useState(loading);
|
|
useEffect(() => { if (loading) setObservedLoading(true); }, [loading]);
|
|
|
|
// Keep one live region mounted through the read. An idle/error-only consumer
|
|
// has not completed a load; unrelated form edits must not announce anything.
|
|
const status = <Box component="span" role="status" aria-live="polite" aria-atomic="true" sx={visuallyHidden}>
|
|
{loading ? t("loading") : !error && observedLoading ? t("contentLoaded") : ""}
|
|
</Box>;
|
|
|
|
if (loading) {
|
|
return (
|
|
<>{status}<Box sx={{ py: compact ? 3 : 6, display: "flex", justifyContent: "center" }}>
|
|
<CircularProgress size={compact ? 24 : 28} aria-label={t("loading")} />
|
|
</Box></>
|
|
);
|
|
}
|
|
|
|
if (!error) return <>{status}</>;
|
|
|
|
const severity = error.kind === "unauthorized" ? "warning" : error.kind === "unavailable" ? "error" : "error";
|
|
const detail = error.message !== title && error.message !== description ? error.message : null;
|
|
|
|
return (
|
|
<>{status}<Alert
|
|
severity={severity}
|
|
sx={{
|
|
mb: compact ? 1.5 : 2,
|
|
alignItems: "flex-start",
|
|
borderRadius: 3,
|
|
}}
|
|
action={error.retryable && onRetry ? <Button color="inherit" size="small" onClick={() => void onRetry()}>{resolvedRetryLabel}</Button> : undefined}
|
|
>
|
|
<Typography sx={{ fontWeight: 800, mb: 0.35 }}>{title}</Typography>
|
|
{description ? <Typography variant="body2">{description}</Typography> : null}
|
|
{detail ? <Typography variant="body2" sx={{ mt: 0.5 }}>{detail}</Typography> : null}
|
|
</Alert></>
|
|
);
|
|
}
|