Skip to content

Commit 84be985

Browse files
authored
fix(ui-patterns): multiselect click behavior (supabase#41726)
* fix(ui-patterns): multiselect click behavior Multiselect clears only when it opens. * fix(ui-patterns): multiselect placement Multiselect placement needs to account for the remaining space so it doesn't get cut off by the window edges. * fixup! fix(ui-patterns): multiselect click behavior
1 parent c0e94ec commit 84be985

1 file changed

Lines changed: 68 additions & 11 deletions

File tree

packages/ui-patterns/src/multi-select/multi-select.tsx

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ interface MultiSelectContextProps {
2727
setActiveIndex: React.Dispatch<React.SetStateAction<number>>
2828
size: MultiSelectorProps['size']
2929
disabled?: boolean
30+
dropdownPlacement: 'top' | 'bottom'
31+
dropdownMaxHeight: number
3032
}
3133

3234
const MultiSelectContext = React.createContext<MultiSelectContextProps | null>(null)
3335

36+
const DROPDOWN_MAX_HEIGHT = 300
37+
const DROPDOWN_GAP = 8
38+
3439
const commandItemClass = cn(
3540
'relative text-foreground-lighter text-left px-2 py-1.5 rounded',
3641
'hover:text-foreground hover:!bg-overlay-hover w-full flex items-center space-x-2',
@@ -81,6 +86,9 @@ function MultiSelector({
8186
const [inputValue, setInputValue] = React.useState<string>('')
8287
const [activeIndex, setActiveIndex] = React.useState<number>(-1)
8388

89+
const [dropdownPlacement, setDropdownPlacement] = React.useState<'top' | 'bottom'>('bottom')
90+
const [dropdownMaxHeight, setDropdownMaxHeight] = React.useState<number>(DROPDOWN_MAX_HEIGHT)
91+
8492
const toggleValue = React.useCallback(
8593
(toggledValue: string) => {
8694
if (values.includes(toggledValue)) {
@@ -92,10 +100,42 @@ function MultiSelector({
92100
[values]
93101
)
94102

103+
const updateDropdownMetrics = React.useCallback(() => {
104+
if (typeof window === 'undefined') return
105+
const triggerEl = ref.current as HTMLDivElement | null
106+
if (!triggerEl) return
107+
108+
const rect = triggerEl.getBoundingClientRect()
109+
const viewportHeight = window.innerHeight
110+
const spaceBelow = viewportHeight - rect.bottom - DROPDOWN_GAP
111+
const spaceAbove = rect.top - DROPDOWN_GAP
112+
const shouldDropUp = spaceBelow < DROPDOWN_MAX_HEIGHT && spaceAbove > spaceBelow
113+
const placement = shouldDropUp ? 'top' : 'bottom'
114+
const availableSpace = Math.max(placement === 'top' ? spaceAbove : spaceBelow, 0)
115+
const nextHeight =
116+
availableSpace > 0 ? Math.min(DROPDOWN_MAX_HEIGHT, availableSpace) : DROPDOWN_MAX_HEIGHT
117+
118+
setDropdownPlacement(placement)
119+
setDropdownMaxHeight(nextHeight)
120+
}, [])
121+
122+
useEffect(() => {
123+
if (!open) return
124+
const controller = new AbortController()
125+
const { signal } = controller
126+
127+
const handleUpdate = updateDropdownMetrics
128+
handleUpdate()
129+
window.addEventListener('resize', handleUpdate, { signal })
130+
window.addEventListener('scroll', handleUpdate, { capture: true, passive: true, signal })
131+
132+
return () => controller.abort()
133+
}, [open, updateDropdownMetrics])
134+
95135
// detect clicks from outside
96136
useOnClickOutside(ref, () => {
97137
if (open) {
98-
setOpen(!open)
138+
setOpen(false)
99139
}
100140
})
101141

@@ -143,6 +183,8 @@ function MultiSelector({
143183
setActiveIndex,
144184
size: size || 'small',
145185
disabled,
186+
dropdownPlacement,
187+
dropdownMaxHeight,
146188
}}
147189
>
148190
<Command
@@ -183,7 +225,8 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
183225
},
184226
ref
185227
) => {
186-
const { activeIndex, values, setInputValue, toggleValue, disabled, setOpen } = useMultiSelect()
228+
const { activeIndex, values, setInputValue, toggleValue, disabled, open, setOpen } =
229+
useMultiSelect()
187230

188231
const inputRef = React.useRef<HTMLButtonElement>(null)
189232

@@ -216,19 +259,27 @@ const MultiSelectorTrigger = React.forwardRef<HTMLButtonElement, MultiSelectorTr
216259

217260
const handleTriggerClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(
218261
(event) => {
219-
setOpen(true)
220-
setInputValue('')
221-
222262
if (IS_INLINE_MODE) {
263+
if (!open) {
264+
setOpen(true)
265+
setInputValue('')
266+
}
267+
223268
event.stopPropagation()
224269
event.preventDefault()
225270

226271
setTimeout(() => {
227272
inlineInputRef.current?.focus()
228273
}, 100)
274+
275+
return
229276
}
277+
278+
const willOpen = !open
279+
setOpen(willOpen)
280+
if (willOpen) setInputValue('')
230281
},
231-
[]
282+
[open, setOpen, setInputValue, IS_INLINE_MODE]
232283
)
233284

234285
return (
@@ -404,16 +455,21 @@ MultiSelector.Input = MultiSelectorInput
404455

405456
const MultiSelectorContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
406457
({ className, children }, ref) => {
407-
const { open } = useMultiSelect()
458+
const { open, dropdownPlacement } = useMultiSelect()
459+
460+
const closedTranslationClass = dropdownPlacement === 'top' ? 'translate-y-3' : '-translate-y-3'
408461

409462
return (
410463
<div
411464
ref={ref}
412465
className={cn(
413-
'absolute w-full bg-overlay shadow-md z-10 border border-overlay top-[calc(100%+0.25rem)] rounded-md transition-all -translate-y-3',
466+
'absolute w-full bg-overlay shadow-md z-10 border border-overlay rounded-md transition-all',
467+
dropdownPlacement === 'top'
468+
? 'bottom-[calc(100%+0.25rem)] origin-bottom'
469+
: 'top-[calc(100%+0.25rem)] origin-top',
414470
open
415471
? 'opacity-100 translate-y-0 visible duration-150 ease-[.76,0,.23,1]'
416-
: 'opacity-0 -translate-y-3 invisible duration-0',
472+
: cn('opacity-0 invisible duration-0', closedTranslationClass),
417473
className
418474
)}
419475
>
@@ -432,7 +488,7 @@ const MultiSelectorList = React.forwardRef<
432488
creatable?: boolean
433489
}
434490
>(({ className, children, creatable = false }, ref) => {
435-
const { open, inputValue, setInputValue, toggleValue } = useMultiSelect()
491+
const { open, inputValue, setInputValue, toggleValue, dropdownMaxHeight } = useMultiSelect()
436492

437493
const options = !!children
438494
? Array.isArray(children)
@@ -452,9 +508,10 @@ const MultiSelectorList = React.forwardRef<
452508
className={cn(
453509
'p-2 flex flex-col gap-2 scrollbar-thin scrollbar-track-transparent transition-colors',
454510
'scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted',
455-
'scrollbar-thumb-rounded-lg w-full max-h-[300px] overflow-y-auto',
511+
'scrollbar-thumb-rounded-lg w-full overflow-y-auto',
456512
className
457513
)}
514+
style={{ maxHeight: dropdownMaxHeight }}
458515
>
459516
{children}
460517
{creatable && inputValue.length > 0 && !isOptionExists ? (

0 commit comments

Comments
 (0)