Skip to content

Commit 6e7254b

Browse files
committed
Do not close dropdown on select when multi select
1 parent 90394c1 commit 6e7254b

2 files changed

Lines changed: 72 additions & 4 deletions

File tree

src/components/Dropdown.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Menu, Transition } from '@headlessui/react'
22
import React, { ChangeEvent, ReactNode, forwardRef, useEffect, useRef, useState } from 'react'
33

44
import classNames from 'classnames'
5+
import { useOutsideClick } from '../utils/useOutsideClick'
56

67
export interface Props {
78
options: Option[]
@@ -64,10 +65,19 @@ export const Dropdown = forwardRef<HTMLDivElement, Props>(function Dropdown(
6465
const [internalSelectedOptions, setInternalSelectedOptions] = useState<Option[]>([])
6566
const [filteredOptions, setFilteredOptions] = useState<Option[]>(options)
6667
const [searchTerm, setSearchTerm] = useState('')
68+
const [isOpen, setIsOpen] = useState(false)
6769
const searchInputRef = useRef<HTMLInputElement>(null)
70+
const dropdownRef = useRef<HTMLDivElement>(null)
6871

6972
const currentSelectedOptions = selectedOptions || internalSelectedOptions
7073

74+
// Close dropdown when clicking outside in multi-select mode
75+
useOutsideClick(dropdownRef, () => {
76+
if (isMultiSelect && isOpen) {
77+
setIsOpen(false)
78+
}
79+
})
80+
7181
useEffect(() => {
7282
if (selectedOption) {
7383
setActiveOption(selectedOption)
@@ -100,10 +110,12 @@ export const Dropdown = forwardRef<HTMLDivElement, Props>(function Dropdown(
100110
if (onSelectionChange) {
101111
onSelectionChange(newSelection)
102112
}
113+
// Keep dropdown open in multi-select mode
103114
} else {
104115
setActiveOption(option)
105116
if (option.onClick) option.onClick()
106117
if (onSelect) onSelect(option)
118+
setIsOpen(false) // Close dropdown in single-select mode
107119
}
108120
}
109121

@@ -144,30 +156,51 @@ export const Dropdown = forwardRef<HTMLDivElement, Props>(function Dropdown(
144156

145157
const canClear = onClear && (isMultiSelect ? currentSelectedOptions.length > 0 : !!activeOption)
146158

159+
// Merge refs to support both internal and forwarded refs
160+
const setRefs = (node: HTMLDivElement | null) => {
161+
// @ts-ignore - TypeScript has issues with ref assignment
162+
dropdownRef.current = node
163+
if (typeof ref === 'function') {
164+
ref(node)
165+
} else if (ref) {
166+
// @ts-ignore - TypeScript has issues with ref assignment
167+
ref.current = node
168+
}
169+
}
170+
147171
return (
148172
<Menu
149173
as="div"
150174
className={classNames('relative inline-block', className)}
151-
ref={ref}
175+
ref={setRefs}
152176
data-testid="dropdown"
153177
{...other}
154178
>
155179
{({ open }) => {
156-
if (open && isSearchable) {
180+
// Sync internal state with Menu's open state
181+
const menuIsOpen = isMultiSelect ? isOpen : open
182+
183+
if (menuIsOpen && isSearchable) {
157184
setTimeout(() => searchInputRef.current?.focus(), 50)
158185
}
159186

160187
return (
161188
<>
162189
{trigger ? (
163-
<Menu.Button className={buttonClassName}>{trigger}</Menu.Button>
190+
<Menu.Button
191+
className={buttonClassName}
192+
onClick={() => isMultiSelect && setIsOpen(!isOpen)}
193+
>
194+
{trigger}
195+
</Menu.Button>
164196
) : (
165197
<div className="relative">
166198
<Menu.Button
167199
className={classNames(
168200
'flex items-center justify-between w-full px-4 py-2 text-sm font-medium border rounded-md shadow-sm text-gray-800 bg-white border-gray-200 group hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-cool-gray-100 focus:ring-gray-300 dark:ring-gray-500 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-700',
169201
buttonClassName
170202
)}
203+
onClick={() => isMultiSelect && setIsOpen(!isOpen)}
171204
>
172205
{displayLabel}
173206
<svg
@@ -208,7 +241,7 @@ export const Dropdown = forwardRef<HTMLDivElement, Props>(function Dropdown(
208241
</div>
209242
)}
210243
<Transition
211-
show={open}
244+
show={menuIsOpen}
212245
enter="transition ease-out duration-300"
213246
enterFrom="transform opacity-0 scale-95"
214247
enterTo="transform opacity-100 scale-100"
@@ -218,6 +251,7 @@ export const Dropdown = forwardRef<HTMLDivElement, Props>(function Dropdown(
218251
className="min-w-sm"
219252
>
220253
<Menu.Items
254+
static={isMultiSelect}
221255
data-testid="dropdown-items"
222256
className={classNames(
223257
'absolute shadow-sm z-10 bg-white origin-top-right dark:bg-gray-800 border divide-y rounded-md outline-none border-cool-gray-200 divide-cool-gray-100 dark:divide-gray-400 dark:border-gray-700',

tests/Dropdown.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,14 @@ describe('Dropdown', () => {
9494
fireEvent.click(item0)
9595
expect(button).toHaveTextContent(options[0].label)
9696

97+
// Re-open dropdown after single-select closes it
98+
fireEvent.click(button)
9799
const item1 = getByTestId('item-1')
98100
fireEvent.click(item1)
99101
expect(button).toHaveTextContent(options[1].label)
100102

103+
// Re-open dropdown after single-select closes it
104+
fireEvent.click(button)
101105
const item2 = getByTestId('item-2')
102106
fireEvent.click(item2)
103107
expect(button).toHaveTextContent(options[2].label)
@@ -268,4 +272,34 @@ describe('Dropdown', () => {
268272
expect(checkbox).not.toBeChecked()
269273
})
270274
})
275+
276+
it('should keep dropdown open when selecting items in multi-select mode', () => {
277+
render(
278+
<MultiSelectUncontrolled
279+
options={connectorOptions}
280+
{...(MultiSelectUncontrolled.args as Partial<Props>)}
281+
/>
282+
)
283+
const button = screen.getByRole('button', { name: /select connectors \(uncontrolled\)/i })
284+
fireEvent.click(button)
285+
286+
// Verify dropdown is open
287+
const itemsContainer = screen.getByTestId('dropdown-items')
288+
expect(itemsContainer).toBeInTheDocument()
289+
290+
// Click first item
291+
const item0 = screen.getByTestId('item-0')
292+
fireEvent.click(item0)
293+
294+
// Verify dropdown is still open after first selection
295+
expect(screen.getByTestId('dropdown-items')).toBeInTheDocument()
296+
297+
// Click second item without re-opening
298+
const item2 = screen.getByTestId('item-2')
299+
fireEvent.click(item2)
300+
301+
// Verify dropdown is still open and both items are selected
302+
expect(screen.getByTestId('dropdown-items')).toBeInTheDocument()
303+
expect(button).toHaveTextContent('2 selected')
304+
})
271305
})

0 commit comments

Comments
 (0)