如何在不使用 setTimeout() 的情况下制作响应式工具栏(带有可折叠溢出按钮)?

问题描述 投票: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
有一个独特的属性,即使它有
visibility: hidden
,它仍然被内部插入到DOM流中。当它在开发工具中处于这种状态时,您可以通过将鼠标悬停在按钮上来看到这一点 - 它仍然具有尺寸并且“存在”。

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

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.