← Files DataARCHIVED FILE

templates/data-app/base/src/components/SourcesReceipt.jsx

11.6 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import React, { useEffect, useId, useRef, useState } from "react";
import { flushSync } from "react-dom";
import dataComposerIcon from "../../../../../assets/datascience-small.svg?url";

import { sourcePreviewPosition } from "../source-preview.js";
import { Icon } from "./Icon.jsx";
import { ReceiptSourceIcon, SourceInspector } from "./SourceInspector.jsx";
import { SourcePreviewCardContent } from "./SourcePreviewCardContent.jsx";

function useReceiptInputModality(rootRef) {
  useEffect(() => {
    const root = rootRef.current;
    const controller = new AbortController();
    const options = { capture: true, signal: controller.signal };
    // Listen before tab handlers and Escape focus restoration, including when
    // focus enters the Shadow DOM from elsewhere in the answer.
    const keyboardInput = (event) => {
      if (event.metaKey || event.altKey || event.ctrlKey || ["Shift", "Control", "Alt", "Meta"].includes(event.key)) return;
      root.dataset.inputModality = "keyboard";
    };
    root.ownerDocument.addEventListener("keydown", keyboardInput, options);
    // Tab can start in the host document and finish inside this iframe. Its
    // keyup reaches the newly focused receipt even when its keydown did not.
    root.ownerDocument.addEventListener("keyup", (event) => {
      if (event.key === "Tab") keyboardInput(event);
    }, options);
    root.ownerDocument.addEventListener("pointerdown", () => {
      root.dataset.inputModality = "pointer";
    }, options);
    return () => controller.abort();
  }, [rootRef]);
}

