Skip to content

Commit

Permalink
fix: use floating-ui middleware to hide dropdown menu
Browse files Browse the repository at this point in the history
  • Loading branch information
LeonardYam committed Aug 3, 2023
1 parent b2c4709 commit d08d6d2
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 95 deletions.
112 changes: 36 additions & 76 deletions frontend/src/components/Dropdown/components/SelectMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Virtuoso } from 'react-virtuoso'
import { List, ListItem } from '@chakra-ui/react'
import { FloatingPortal } from '@floating-ui/react-dom-interactions'

import { VIRTUAL_LIST_OVERSCAN_HEIGHT } from '../constants'
import { useSelectContext } from '../SelectContext'
Expand All @@ -8,95 +9,54 @@ import { itemToValue } from '../utils/itemUtils'
import { DropdownItem } from './DropdownItem'
import { useSelectPopover } from './SelectPopover'

const ListItems = () => {
const { items } = useSelectContext()
return (
<>
{items.map((item, idx) => {
return (
<DropdownItem
key={`${itemToValue(item)}${idx}`}
item={item}
index={idx}
/>
)
})}
</>
)
}

const NothingFoundItem = () => {
const { styles, nothingFoundLabel } = useSelectContext()
return (
<ListItem role="option" sx={styles.emptyItem}>
{nothingFoundLabel}
</ListItem>
)
}

const RenderIfOpen = ({
isOpen,
children,
}: {
isOpen: boolean
children: JSX.Element
}) => {
if (!isOpen) {
return null
}
return children
}

export const SelectMenu = (): JSX.Element => {
const {
getMenuProps,
isOpen,
items,
nothingFoundLabel,
styles,
virtualListRef,
virtualListHeight,
fullWidth,
} = useSelectContext()

const { floatingRef, floatingStyles } = useSelectPopover()

const listSx = {
...styles.list,
...(fullWidth ? { maxH: '100%' } : {}),
}

return (
<List
{...getMenuProps({ ref: floatingRef })}
style={floatingStyles}
sx={listSx}
zIndex="dropdown"
>
<RenderIfOpen isOpen={isOpen}>
{items.length > 0 ? (
fullWidth ? (
<ListItems />
) : (
<Virtuoso
ref={virtualListRef}
data={items}
overscan={VIRTUAL_LIST_OVERSCAN_HEIGHT}
style={{ height: virtualListHeight }}
itemContent={(index, item) => {
return (
<DropdownItem
key={`${itemToValue(item)}${index}`}
item={item}
index={index}
/>
)
}}
/>
)
) : (
<NothingFoundItem />
<FloatingPortal>
<List
{...getMenuProps(
{ ref: floatingRef },
// Suppressing ref error since this will be in a portal and will be conditionally rendered.
// See https://github.com/downshift-js/downshift/issues/1272#issuecomment-1063244446
{ suppressRefError: true },
)}
style={floatingStyles}
sx={styles.list}
>
{isOpen && items.length > 0 && (
<Virtuoso
ref={virtualListRef}
data={items}
overscan={VIRTUAL_LIST_OVERSCAN_HEIGHT}
style={{ height: virtualListHeight }}
itemContent={(index, item) => {
return (
<DropdownItem
key={`${itemToValue(item)}${index}`}
item={item}
index={index}
/>
)
}}
/>
)}
</RenderIfOpen>
</List>
{isOpen && items.length === 0 ? (
<ListItem role="option" sx={styles.emptyItem}>
{nothingFoundLabel}
</ListItem>
) : null}
</List>
</FloatingPortal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
autoUpdate,
flip,
hide,
Middleware,
offset,
size,
useFloating,
Expand All @@ -18,35 +19,61 @@ export const SelectPopoverProvider: FC = ({ children }): JSX.Element => {

const wrapperRef = useRef<HTMLDivElement | null>(null)

const { x, y, refs, reference, floating, strategy, update } = useFloating({
placement: 'bottom-start',
strategy: 'absolute',
open: isOpen,
middleware: [
// offset middleware should be the first middleware
offset(1),
flip(),
hide(),
// Set width to be the same as the reference element.
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
})
// Custom hide middleware that ensures dropdown options are hidden correctly
// by the sliding MiniHeader in public form pages.
const hideWhenCoveredByHeader: Middleware = {
name: 'hideWhenCoveredByHeader',
fn(state) {
const { reference } = state.elements
const { ownerDocument } = reference as Element
const { x, y, height } = reference.getBoundingClientRect()
// Get className of all elements that cover the bottom-left corner of the combobox
const elementsClassName = ownerDocument
.elementsFromPoint(x, y + height)
.map((e: Element) => e.className)
return {
data: {
isCovered: elementsClassName.includes('chakra-slide'),
},
}),
],
})
}
},
}

const { x, y, refs, reference, floating, strategy, update, middlewareData } =
useFloating({
placement: 'bottom-start',
strategy: 'absolute',
open: isOpen,
middleware: [
// offset middleware should be the first middleware
offset(1),
flip(),
hide(),
hideWhenCoveredByHeader,
// Set width to be the same as the reference element.
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
})
},
}),
],
})

const mergedReferenceRefs = useMergeRefs(reference, wrapperRef)

const { referenceHidden } = middlewareData.hide || {}
const { isCovered } = middlewareData.hideWhenCoveredByHeader || {}

const floatingStyles = useMemo(
() => ({
visibility: referenceHidden || isCovered ? 'hidden' : 'visible',
position: strategy,
top: y ?? 0,
left: x ?? 0,
}),
[strategy, x, y],
[referenceHidden, isCovered, strategy, x, y],
)

// Allows
Expand Down

0 comments on commit d08d6d2

Please sign in to comment.