再續前一篇研究了一輪,這篇就來實作:
透過 Hook 可以達成:
以 Dialog 情境來說,點擊非 Dialog 本身要能夠進一步關閉。
首先,建立一個類似 Dialog 的內容,並且可以開開關關:
function Example() {
const [open, setOpen] = useState(false)
const handleToggle = () => {
setOpen((prev) => !prev)
}
return (
<>
<Button onClick={handleToggle}>Open</Button>
<PortalDialog open={open} onClose={handleToggle} />
</>
)
}
<PortalDialog/>
則是利用 Chakra-UI <Portal/>
來處理 Dialog 渲染到document.body 中的最後一個子元素,<PortalDialog/>
則是長這樣子:
function PortalDialog(props) {
if (!props.open) return null
return (
<Portal>
<Dialog {...props} />
</Portal>
)
}
function useClickOutside(cb) {
const ref = useRef()
useEffect(() => {
const handleClick = (event) => {
const target = event.target
const isInside = ref.current.contains(target)
if (!isInside) {
//do the job
cb()
}
}
document.addEventListener("click", handleClick)
return () => {
document.removeEventListener("click", handleClick)
}
}, [])
return ref
}
我們搭配 useEffect + useRef 的組合,這邊的 ref 是要來存取 DOM element 的,會在這個元素上額外「手動」添加 click event;handleClick
是主要的核心:
const isInside = ref.current.contains(target)
而功能就是當點擊的目標並不存在於 element 裡面的話,就會執行一開始所傳入的 callback。
<Dialog/>
本身是長這樣:
function Dialog({ onClose }) {
const ref = useClickOutside(onClose) //把要執行的cb傳入 hook
return (
<Overlay> // 將內容呈現最上方
<Dialog ref={ref}> // 點擊的範圍,這邊的範圍之外,都是「外面」
<Text>{"Hello (ノ>ω<)ノ"}</Text>
<Button colorScheme="red" onClick={onClose}>
Close
</Button>
</Dialog>
</Overlay>
)
}
Overlay 與 Dialog 只是一般的 div element,這邊把結構語意化比較好理解。嘗試重現的話,要再留意是否需要fowardRef。
如圖,期望的點擊範圍就是紅線虛線所包起來的白色區域(Dialog)
我們把 overlay 再加上一點顏色,來讓整個 Dialog 更明顯,這樣功能大致上就完成了!
useClickOutside 並沒有特定侷限要用在 dialog 上,也可以用於其他需要搭配點擊外面來觸發額外動作的情境;進一步,其他的事件像是觸發頻率更高的 mouseover,也可以用一樣的概念製作。