// Reuse report source-preview timing and placement. Once a tooltip is open,
// moving to an adjacent source changes its content without another hover delay.
function useReceiptTooltips(rootRef, tooltipRef, setTooltipContent) {
  useEffect(() => {
    const root = rootRef.current;
    const tooltip = tooltipRef.current;
    const controller = new AbortController();
    const listen = (target, type, handler, options = {}) =>
      target.addEventListener(type, handler, { ...options, signal: controller.signal });
    const selector = "[data-source-title], [data-tooltip-text]";
    let active, hovered, focused, dismissed, openTimer, closeTimer;
    const cancelOpen = () => { clearTimeout(openTimer); openTimer = null; };
    const cancelClose = () => { clearTimeout(closeTimer); closeTimer = null; };
    const hide = () => {
      cancelOpen(); cancelClose();
      active?.removeAttribute("aria-describedby");
      active = null;
      tooltip.hidden = true;
      delete tooltip.dataset.visible;
    };
    const needsTooltip = (chip) => chip?.getClientRects().length &&
      (!chip.matches(".receipt-filter") || [...chip.children].some((part) => part.scrollWidth > part.clientWidth));
    const place = () => {
      if (!active || tooltip.hidden) return;
      const anchor = active.getBoundingClientRect();
      if (anchor.bottom <= 12 || anchor.top >= window.innerHeight - 12) { hide(); return; }
      tooltip.style.left = "12px";
      tooltip.style.top = "12px";
      tooltip.style.maxHeight = "";
      const bounds = tooltip.getBoundingClientRect();
      if (active.dataset.sourceTitle) {
        const height = tooltip.scrollHeight + tooltip.offsetHeight - tooltip.clientHeight;
        const position = sourcePreviewPosition(anchor, { width: bounds.width, height },
          { width: window.innerWidth, height: window.innerHeight });
        tooltip.style.left = `${position.left}px`;
        tooltip.style.top = `${position.top}px`;
        tooltip.style.maxHeight = `${position.maxHeight}px`;
      } else {
        const width = document.documentElement.clientWidth;
        const height = window.innerHeight;
        tooltip.style.left = `${Math.max(8, Math.min(anchor.left + (anchor.width - bounds.width) / 2, width - bounds.width - 8))}px`;
        const above = anchor.top - bounds.height - 8;
        tooltip.style.top = `${Math.max(8, above >= 8 ? above : Math.min(anchor.bottom + 8, height - bounds.height - 8))}px`;
      }
    };
    const show = (chip) => {
      if (!needsTooltip(chip) || dismissed === chip) return;
      cancelOpen(); cancelClose();
      active?.removeAttribute("aria-describedby");
      active = chip;
      if (chip.dataset.sourceTitle) {
        const sourceType = { table: "Table", file: "File", dashboard: "Dashboard", link: "Document or link" }[chip.dataset.sourceKind];
        flushSync(() => setTooltipContent({ kind: "source", preview: {
          href: chip.href, source: sourceType ?? "Source", sourceKind: chip.dataset.sourceKind, title: chip.dataset.sourceTitle,
          summary: chip.dataset.sourceReason,
        } }));
      } else {
        flushSync(() => setTooltipContent({ kind: "text", text: chip.dataset.tooltipText }));
      }
      tooltip.hidden = false;
      chip.setAttribute("aria-describedby", tooltip.id);
      place();
      tooltip.dataset.visible = "true";
    };
    const schedule = (chip, delay = 250) => {
      cancelOpen(); cancelClose();
      if (chip === active || chip === dismissed) return;
      if (!needsTooltip(chip)) { hide(); return; }
      if (active && !tooltip.hidden) show(chip);
      else openTimer = setTimeout(() => show(chip), delay);
    };
    const scheduleClose = (chip) => {
      cancelOpen(); cancelClose();
      closeTimer = setTimeout(() => {
        if (!tooltip.matches(":hover") && !hovered && focused !== chip) hide();
      }, 180);
    };
    const dismissOnEscape = (event) => {
      if (event.key !== "Escape") return;
      const target = active || hovered || focused;
      if (target) { event.preventDefault(); event.stopPropagation(); dismissed = target; hide(); }
    };
    listen(root, "pointerover", (event) => {
      const chip = event.target.closest(selector);
      if (event.pointerType === "touch" || !chip || chip.contains(event.relatedTarget)) return;
      hovered = chip; dismissed = null; schedule(chip);
    });
    listen(root, "pointerout", (event) => {
      const chip = event.target.closest(selector);
      if (!chip || chip.contains(event.relatedTarget)) return;
      hovered = null;
      scheduleClose(chip);
    });
    listen(root, "focusin", (event) => {
      focused = event.target.closest(selector);
      if (focused) { dismissed = null; schedule(focused, 0); }
    });
    listen(root, "focusout", (event) => {
      const chip = event.target.closest(selector);
      focused = null;
      if (hovered !== chip) hide();
    });
    listen(root, "pointerdown", (event) => {
      const chip = event.target.closest(selector);
      if (event.pointerType === "touch" && needsTooltip(chip)) chip.dataset.touchInspect = active === chip ? "open" : "preview";
    });
    listen(root, "click", (event) => {
      const chip = event.target.closest(selector);
      if (!chip) { hide(); return; }
      if (chip.dataset.touchInspect === "preview") { event.preventDefault(); dismissed = null; show(chip); }
      else if (!chip.matches("a[href]")) { dismissed = null; show(chip); }
      delete chip.dataset.touchInspect;
    });
    listen(root, "keydown", (event) => {
      if (event.key === "Escape") {
        dismissOnEscape(event);
        return;
      }
      const chip = event.target.closest(selector);
      if (chip && ["Enter", " "].includes(event.key) && !chip.matches("a[href]")) {
        event.preventDefault(); dismissed = null; show(chip);
      }
    }, { capture: true });
    // Hover can start while keyboard focus is outside the Shadow-DOM receipt.
    listen(root.ownerDocument, "keydown", dismissOnEscape);
    listen(tooltip, "pointerenter", cancelClose);
    listen(tooltip, "pointerleave", () => { if (!hovered && !focused) scheduleClose(active); });
    listen(root, "scroll", (event) => { if (event.target !== tooltip) active?.dataset.sourceTitle ? place() : hide(); }, { capture: true });
    listen(window, "scroll", (event) => { if (event.target !== tooltip) active?.dataset.sourceTitle ? place() : hide(); }, { capture: true });
    listen(window, "resize", () => active?.dataset.sourceTitle ? place() : hide());
    return () => { hide(); controller.abort(); };
  }, [rootRef, tooltipRef, setTooltipContent]);
}

