下拉筛选隐藏式遮罩

0. 缘起

本项目为taro+react的微信小程序,下拉筛选组件有许多改动。记录下有一个需求,当按钮或者输入框在筛选遮罩上方,希望点击按钮或者输入框遮罩自动关闭。

1. 思路

如果是web,直接document.addEventListener监听当前页面点击事件,如果在筛选器外,自动关闭筛选器。
但本项目是微信小程序,无法监听页面点击事件,且tarocreateSelectorQuery只能获取到当前项长宽高定位信息。
看社区发现这里有个巧妙的思路,即上方增加透明遮罩,点击遮罩部分关闭。

createSelectorQuery

const q = Taro.createSelectorQuery();
q.select(`#${uniqueId}`)
  .boundingClientRect((res: NodesRef.BoundingClientRectCallbackResult) => {
    $title.current = res;
  })
  .exec();

注意细节点

如果有多个下拉筛选,透明遮罩层不会覆盖在上面,靠closeOverlay解决。这里用到了globalState用于存储全局变量
如果不是筛选,则靠透明遮罩。

2. 组件

代码如下

import React, {
  useState,
  useEffect,
  useRef,
  useImperativeHandle,
  forwardRef,
} from 'react';
import { RootPortal, View } from '@tarojs/components';
import Taro, { NodesRef, useDidShow } from '@tarojs/taro';
import { globalState } from '@/global';
import styles from './index.module.less';

interface DropdownMenuProps {
  title: React.ReactNode | string;
  children: React.ReactNode;
  // 是否在点击遮罩层后关闭
  closeOnClickOverlay?: boolean;
  onClose: () => void;
  onOpen?: () => void;
}

export type DropdownInstance = {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
};

const DropdownMenu: React.ForwardRefRenderFunction<
  DropdownInstance,
  DropdownMenuProps
> = ({ title, closeOnClickOverlay = true, children, onClose, onOpen }, ref) => {
  const [isOpen, setIsOpen] = useState(false);
  const [screenWidth, setScreenWidth] = useState(0);

  const $title = useRef<NodesRef.BoundingClientRectCallbackResult>();
  const indexRef = useRef<any>(`${+new Date()}${Math.ceil(Math.random() * 10000)}`);

  useEffect(() => {
    const systemInfo = Taro.getSystemInfoSync();
    setScreenWidth(systemInfo.screenWidth);
  }, []);

  const uniqueId = `DROP_ROOT_TITLE_${indexRef.current}`;

  useDidShow(() => {
    Taro.nextTick(() => {
      const q = Taro.createSelectorQuery();
      q.select(`#${uniqueId}`)
        .boundingClientRect((res: NodesRef.BoundingClientRectCallbackResult) => {
          $title.current = res;
        })
        .exec();
    });
  });

  useImperativeHandle(ref, () => {
    return {
      isOpen,
      setIsOpen,
    };
  });

  const closeOverlay = () => {
    if (isOpen) {
      onClose();
      globalState.dropdownBus = [];
    } else if (ref) {
      // When open the dropdown, remember it.
      globalState.dropdownBus.push({ id: uniqueId, open: true, ref: ref });
      const isExistDuplicateOpen =
        globalState.dropdownBus.filter(item => item.open)?.length > 1;

      // When the dropdown opened, the previous dropdown closed.
      if (isExistDuplicateOpen) {
        const previousOpen = globalState.dropdownBus.find(
          item => item.open && !uniqueId.includes(item.id)
        );
        if (previousOpen) {
          previousOpen.ref?.current?.setIsOpen(false);
          globalState.dropdownBus = globalState.dropdownBus.map(item => {
            if (item.open && !uniqueId.includes(item.id)) {
              return { ...item, open: false };
            }
            return item;
          });
        }
      }
    }
  };

  const handleToggle = () => {
    closeOverlay();
    setIsOpen(!isOpen);
  };

  const getNowTop = () => {
    return ($title.current?.top || 0) + ($title.current?.height || 0);
  };

  const getNowBottom = () => {
    const height = getNowTop();
    const bottom = `calc(100vh - ${height}px)`;
    return bottom;
  };

  const closeLayer = () => {
    if (closeOnClickOverlay) {
      closeOverlay();
      setIsOpen(false);
    }
  };

  return (
    <View>
      <View className={styles['drop-wrapper']}>
        <View id={uniqueId} onClick={handleToggle}>
          {title}
        </View>
        {isOpen && (
          <RootPortal enable>
            <View
              catchMove
              className={styles['drop-box']}
              style={{
                width: `${screenWidth}px`,
                top: getNowTop(),
              }}
              onClick={closeLayer}>
              <View
                onClick={e => {
                  e.preventDefault();
                  e.stopPropagation();
                }}
                style={{ background: '#fff' }}>
                {children}
              </View>
            </View>
          </RootPortal>
        )}
      </View>
      {isOpen && (
        <View
          onClick={closeLayer}
          className={styles['hide-layer']}
          style={{ bottom: getNowBottom() }}></View>
      )}
    </View>
  );
};

export default forwardRef(DropdownMenu);
posted @ 2024-10-28 15:56  乐盘游  阅读(17)  评论(0编辑  收藏  举报