Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/components/screens/shared/RefreshIconButton.tsx
T

69 lines
2.2 KiB
TypeScript

// Shared refresh affordance for list screens. The plain IconButton `loading`
// state hides the icon behind the generic loader, which reads as "the button
// vanished" rather than "the list is reloading" — so this keeps the refresh
// arrows and rotates them instead.
import { useEffect, useRef, useState } from "react";
import { FeatherRefreshCw } from "@subframe/core";
import { IconButton } from "~/ui/components/IconButton";
/**
* Keeps a transient busy flag on for at least `minMs`. A revalidation that
* returns in 80ms would otherwise make the spinner flicker once, which reads as
* a glitch rather than as feedback.
*/
export function useMinimumDuration(active: boolean, minMs = 600): boolean {
const [held, setHeld] = useState(active);
const startedAt = useRef(0);
useEffect(() => {
if (active) {
if (!held) startedAt.current = Date.now();
setHeld(true);
return;
}
if (!held) return;
const remaining = Math.max(0, minMs - (Date.now() - startedAt.current));
if (remaining === 0) {
setHeld(false);
return;
}
const timer = setTimeout(() => setHeld(false), remaining);
return () => clearTimeout(timer);
}, [active, held, minMs]);
return held;
}
export type RefreshIconButtonProps = {
/** True while the underlying data is reloading (e.g. `revalidator.state !== "idle"`). */
readonly busy: boolean;
readonly onRefresh: () => void;
/** Translated label, used for both the accessible name and the tooltip. */
readonly label: string;
readonly dataUiId?: string;
};
export function RefreshIconButton({ busy, onRefresh, label, dataUiId }: RefreshIconButtonProps): React.JSX.Element {
const spinning = useMinimumDuration(busy);
return (
<IconButton
variant="ghost"
size="small"
icon={
// The hover tilt makes the button answer the pointer before the click;
// while reloading the arrows spin instead.
<FeatherRefreshCw
className={spinning ? "animate-spin" : "transition-transform duration-200 group-hover/af9405b1:rotate-45"}
/>
}
disabled={spinning}
data-busy={spinning ? "true" : undefined}
onClick={onRefresh}
aria-label={label}
title={label}
data-ui-id={dataUiId}
/>
);
}