function SourceCard({ item, collapsible }) {
  const [open, setOpen] = useState(!collapsible);
  const contentId = `receipt-card-${useId()}`;
  const toggle = useRef(null);
  function collapse() {
    setOpen(false);
    window.requestAnimationFrame(() => toggle.current?.focus());
  }
  return <article className="receipt-card" data-open={open} aria-label={item.title}
    onKeyDown={(event) => {
      if (collapsible && open && event.key === "Escape" && !event.defaultPrevented) {
        event.preventDefault(); event.stopPropagation(); collapse();
      }
    }}>
    {collapsible && <div className="receipt-card-label" hidden={open} aria-hidden="true">
      <span>{item.title}</span>
    </div>}
    {collapsible && <button ref={toggle} type="button" className="receipt-card-toggle"
      aria-label={open ? `Collapse ${item.title}` : item.title}
      aria-expanded={open} aria-controls={contentId} onClick={open ? collapse : () => setOpen(true)}>
      <Icon name="chevronDown" size={16} />
    </button>}
    <div id={contentId} hidden={!open} className="receipt-card-content">
      <SourceInspector component={{ id: item.id, title: item.title, description: item.description, kind: "narrative" }}
        receiptQueries={item.queries} filters={[]} allowCopy={false}
        receiptAssumptions={item.assumptions}
        onReceiptCollapse={collapsible ? collapse : undefined} receiptContentId={contentId}
        externalReceiptToggle={collapsible} />
    </div>
  </article>;
}

/** One answer-level disclosure; native answer prose is intentionally not part of this payload. */
export function SourcesReceipt({ items }) {
  const root = useRef(null);
  const tooltip = useRef(null);
  const tooltipId = `receipt-tooltip-${useId()}`;
  const [tooltipContent, setTooltipContent] = useState(null);
  useReceiptInputModality(root);
  useReceiptTooltips(root, tooltip, setTooltipContent);
  const [open, setOpen] = useState(false);
  const contentId = `receipt-${useId()}`;
  const toggle = useRef(null);
  return <div ref={root} className="sources-receipt" data-input-modality="pointer" onKeyDown={(event) => {
    if (open && event.key === "Escape" && !event.defaultPrevented) {
      event.preventDefault(); event.stopPropagation(); setOpen(false); toggle.current?.focus();
    }
  }}>
    <div className="receipt-toolbar"><button ref={toggle} type="button" className="receipt-disclosure" aria-expanded={open}
      aria-controls={contentId} onClick={() => setOpen(!open)}>
      <img src={dataComposerIcon} width={16} height={16} alt="" aria-hidden="true" />
      <span>{items.length === 1 ? "Sources" : `Sources • ${items.length}`}</span>
      <Icon name="chevronDown" size={14} />
    </button></div>
    <div id={contentId} className="receipt-expander" data-open={open} inert={!open} aria-hidden={!open}>
      <div className="receipt-expander-inner"><div className="receipt-cards">
        {items.map((item) => <SourceCard key={item.id} item={item} collapsible={items.length > 1} />)}
      </div></div>
    </div>
    <div ref={tooltip} id={tooltipId} role="tooltip" hidden
      className={tooltipContent?.kind === "source" ? "receipt-tooltip popover source-preview-card" : "receipt-tooltip"}
      data-kind={tooltipContent?.kind === "source" ? "source" : undefined}>
      {tooltipContent?.kind === "source" ? <SourcePreviewCardContent preview={tooltipContent.preview}
        icon={<span className="source-preview-provider-icon" aria-hidden="true">
          <ReceiptSourceIcon kind={tooltipContent.preview.sourceKind} label={tooltipContent.preview.title} />
        </span>} sourceLabel={tooltipContent.preview.source} /> : tooltipContent?.text}
    </div>
  </div>;
}

SHA-256: 9485a522cf62c4ed35e89dcf500344d0ba6618ee41147564d0c6cc46b5174a3d