import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createEffect, createSignal, omit, Show, untrack } from "solid-js"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { useAvatarRootContext } from "../avatar-root-context"; import type { AvatarRootState } from "../root/avatar-root"; export interface AvatarFallbackState extends AvatarRootState {} export interface AvatarFallbackProps extends BaseUIComponentProps<"span", AvatarFallbackState> { /** Tag/component to render. Defaults to `"span"`. */ as?: ValidComponent; /** * How long to wait before showing the fallback. Specified in milliseconds. * * When `delay` is `undefined`, the fallback is eligible immediately. When * it is a number, the fallback stays hidden for that many milliseconds * after mount, giving the image a chance to load first. */ delay?: number | undefined; } /** * Rendered when the image fails to load or when no image is provided. * Renders a `` element. */ export function AvatarFallback(props: AvatarFallbackProps): JSX.Element { const context = useAvatarRootContext(); // `delayPassed` starts true when `delay` is undefined (fallback is eligible // immediately) and is flipped by the timer below once the delay elapses. // Internal plumbing — `ownedWrite` lets the split-effect's apply half (owned // scope) flip it without tripping SIGNAL_WRITE_IN_OWNED_SCOPE. // Initial value is a snapshot — the split effect below tracks // `props.delay` independently and re-flips this signal on changes, so // the seed read can be untracked to silence `[STRICT_READ_UNTRACKED]`. const [delayPassed, setDelayPassed] = createSignal( untrack(() => props.delay === undefined), { ownedWrite: true, }, ); // Split effect: the compute half tracks `props.delay`; the apply half // installs / clears the timer. When `delay` changes, Solid cleans up the // previous apply run (clearing the stale timer) and re-runs with the new // value — matching Base UI React, which keys its effect on `delay`. // `onSettled` wouldn't give restart-on-change semantics; a split // `createEffect` does. createEffect( () => props.delay, (delay) => { if (delay === undefined) { setDelayPassed(true); return undefined; } setDelayPassed(false); const timeoutId = window.setTimeout(() => { setDelayPassed(true); }, delay); return () => { window.clearTimeout(timeoutId); }; }, ); // Visible when the image is not yet loaded (idle/loading/error) AND the // configured delay has passed. Use a memo-ish accessor — Solid tracks // both reads inside ``'s compute naturally. const shouldShow = (): boolean => context.imageLoadingStatus() !== "loaded" && delayPassed(); const userStyling = resolveClassStyle(props, () => ({ imageLoadingStatus: context.imageLoadingStatus(), })); const userHtmlProps = omit( props, "class", "style", "attrs", "as", "delay", ) as Record; const styledUserProps = mergeRootProps(userStyling, userHtmlProps); const merged = mergeRootProps({}, styledUserProps); const Tag = dynamic(() => props.as ?? "span"); return ( ); }