手搓带蒙层的下拉选择框/手风琴

观前提示:因为笔者很懒,本文不单独写 css,样式都以 tailwind 方式书写

1. 实现高度 auto 情况下的 transition

基本上,在内容不定的情况下,我们当然希望容器能够自适应内容的高度自动被撑开。但是对于一个我们希望添加平滑展开效果的选择栏来说,这里最大的问题就是:transistion 不支持 height: auto

其他不多作介绍,目前看到的最好解决方案无疑是:CSS 如何让auto height完美支持过渡动画? - 掘金 (juejin.cn)。简单来说,就是将需要 transition 属性的内容包裹在一个 grid 的其中一个 span,在折叠时将该 span 设置为 0froverflow: hidden 以及 min-height: 0,在展开时将该 span 设置为 1fr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    <div
onClick={() => {
setOpen(!open);
}}
className={`rounded-md cursor-pointer transition-all z-20 duration-300 ease-in-out grid px-4 py-1 bg-red-800  ${
open ? " grid-rows-[0fr_1fr] " : " grid-rows-[0fr_0fr]"
}`}
>
<div className=" flex flex-row justify-center items-center font-bold p-2 gap-2">
{select === "" ? "dropdown" : select}
<AiFillCaretDown
className={`ease-in-out duration-300 ${open ? "-rotate-180" : "0"}`}
/>
</div>
<div className="min-h-0 overflow-hidden flex flex-col gap-y-2 w-full">
{open &&
data.map((_, i) => (
<div
onClick={() => {
setSelect(_);
}}
key={_}
className=" flex flex-row justify-center  items-center p-2 hover:bg-[#FFFFFF70]"
>
{_}
</div>
))}
</div>
</div>

2. 蒙层

把蒙层和组件本身封装到一起,解决 z 层级问题的推荐方案是使用上下文堆叠。用一个定位为 relativez-index 大于等于蒙层的父组件,包裹起选择框本体的组件,选择框本体则因为上下文层级计算必然在蒙版之上。

1
2
3
4
5
6
7
8
9
10
11
12
13
{/*蒙层*/}
 <div
        style={{
          opacity: open ? 0.55 : 0,
          visibility: open ? "visible" : "hidden",
        }}
        className="w-full h-full fixed top-0 left-0 z-10 bg-black ease-in-out duration-300 "
        onClick={() => setOpen(false)}
      />
{/*本体*/}
<div className="p-5 text-white relative z-10">
{...}
</div>

3. 只能展开一个折叠内容的手风琴与能展开多项的手风琴

手风琴的每一个面板的实现方式基本与前文的选择框一致,只是每个面板的展开情况不再由该面板自己管理,而是由手风琴父组件统一管理。

传统意义上的只能展开一个折叠内容的手风琴可以由一个钩子函数保存当前展开 tabindex ,可展开多个内容的则需维护一整个指示展开下标的数组。以下样例代码中用一个 boolean 数组 tabs 保存每个面板现在展开状态,并且兼容单展开的情况,接受一个储存希望默认展开的面板的下标的 defaultValues 数组。

1
2
3
4
5
6
7
8
9
10
  const [tabs, setTabs] = useState<boolean[]>(
    new Array(data.length).fill(false),
  );
  useEffect(() => {
    const initTabs = tabs.map((tab, index) => {
      if (multiple) return defaultValues.includes(index);
      else return defaultValues.indexOf(index) === 0;
    });
    setTabs(initTabs);
  }, []);

完整的样例代码如下,可在 CodeSandbox 中编辑:

(想要预览,请在下方点击 Open devtool 的加号,选择 Preview: 3000,并在地址栏后添加 /selectbar/accordion