下拉筛选隐藏式遮罩
0. 缘起
本项目为taro
+react
的微信小程序,下拉筛选组件有许多改动。记录下有一个需求,当按钮或者输入框在筛选遮罩上方,希望点击按钮或者输入框遮罩自动关闭。
1. 思路
如果是web
,直接document.addEventListener
监听当前页面点击事件,如果在筛选器外,自动关闭筛选器。
但本项目是微信小程序,无法监听页面点击事件,且taro
的createSelectorQuery
只能获取到当前项长宽高定位信息。
看社区发现这里有个巧妙的思路,即上方增加透明遮罩,点击遮罩部分关闭。
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);
人生到处知何似,应似飞鸿踏雪泥。