Vant Dropdown实现

github地址open in new window体验地址open in new window

在前东家猪厂的时候, 部门开发移动端组件库open in new window, 当时市面上react版的移动端组件库质量都不太行(对,说的就是你, antd-mobile(旧版本open in new window)(现在重构后v5版本open in new window应该好很多了)), 市面上做的比较好的主要是基于VueVant, 然后想移植DropdownMenu组件, 就需要把实现从Vue翻译到react, 也是一次比较深入的阅读源码过程(开源项目的源码一般都比较复杂和高封装, 还是得带着一些为什么去读, 不然读起来会像无头苍蝇)

体验了下demo, 难点应该是动画的处理和位置的计算

  • 动画处理, transition监听不了display: none的变化, 翻看了Vant的源码, 主要就是VueTransition组件, 那React也有相关的组件 import { CSSTransition } from 'react-transition-group'

  • 位置计算, 需要获取父元素的rect, 并且监听页面滚动时需同步更新页面

Vant 最简接入

相关参数应该可以意会,就不做过多赘述, 具体可参照Vant官方文档open in new window

  <van-dropdown-menu>
    <van-dropdown-item v-model="value1" :options="option1" />
    <van-dropdown-item v-model="value2" :options="option2" />
  </van-dropdown-menu>

  import { ref } from 'vue';
  export default {
    setup() {
      const value1 = ref(0);
      const value2 = ref('a');
      const option1 = [
        { text: '全部商品', value: 0 },
        { text: '新款商品', value: 1 },
        { text: '活动商品', value: 2 },
      ];
      const option2 = [
        { text: '默认排序', value: 'a' },
        { text: '好评排序', value: 'b' },
        { text: '销量排序', value: 'c' },
      ];

      return {
        value1,
        value2,
        option1,
        option2,
      };
    },
  };
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

效果图

位置计算

计算图

如果PopUp是往下,实际要获取的位置就是绿色box,那top是蓝色box的bottom

如果PopUp是往上,实际要获取的位置就是红色box,那bottom是100vh - 蓝色box的top

direction === 'down'
  ? {
      top: dropDownMenuRef.current?.getBoundingClientRect()
        .bottom,
      bottom: 0,
    }
  : {
      top: 0,
      // bottom: `calc(100vh - ${dropDownMenuRef.current?.getBoundingClientRect().top}px)`,
      bottom: `calc(${window.innerHeight}px - ${
        dropDownMenuRef.current?.getBoundingClientRect().top
      }px)`,
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

点击外部区域自动收起 closeOnClickOutside

给外部加点击事件

vant的实现, 给document绑定事件代理,通过element.contains(event.target as Node)判断是否是外部点击,是的话就执行绑定的listener(即关闭dropdownMenu), react其实可以直接复用ahooksuseClickAwayopen in new window, 实现基本一致

  export function useClickAway(
    target: Element | Ref<Element | undefined>,
    listener: EventListener,
    options: UseClickAwayOptions = {}
  ) {
    if (!inBrowser) {
      return;
    }

    const { eventName = 'click' } = options;

    const onClick = (event: Event) => {
      const element = unref(target);
      if (element && !element.contains(event.target as Node)) {
        listener(event);
      }
    };

    useEventListener(eventName, onClick, { target: document });
  }

  const onClickAway = () => {
    if (props.closeOnClickOutside) {
      children.forEach((item) => {
        item.toggle(false);
      });
    }
  };

  useClickAway(root, onClickAway);

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
30
31

ref消失

遇到的问题,至今没有解决,我们一般会使用ref传入target dom, 但遇到问题ref在react-transition-group结合使用时反复切换时会变成null的问题,记个TODO,参考issueopen in new window,codeSanBoxopen in new window

ref问题解决,请教了下同事 问题代码如下,我们会根据不同的condition生成一个CSSTransition组件,并通过ref去获取到其中的dom节点

<CSSTransition
  in={condition1}
  unmountOnExit
  classNames="alert"
>
  <div ref={ref}>content1</div>
  <div>overlay</div>
</CSSTransition>
<CSSTransition
  in={condition2}
  unmountOnExit
  classNames="alert"
>
  <div ref={ref}>content2</div>
  <div>overlay</div>
</CSSTransition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如下代码可以捕获到ref的正确引用

function Demo2({ children }) {
  return <div>{children}</div>;
}

function Demo1() {
  const ref = useRef(null);
  const [show, setShow] = useState(false);
  return (
    <div>
      Change Ref
      <Button
        onClick={() => setShow((pre) => !pre)}
        size="lg"
      >
        change ref dom
      </Button>
      <Demo2>
        {show && <div ref={ref}>Demo2</div>}
      </Demo2>
      <Demo2>
        {!show && <span ref={ref}>Demo3</span>}
      </Demo2>
    </div>
  );
}
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

那问题应该就是CSSTransition高阶组件做了更多额外的处理,过程是ref = div1 --> ref = div2 --> ref = null, 而我们写的demo是ref = div1 --> ref = null --> ref = div2, 最后的解决方案是采取了refs数组,每个CSSTransition都有独属于它的ref, 最后的过程ref1 = div1 --> ref2 = div2 --> ref1 = null

zIndex计算

TODO找个时间完善

具体可参照nutuiopen in new window这篇文章,vant的实现基本也差不多

Last Updated:
Contributors: 赵仁建