Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/components/SuggestionOptions.tsx

tsx58 行查看原文
import type { ReactNode } from "react";
import { useLayoutEffect, useRef, useState } from "react";

// Reveal keyboard selections inside the list without scrolling any ancestor.
export function SuggestionOptions({
  selectedIndex,
  options,
  children,
  label,
}: {
  selectedIndex: number;
  options: readonly unknown[];
  children: ReactNode;
  label?: string;
}) {
  const root = useRef<HTMLDivElement>(null);
  const [mask, setMask] = useState<string>();
  const updateMask = () => {
    const element = root.current;
    if (!element) return;
    const top = element.scrollTop > 1;
    const bottom = element.scrollTop < element.scrollHeight - element.clientHeight - 1;
    setMask(
      top || bottom
        ? `linear-gradient(to bottom, ${top ? "transparent" : "black"} 0px, black 24px, black calc(100% - 24px), ${bottom ? "transparent" : "black"} 100%)`
        : undefined,
    );
  };
  useLayoutEffect(() => {
    const element = root.current;
    if (!element) return;
    const option = element.querySelectorAll<HTMLElement>('[role="option"]')[selectedIndex];
    if (option) {
      const viewport = element.getBoundingClientRect();
      const row = option.getBoundingClientRect();
      if (row.top < viewport.top) element.scrollTop += row.top - viewport.top;
      else if (row.bottom > viewport.bottom) element.scrollTop += row.bottom - viewport.bottom;
    }
    updateMask();
  }, [selectedIndex, options]);
  useLayoutEffect(() => {
    const observer = new ResizeObserver(updateMask);
    if (root.current) observer.observe(root.current);
    return () => observer.disconnect();
  }, []);
  return (
    <div
      ref={root}
      className="command-options"
      {...(label ? { role: "listbox", "aria-label": label } : {})}
      onScroll={updateMask}
      style={{ maskImage: mask, maskRepeat: "no-repeat", maskSize: "100% 100%" }}
    >
      {children}
    </div>
  );
}