有条件地显示带有悬停和超时的 React 组件

Conditionally displaying React components with hover and timeouts

我 运行 遇到了一个棘手的 React 布局问题。这是一个足够简单的问题,只是很难解释,所以我尽量说得清楚。我有一些数据可以像这样映射各个组件:

map => <TableRow name={props.name} actionOne={props.someAction} />

Name Type Show/Hide Controls Buttons (hidden by default)
John Deere Tractor show/hide <div style="hidden"><button id="actionOneButton"></div>
John Smith Person show/hide <div style="hidden"><button id="actionOneButton"></div>

控制按钮默认隐藏。这里的想法是仅当用户停留在某一行时或如果用户决定让某些控制按钮在某些行上永久可见时才显示按钮工具

我们需要做两件事:

  1. 当输入 table 并将鼠标悬停在一行上时,会有 500 毫秒的延迟,之后只会显示该行的操作按钮。如果此时我们将光标向上或向下移动到不同的行内,没有延迟,新悬停的行立即显示其对应的按钮,而上一行中先前显示的按钮立即隐藏

  2. 当用户将光标移到 table 之外时,将进行 500 毫秒的倒计时。在此期间,最后显示在一行中的按钮将保持显示状态。计时器启动后,最后显示按钮的行现在将其隐藏。 将其视为“悬停时显示”,但在进入和退出时有 500 毫秒的延迟 table。

快完成了!

  1. 一个警告:点击show link 将永久显示该行中的按钮,而隐藏 link returns 将按钮隐藏到原始状态它。 (我们回到#1) 手动显示的按钮会永久保留,直到关闭,此时它们的行为与开始时相同。

重要说明: 如果光标退出 table,然后在“退出计时器”结束前悬停在 table 内:

问题

我有一些松散的想法,但在设计这样的东西时想到的是,所有的状态和超时(甚至是要走的路)都被封装在最多两个组件中 - 像 table 和行数?

我该如何设计这个组件的功能?我是不是找错人了 show/hide 可以由聪明的 CSS 完成吗?

我会按照以下方式解决这个问题

Table 状态:

  1. lastHovered - 最后悬停行的 id(或 null)
  2. hoverTimerId - 当前定时器的定时器id
  3. hoverRowId - 当前定时器的行号

行道具:

  1. isLastHovered - 布尔值,当这是最后显示的道具时为真

行状态:

  1. alwaysShow - boolean, true when the always show controls button is selected

行的悬停事件/table

//can be called with null for outside hover or rowId for row hover
onRowHover = (rowId) => {
    if(rowId === hoverRowId)
        return
    if(hoverTimerId !== null)
        clearTimeout(hoverTimerId)
    const timerId = setTimeout(() => {
        setLastHovered(rowId)
    }, 500)
    setHoverTimerId(timerId)
    setHoverRowId(rowId)
}

最后,如果 isLastHovered 属性为真,或者始终显示状态为真,请确保显示控件。

(isLastHovered状态由行中的show/hide按钮|控制)

我相信这可能是一种可行的解决方案。通过更多调整,我认为它可以实现您概述的所需行为。

通过使用鼠标离开 table 点的坐标,从行列表中捕获光标离开 table 的最后一个元素。在示例中,退出的元素将保持可见,但您可以决定如何处理它。

[...table.children[0].children].forEach(tr => {
    tr.classList.remove('exited');
    if(evt.offsetY >= tr.offsetTop 
       && evt.offsetY <= tr.offsetTop + tr.clientHeight
     ){
       tr.classList.add('exited'); // in react you could instead set this element to state.
     }
  });

希望它不会成为转换为 JSX 的问题。您可以使用 refObject 保留对 table 的 DOM 元素的引用。

const table = document.getElementById("table");
let hoverTimer = 0;

table.addEventListener("mouseenter", () => {
  clearTimeout(hoverTimer);
  hoverTimer = setTimeout(() => 
    table.classList.add('active'),
    500
  );
});

table.addEventListener("mouseleave", (evt) => {
  [...table.children[0].children].forEach(tr => {
    tr.classList.remove('exited');
    if(evt.offsetY >= tr.offsetTop 
       && evt.offsetY <= tr.offsetTop + tr.clientHeight
     ){
       tr.classList.add('exited');
     }
  });
  
  clearTimeout(hoverTimer);
  hoverTimer = setTimeout(() => 
    table.classList.remove('active'),
    500
  );
});
tr.table-row button.action-btn {
  pointer-events: none;
}

table.active tr.table-row:hover button.action-btn {
  pointer-events: auto;
}

tr.table-row {
  opacity: 0;
  transition: opacity 0.5s;
}

table.active tr.table-row:hover {
  opacity: 1;
}

tr.exited {
  opacity: 1;
}
<table id="table">
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
<tr class="table-row"><td><button class="action-btn">click me!</button></td></tr>
</table>