import type { JSX, ValidComponent } from "@solidjs/web"; import { dynamic } from "@solidjs/web"; import { createEffect, createMemo, omit, onCleanup, untrack } from "solid-js"; import { useBaseUiId } from "../../internals/base-ui-id"; import { composeRefs, userRef } from "../../internals/compose-refs"; import { useFocus } from "../../internals/floating-ui/hooks/use-focus"; import { useHover } from "../../internals/floating-ui/hooks/use-hover"; import { safePolygon } from "../../internals/floating-ui/safe-polygon"; import type { Delay } from "../../internals/floating-ui/types"; import { mergeRootProps } from "../../internals/merge-root-props"; import { resolveClassStyle } from "../../internals/resolve-class-style"; import type { BaseUIComponentProps } from "../../internals/types"; import { useIsoLayoutEffect } from "../../internals/use-iso-layout-effect"; import { CLOSE_DELAY, OPEN_DELAY } from "../constants"; import { usePreviewCardRootContext } from "../preview-card-root-context"; import type { PreviewCardHandle } from "../store/preview-card-store"; export interface PreviewCardTriggerState { /** Whether the preview card is currently open. */ open: boolean; } export interface PreviewCardTriggerProps extends BaseUIComponentProps<"a", PreviewCardTriggerState> { /** * Tag/component to render. Defaults to `"a"`. Replaces the * pre-spike `render` prop (axis G). */ as?: ValidComponent; /** @default 600 */ delay?: number | undefined; /** @default 300 */ closeDelay?: number | undefined; /** * Optional handle this trigger should register with. When supplied, the * trigger registers in `handle.store.context.triggerElements` instead of * the enclosing root's store. */ handle?: PreviewCardHandle | undefined; /** Stable id for multi-trigger setups. */ id?: string | undefined; /** Payload associated with this trigger (surfaces via `payload` render-fn). */ payload?: Payload | undefined; } /** * A link that opens the preview card on hover / focus. Renders an `` * element (matches Base UI React — preview-card is designed for inline * links that surface a preview on hover). * * Solid 2.0-native rewire (§1 wave 2): replaces `renderElement` with * `dynamic(() => props.as ?? "a")` and composes hover + focus + * dismiss reference props + state-attr getters via `mergeRootProps`. */ export function PreviewCardTrigger( props: PreviewCardTriggerProps, ): JSX.Element { const root = usePreviewCardRootContext(); const delayProp = (): number => props.delay ?? OPEN_DELAY; const closeDelayProp = (): number => props.closeDelay ?? CLOSE_DELAY; const fallbackId = useBaseUiId(); const thisTriggerId = (): string => props.id ?? fallbackId; const isActive = createMemo(() => { return root.activeTriggerId() === thisTriggerId(); }); useIsoLayoutEffect( () => ({ active: isActive(), delay: closeDelayProp() }), ({ active, delay }) => { if (active) { root.closeDelayRef.current = delay; } }, ); const hoverProps = useHover(root.floatingRootContext, { mouseOnly: true, move: false, handleClose: safePolygon(), delay: (): Delay => ({ open: delayProp(), close: closeDelayProp(), }), }); const focusProps = useFocus(root.floatingRootContext, { delay: delayProp(), }); const referenceProps = root.interactions.getReferenceProps(); const state = createMemo(() => ({ open: root.open() && isActive(), })); // Trigger registration in the store's trigger map. const storeFor = () => props.handle?.store ?? root.store; let registeredId: string | null = null; let registeredElement: HTMLElement | null = null; const triggerRef = (el: HTMLElement | null): void => untrack(() => { const id = thisTriggerId(); const s = storeFor(); if (registeredId !== null && registeredElement !== null) { const current = s.context.triggerElements.getById(registeredId); if (current === registeredElement) { s.context.triggerElements.delete(registeredId); } registeredId = null; registeredElement = null; } if (el && id !== undefined) { s.context.triggerElements.add(id, el); registeredId = id; registeredElement = el; } if (el && (isActive() || root.activeTriggerId() == null)) { root.setTriggerElement(el); if (root.activeTriggerId() == null) { root.setActiveTriggerId(thisTriggerId()); } } }); onCleanup(() => { if (registeredId !== null && registeredElement !== null) { const s = storeFor(); const current = s.context.triggerElements.getById(registeredId); if (current === registeredElement) { s.context.triggerElements.delete(registeredId); } } }); createEffect( () => ({ active: isActive(), payload: props.payload }), ({ active, payload }) => { if (active && "payload" in props) { root.setPayload(payload); } }, ); const markActive = (): void => { const id = thisTriggerId(); if (id !== undefined) { root.setActiveTriggerId(id); } if ("payload" in props) { root.setPayload(props.payload); } }; const stateAttrs = { get "data-popup-open"() { return state().open ? "" : undefined; }, get id() { return thisTriggerId(); }, onPointerEnter: markActive, onFocus: markActive, }; const userClassStyle = resolveClassStyle(props, () => state()); const userProps = omit( props, "as", "class", "style", "attrs", "delay", "closeDelay", "handle", "payload", ); let merged: Record = mergeRootProps( hoverProps ?? {}, focusProps.reference, ); merged = mergeRootProps(merged, referenceProps); merged = mergeRootProps(merged, stateAttrs); merged = mergeRootProps(merged, userClassStyle); merged = mergeRootProps(merged, userProps); const Tag = dynamic(() => props.as ?? "a"); return ; }