Skip to content

Commit

Permalink
Use popper to position the root menu
Browse files Browse the repository at this point in the history
  • Loading branch information
cdes committed Sep 30, 2020
1 parent d982040 commit aa974ca
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 87 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@
"typescript": "^4.0.2"
},
"dependencies": {
"@popperjs/core": "^2.5.3",
"immer": "^7.0.9",
"react": "^16.13.1"
"react": "^16.13.1",
"react-popper": "^2.2.3"
}
}
121 changes: 54 additions & 67 deletions src/react-headless-nested-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from 'react'
import React, { useState } from 'react'
import produce, { Draft } from 'immer'
import getEventPath, { handleRefs, getDirection } from './utils'
import { usePopper } from 'react-popper'
import { Placement } from '@popperjs/core'
import { Options } from '@popperjs/core/lib/modifiers/offset'

import getEventPath, { handleRefs, getDirection } from './utils'
export interface MenuItem {
id: string
label: string
Expand Down Expand Up @@ -35,8 +38,6 @@ interface ClosePathAction {

type Action = ToggleAction | OpenPathAction | ClosePathAction

type Placement = 'top' | 'bottom' | 'start' | 'end'

/**
* @ignore
*/
Expand All @@ -45,7 +46,7 @@ interface NestedMenuState {
isOpen: boolean
currentPath: string[]
currentPathItems: MenuItem[]
placement: Placement
placement?: Placement
}

/**
Expand Down Expand Up @@ -83,6 +84,7 @@ interface NestedMenuProps {
isOpen?: boolean
defaultOpenPath?: string[]
placement?: Placement
offset?: Options['offset']
}

// interface HitAreaProps {
Expand All @@ -98,7 +100,8 @@ export const useNestedMenu = ({
items = [],
isOpen = false,
defaultOpenPath = [],
placement = 'end',
placement,
offset,
}: NestedMenuProps) => {
const [state, dispatch] = React.useReducer(reducer, {
items,
Expand Down Expand Up @@ -171,15 +174,30 @@ export const useNestedMenu = ({
})

const menuRefs = React.useRef<{ [key: string]: HTMLElement }>({})

const getMenuProps = (item?: MenuItem) => ({
key: item?.id || 'root',
ref: handleRefs((itemNode) => {
if (itemNode) {
menuRefs.current[item?.id || 'root'] = itemNode
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null)

const getMenuProps = (item?: MenuItem) => {
if (item) {
return {
key: item.id,
ref: handleRefs((itemNode) => {
if (itemNode) {
menuRefs.current[item.id] = itemNode
}
}),
style: getMenuOffsetStyles(item),
}
}),
})
} else {
return {
key: 'root',
ref: handleRefs((itemNode) => {
setPopperElement(itemNode)
}),
style: styles.popper,
...attributes.popper,
}
}
}

const itemRefs = React.useRef<{ [key: string]: HTMLElement }>({})

Expand All @@ -192,63 +210,16 @@ export const useNestedMenu = ({
}),
})

const getMenuOffsetStyles = (currentItem?: MenuItem) => {
const item = currentItem ? itemRefs.current[currentItem.id] : null
const button = toggleButtonRef.current as HTMLElement
const getMenuOffsetStyles = (currentItem?: MenuItem): React.CSSProperties => {
if (!currentItem) return {}
const item = itemRefs.current[currentItem.id]

const dir = getDirection()
const rootXEnd =
dir === 'ltr'
? button.getBoundingClientRect().right
: window.innerWidth - button.getBoundingClientRect().left

let vertical: string = 'top'
let horizontal: string = dir === 'ltr' ? 'left' : 'right'
let verticalValue = item ? 0 : button.getBoundingClientRect().top
let horizontalValue = item ? item.getBoundingClientRect().width : rootXEnd

if (dir === 'ltr') {
if (placement === 'top') {
vertical = item ? 'top' : 'bottom'
verticalValue = item ? 0 : window.innerHeight - button.getBoundingClientRect().top
horizontalValue = item
? item.getBoundingClientRect().width
: button.getBoundingClientRect().left
} else if (placement === 'bottom') {
verticalValue = item ? 0 : button.getBoundingClientRect().bottom
horizontalValue = item
? item.getBoundingClientRect().width
: button.getBoundingClientRect().left
} else if (placement === 'start') {
horizontal = item ? 'left' : 'right'
horizontalValue = item
? item.getBoundingClientRect().width
: window.innerWidth - button.getBoundingClientRect().left
}
} else {
if (placement === 'top') {
vertical = item ? 'top' : 'bottom'
horizontal = 'right'
verticalValue = item ? 0 : window.innerHeight - button.getBoundingClientRect().top
horizontalValue = item
? item.getBoundingClientRect().width
: window.innerWidth - button.getBoundingClientRect().right
} else if (placement === 'bottom') {
verticalValue = item ? 0 : button.getBoundingClientRect().bottom
horizontalValue = item
? item.getBoundingClientRect().width
: window.innerWidth - button.getBoundingClientRect().right
} else if (placement === 'start') {
horizontal = item ? 'right' : 'left'
horizontalValue = item
? item.getBoundingClientRect().width
: button.getBoundingClientRect().right
}
}

return {
[vertical]: verticalValue,
[horizontal]: horizontalValue,
position: 'absolute',
top: 0,
[dir === 'ltr' ? 'left' : 'right']: item.clientWidth,
}
}

Expand Down Expand Up @@ -294,6 +265,22 @@ export const useNestedMenu = ({
const anchorRef = React.useRef<HTMLElement>()
const menuRef = React.useRef<HTMLElement>()

const { styles, attributes } = usePopper(toggleButtonRef.current, popperElement, {
placement: 'right-start',
modifiers: [
{
name: 'offset',
options: {
offset: offset,
},
},
{
name: 'flip',
enabled: false,
},
],
})

return {
getToggleButtonProps,
getMenuProps,
Expand Down
32 changes: 13 additions & 19 deletions test/react-headless-nested-menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const simpleList = [
id: 'ckeri9fsh00023g65zjdr0wdx',
label: 'Email',
},
];
]

const Basic: React.FC = () => {
const {
Expand All @@ -27,7 +27,7 @@ const Basic: React.FC = () => {
getOpenTriggerProps,
toggleMenu,
isSubMenuOpen,
isOpen
isOpen,
} = useNestedMenu({
items: simpleList,
})
Expand All @@ -41,9 +41,7 @@ const Basic: React.FC = () => {
toggleMenu()
}}
>
<div
className={isSubMenuOpen(item) ? 'sub-open': 'sub-closed'}
>
<div className={isSubMenuOpen(item) ? 'sub-open' : 'sub-closed'}>
{item.label}
{item.subMenu && <span className="chevron" />}
</div>
Expand All @@ -55,10 +53,6 @@ const Basic: React.FC = () => {
const renderMenu = (items: Items, parentItem?: Items[0]) => (
<div
{...getMenuProps(parentItem)}
style={{
position: 'absolute',
...getMenuOffsetStyles(parentItem),
}}
className={typeof parentItem === 'undefined' ? 'root' : 'sub'}
{...getCloseTriggerProps('onPointerLeave', parentItem)}
>
Expand Down Expand Up @@ -91,15 +85,15 @@ const Basic: React.FC = () => {

describe('Hook', () => {
it('renders basic menu', () => {
const { container, queryByText } = render(<Basic />);
const btn = queryByText('Toggle');
expect(btn).toBeDefined();
expect(queryByText('Name')).toBe(null);
expect(queryByText('Photo')).toBe(null);
expect(queryByText('Email')).toBe(null);
fireEvent.click(btn!);
expect(queryByText('Name')).toBeDefined();
expect(queryByText('Photo')).toBeDefined();
expect(queryByText('Email')).toBeDefined();
const { container, queryByText } = render(<Basic />)
const btn = queryByText('Toggle')
expect(btn).toBeDefined()
expect(queryByText('Name')).toBe(null)
expect(queryByText('Photo')).toBe(null)
expect(queryByText('Email')).toBe(null)
fireEvent.click(btn!)
expect(queryByText('Name')).toBeDefined()
expect(queryByText('Photo')).toBeDefined()
expect(queryByText('Email')).toBeDefined()
})
})
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,11 @@
dependencies:
"@types/node" ">= 8"

"@popperjs/core@^2.5.3":
version "2.5.3"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.5.3.tgz#4982b0b66b7a4cf949b86f5d25a8cf757d3cfd9d"
integrity sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==

"@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
Expand Down Expand Up @@ -7289,11 +7294,24 @@ react-dom@^16.13.1:
prop-types "^15.6.2"
scheduler "^0.19.1"

react-fast-compare@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==

react-is@^16.12.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

react-popper@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.3.tgz#33d425fa6975d4bd54d9acd64897a89d904b9d97"
integrity sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw==
dependencies:
react-fast-compare "^3.0.1"
warning "^4.0.2"

react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
Expand Down Expand Up @@ -9153,6 +9171,13 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"

warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"

wcwidth@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
Expand Down

0 comments on commit aa974ca

Please sign in to comment.