React - 响应式工具栏(可折叠溢出按钮)

问题描述 投票:0回答:1

我正在为我的 richTextEditor (tiptap) 创建一个工具栏。我希望溢出编辑器宽度的按钮隐藏在“更多”按钮下。我在 reddit 上看到了一篇带有流程解释的帖子,但我在实施方面遇到了困难。 Reddit 帖子

return (
    <nav
      className={cn("flex flex-grow gap-1 rounded-md bg-white p-1", className)}
      ref={mainToolbarRef}
    >
      <TooltipProvider>
        {/* Color Picker */}
        {isButtonVisible("color") && (
          <Tooltip>
          ...
          <Tooltip/>
       /* Headings */}
        {isButtonVisible("heading") && (
          <Popover>
            ...
          </Popover>
        )}

然后有“更多”按钮,其中包含整个列表的副本,并以 MoreVisible 为条件 我正在检查那里!isButtonVisible && 等

我尝试使用 useEffect、useLayoutEffect 和 useRef 来处理宽度测量和调整。我的方法是这样的:

 const mainToolbarRef = useRef<HTMLElement>(null);
  const [visibleButtons, setVisibleButtons] = useState<ButtonName[]>([]);
  const [showMore, setShowMore] = useState(false);

  useLayoutEffect(() => {
    const button_name_list = [...BUTTON_NAMES_LIST];
    const doAdapt = () => {
      const mainToolbar = mainToolbarRef.current;
      if (mainToolbar) {
        setVisibleButtons(button_name_list);

        let isOverflowing = false;
        const buttonsArray = Array.from(mainToolbar.children);

        buttonsArray.forEach((button) => {
          if (!isFullyVisible(button as HTMLElement)) {
            isOverflowing = true;
            return false;
          }
        });

        if (isOverflowing) {
          const tempVisibleButtons: ButtonName[] = [];

          buttonsArray.forEach((button, index) => {
            const btn = button as HTMLElement;
            if (isFullyVisible(btn)) {
              tempVisibleButtons.push(button_name_list[index]!);
            }
          });

          setVisibleButtons(tempVisibleButtons);
          setShowMore(true);
        } else {
          setShowMore(false);
        }
      }
    };

    const isFullyVisible = (element: HTMLElement) => {
      const rect = element.getBoundingClientRect();
      const mainToolbarRect = mainToolbarRef.current?.getBoundingClientRect();
      return rect.right <= (mainToolbarRect!.right - 40 || 0);
    };

    window.addEventListener("resize", doAdapt);
    doAdapt();

    return () => {
      window.removeEventListener("resize", doAdapt);
    };
  }, [mainToolbarRef]);

  // if in visibleButtons
  const isButtonVisible = (buttonName: ButtonName) => {
    return visibleButtons.includes(buttonName);
  };

主要错误是第一次加载时我的工具栏无法适应。我必须调整窗口大小才能正确触发适应功能。我做了一些检查,我知道在第一次运行 doAdapt() 时

        mainToolbar.children // type HTMLCollection, 11 elements
        const buttonsArray = Array.from(mainToolbar.children) // 0 elements, empty list

buttonsArray 未正确创建。

现在我成功地创造了类似的东西。

onst button_name_list = useMemo(() => [...BUTTON_NAMES_LIST], []);

  const doAdapt = useCallback(() => {
    const mainToolbar = mainToolbarRef.current;
    if (mainToolbar) {
      setVisibleButtons([...BUTTON_NAMES_LIST]);

      let isOverflowing = false;
      const buttonsArray = Array.from(mainToolbar.children);

      buttonsArray.forEach((button) => {
        if (!isFullyVisible(button as HTMLElement)) {
          isOverflowing = true;
          return false;
        }
      });

      if (isOverflowing) {
        const tempVisibleButtons: ButtonName[] = [];

        buttonsArray.forEach((button, index) => {
          const btn = button as HTMLElement;
          if (isFullyVisible(btn)) {
            tempVisibleButtons.push(BUTTON_NAMES_LIST[index]!);
          }
        });

        setVisibleButtons(tempVisibleButtons);
        setShowMore(true);
      } else {
        setShowMore(false);
      }
    }
  }, [mainToolbarRef]);

  const isFullyVisible = (element: HTMLElement) => {
    const rect = element.getBoundingClientRect();
    const mainToolbarRect = mainToolbarRef.current?.getBoundingClientRect();
    return rect.right <= (mainToolbarRect!.right - 40 || 0);
  };

  useLayoutEffect(() => {
    const handleResize = () => {
      doAdapt();
    };

    window.addEventListener("resize", handleResize);

    // Delay the first call to doAdapt to ensure elements are mounted
    const timer = setTimeout(() => {
      doAdapt();
    }, 100);

    return () => {
      window.removeEventListener("resize", handleResize);
      clearTimeout(timer);
    };
  }, [doAdapt]);

  useLayoutEffect(() => {
    if (mainToolbarRef.current) {
      doAdapt();
    }

    window.addEventListener("resize", doAdapt);

    return () => {
      window.removeEventListener("resize", doAdapt);
    };
  }, [doAdapt]);

  const isButtonVisible = (buttonName: ButtonName) => {
    return visibleButtons.includes(buttonName);
  };

它有效,但我觉得它很糟糕,应该看起来像那样

请大家帮忙:>

reactjs typescript next.js responsive-design toolbar
1个回答
0
投票

问题的核心是你遇到了一种“先有鸡还是先有蛋”的问题。

您当前使用

isButtonVisible
检查来保护按钮。如果返回
false
那么整个按钮将不会渲染。
isButtonVisible
的结果由测量按钮是否合适的逻辑决定。然而,它无法测量不存在的东西。

要确定哪些按钮应该可见,它们必须位于 DOM 中并由浏览器绘制。否则,就没有什么可衡量的。因此,mount上

mainToolbarRef.current.children
为空,无法进行溢出检查。要进行所需的测量,必须首先实际渲染元素。

出现用于调整大小的原因是您当前在

setVisibleButtons([...BUTTON_NAMES_LIST]);
内部调用
doAdapt
,这将它们全部设置为再次可见。我怀疑这是之前的修复/黑客行为,但它是对症状的修复,而不是问题的根本原因

这实际上只会让它们在短时间内再次可见,因为状态设置不会立即生效(React 将更新排队)。

因为调整大小事件通常会快速连续发生许多事件,所以未来的事件会经历“所有可见”DOM 状态,并且能够测量按钮。这意味着调整大小行为也有点不正常,它对

n+1
事件的响应比实际应该晚。它可能也会在好状态和坏状态之间快速切换,这只是由于 React 批量更新的方式,它在某些条件下调整大小时似乎可以正常工作。

但是如果它们必须在 DOM 中才能知道它们是否应该出现,那么我们如何控制它们的可见性?

本质上,您需要始终渲染它们,但使用有条件设置为

visibility
hidden
的 CSS
visible
属性来控制它们的可见性。请注意,
display: none
在这里也不起作用,因为这意味着它们仍然没有被绘制。

因此,我们首先需要删除工具栏中每个按钮的渲染条件,然后移动它来控制

visibility
CSS 属性。例如:

{isButtonVisible('color') && (
  <Tooltip>
    <TooltipTrigger asChild>

变成:

<Tooltip>
  <TooltipTrigger
    asChild
    style={{
      visibility: isButtonVisible('color') ? 'visible' : 'hidden',
    }}
  >

请注意,您可能想使用 CSS 类来执行此操作。请注意,我们不会更改溢出菜单中存在的按钮的防护。它们可以保持不变,因为我们没有测量它们。只有工具栏中的内容直接需要进行此更改。

另请注意,“三点”查看更多图标本身,确实也需要进行此更改,因为它位于工具栏中。我们还需要对此处的 CSS 进行更改,以正确定位它,现在其他按钮“呈现但隐藏”,这样它就不会被推离屏幕边缘:

{/* More */}
<Popover>
  <Tooltip>
  <TooltipTrigger
    asChild
    className="h-9 w-9 shrink-0 cursor-pointer rounded-md px-2.5 transition-colors hover:bg-muted hover:text-muted-foreground"
    style={{
      position: 'absolute',
      right: 0,
      visibility: showMore ? 'visible' : 'hidden',
     }}
  >

最后,我们可以清理调整大小逻辑,以删除以前围绕不再需要的可见性进行的修改。

  const mainToolbarRef = useRef<HTMLElement>(null);
  const [visibleButtons, setVisibleButtons] = useState<ButtonName[]>([]);
  const [showMore, setShowMore] = useState(false);

  const doAdapt = useCallback(() => {
    const mainToolbar = mainToolbarRef.current;
    if (mainToolbar) {
      const buttonsArray = Array.from(mainToolbar.children);

      const tempVisibleButtons: ButtonName[] = [];

      buttonsArray.forEach((button, index) => {
        const btn = button as HTMLElement;
        if (isFullyVisible(btn)) {
          tempVisibleButtons.push(BUTTON_NAMES_LIST[index]!);
        }
      });

      setVisibleButtons(tempVisibleButtons);
      setShowMore(tempVisibleButtons.length < buttonsArray.length - 1);
    }
  }, [mainToolbarRef]);

  const isFullyVisible = (element: HTMLElement) => {
    const rect = element.getBoundingClientRect();
    const mainToolbarRect = mainToolbarRef.current?.getBoundingClientRect();
    return rect.right <= (mainToolbarRect!.right - 40 || 0);
  };

  useLayoutEffect(() => {
    doAdapt();

    window.addEventListener('resize', doAdapt);

    return () => {
      window.removeEventListener('resize', doAdapt);
    };
  }, [doAdapt]);

  const isButtonVisible = (buttonName: ButtonName) => {
    return visibleButtons.includes(buttonName);
  };

我已将所有这些整合到一个工作 StackBlitz中。

© www.soinside.com 2019 - 2024. All rights reserved.