Skip to content

Commit

Permalink
fix: Fix dropdown trigger bug
Browse files Browse the repository at this point in the history
  • Loading branch information
nabeliwo committed Dec 10, 2019
1 parent 88acbdf commit 34aee6b
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 16 deletions.
28 changes: 19 additions & 9 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'
import React, { FC, ReactNode, createContext, useState, useEffect, useRef, useMemo } from 'react'
import { createPortal } from 'react-dom'

import { Rect, hasParentElement } from './dropdownHelper'
import { Rect, getRandomStr, includeDropdownElement } from './dropdownHelper'

type Props = {
children: ReactNode
}

type DropdownContextType = {
dropdownKey: string
active: boolean
triggerRect: Rect
onClickTrigger: (rect: Rect) => void
onClickCloser: () => void
DropdownContentRoot: React.FC<{ children: React.ReactNode }>
DropdownContentRoot: FC<{ children: ReactNode }>
}

const initialRect = { top: 0, right: 0, bottom: 0, left: 0 }

export const DropdownContext = React.createContext<DropdownContextType>({
export const DropdownContext = createContext<DropdownContextType>({
dropdownKey: '',
active: false,
triggerRect: initialRect,
onClickTrigger: () => {},
onClickCloser: () => {},
DropdownContentRoot: () => null,
})

export const Dropdown: React.FC<{}> = ({ children }) => {
export const Dropdown: FC<Props> = ({ children }) => {
const [active, setActive] = useState(false)
const [triggerRect, setTriggerRect] = useState<Rect>(initialRect)
const [dropdownKey] = useState(getRandomStr())

const element = useRef(document.createElement('div')).current

useEffect(() => {
const onClickBody = (e: any) => {
if (hasParentElement(e.target, element)) return
if (includeDropdownElement(e.target, `dropdown-${dropdownKey}`)) return
setActive(false)
}

Expand All @@ -40,10 +47,10 @@ export const Dropdown: React.FC<{}> = ({ children }) => {
document.body.removeChild(element)
document.body.removeEventListener('click', onClickBody, false)
}
}, [element])
}, [dropdownKey, element])

// This is the root container of a dropdown content located in outside the DOM tree
const DropdownContentRoot = useMemo<React.FC<{ children: React.ReactNode }>>(
const DropdownContentRoot = useMemo<FC<{ children: ReactNode }>>(
() => props => {
if (!active) return null
return createPortal(props.children, element)
Expand All @@ -56,14 +63,17 @@ export const Dropdown: React.FC<{}> = ({ children }) => {
return (
<DropdownContext.Provider
value={{
dropdownKey: `dropdown-${dropdownKey}`,
active,
triggerRect,
onClickTrigger: rect => {
const newActive = !active
setActive(newActive)
if (newActive) setTriggerRect(rect)
},
onClickCloser: () => setActive(false),
onClickCloser: () => {
setActive(false)
},
DropdownContentRoot,
}}
>
Expand Down
5 changes: 4 additions & 1 deletion src/components/Dropdown/DropdownCloser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useContext } from 'react'

import { DropdownContext } from './Dropdown'
import { DropdownContentContext } from './DropdownContent'

type Props = {
Expand All @@ -8,9 +9,11 @@ type Props = {
}

export const DropdownCloser: React.FC<Props> = ({ children, className = '' }) => {
const { dropdownKey } = useContext(DropdownContext)
const { onClickCloser } = useContext(DropdownContentContext)

return (
<div className={className} onClick={onClickCloser}>
<div className={`${dropdownKey} ${className}`} onClick={onClickCloser}>
{children}
</div>
)
Expand Down
7 changes: 5 additions & 2 deletions src/components/Dropdown/DropdownContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export const DropdownContent: React.FC<Props> = ({
className = '',
children,
}) => {
const { DropdownContentRoot, triggerRect, onClickCloser } = useContext(DropdownContext)
const { dropdownKey, DropdownContentRoot, triggerRect, onClickCloser } = useContext(
DropdownContext,
)

return (
<DropdownContentRoot>
<DropdownContentContext.Provider value={{ onClickCloser }}>
<DropdownContentInner triggerRect={triggerRect} className={className}>
<DropdownContentInner triggerRect={triggerRect} className={`${dropdownKey} ${className}`}>
{controllable ? children : <DropdownCloser>{children}</DropdownCloser>}
</DropdownContentInner>
</DropdownContentContext.Provider>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Dropdown/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Props = {
}

export const DropdownTrigger: React.FC<Props> = ({ children, className = '' }) => {
const { active, onClickTrigger } = useContext(DropdownContext)
const { dropdownKey, active, onClickTrigger } = useContext(DropdownContext)

return (
<Wrapper
Expand All @@ -22,7 +22,7 @@ export const DropdownTrigger: React.FC<Props> = ({ children, className = '' }) =
left: rect.left,
})
}}
className={className}
className={`${dropdownKey} ${className}`}
>
{React.Children.map(children, (child: any) => {
const props = child.props ? child.props : {}
Expand Down
16 changes: 14 additions & 2 deletions src/components/Dropdown/dropdownHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@ export type Rect = {
left: number
}

export function hasParentElement(element: HTMLElement | null, parent: HTMLElement | null): boolean {
export function getRandomStr() {
return Math.random()
.toString(32)
.substring(2)
}

export function includeDropdownElement(
element: HTMLElement | null,
dropdownClassName: string,
): boolean {
if (!element) return false
return element === parent || hasParentElement(element.parentElement, parent)
return (
element.classList.contains(dropdownClassName) ||
includeDropdownElement(element.parentElement, dropdownClassName)
)
}

type Size = { width: number; height: number }
Expand Down

0 comments on commit 34aee6b

Please sign in to comment.