-
Notifications
You must be signed in to change notification settings - Fork 0
[Component] Dropdown #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: nextjs-tailwind
Are you sure you want to change the base?
Changes from all commits
25f30c3
5ed8a84
a4e5fbf
c45f354
8505b0e
60e6770
10e13db
f085ae1
7a062b8
c7be2e0
0da3bb2
4796434
39594fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
amnambiar marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
'use client'; | ||
import React, { useEffect, useRef, useState } from 'react'; | ||
import Button from './Button'; | ||
import Checkbox from './Checkbox'; | ||
import {RadioButton} from './RadioGroup'; | ||
import {IconProps} from './Icon'; | ||
|
||
|
||
interface Option { | ||
label: string; | ||
value: string; | ||
disabled?: boolean; | ||
defaultChecked?: boolean; | ||
suffixText?: string; | ||
} | ||
|
||
export interface DropdownProps { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the dropdownMenu component I added a size property. Can you take a look at how I implemented that and add that prop to this file? This will help allow the component to be more condensed when used inside of a table. |
||
options: Option[]; | ||
type?: "checkbox" | "radio" | "menuItem"; | ||
btnLabel: string; | ||
startIcon?: IconProps; | ||
endIcon?: IconProps; | ||
amnambiar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
selected: string[] | string | null; | ||
onChange?: (selected: string[] | string | null) => void; | ||
position?: 'left' | 'right'; | ||
} | ||
|
||
export default function Dropdown({ | ||
options, | ||
type = "menuItem", | ||
btnLabel, | ||
startIcon, | ||
endIcon, | ||
position = 'left', | ||
selected, | ||
onChange | ||
}: DropdownProps) { | ||
const [selectedValues, setSelectedValues] = useState<string[] | string | null>(() => { | ||
// Initialize selectedValues based on defaultChecked | ||
if (type === "checkbox") { | ||
// collect all values with defaultChecked: true | ||
return options.filter((option) => option.defaultChecked).map((option) => option.value); | ||
} else if (type === "radio") { | ||
// first option with defaultChecked: true | ||
const defaultOption = options.find((option) => option.defaultChecked); | ||
return defaultOption ? defaultOption.value : null; | ||
} | ||
return null; | ||
}); | ||
|
||
const [open, setOpen] = useState(false); | ||
const openRef = useRef<boolean>(false); | ||
const dropdownRef = useRef<HTMLDivElement>(null); | ||
|
||
// Sync internal state with the `selected` prop when it changes | ||
useEffect(() => { | ||
if (Array.isArray(selected) ? selected.length !== 0 : selected !== null) { | ||
setSelectedValues(selected); | ||
} | ||
}, [selected]); | ||
|
||
// keep ref in sync with state | ||
useEffect(() => { | ||
openRef.current = open; | ||
}, [open]); | ||
|
||
const handleSelect = (option: Option) => { | ||
const value = option.value; | ||
if (type === "checkbox") { | ||
// For checkbox, toggle the value in the array | ||
const newValues = Array.isArray(selectedValues) | ||
? selectedValues.includes(value) | ||
? selectedValues.filter((v) => v !== value) | ||
: [...selectedValues, value] | ||
: [value]; | ||
setSelectedValues(newValues); | ||
onChange?.(newValues); | ||
} else { | ||
// For radio and menuItem, set the single selected value | ||
setSelectedValues(value); | ||
onChange?.(value); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
const listener = (event: MouseEvent) => { | ||
// !openRef.current - act if dropdown is currently open | ||
// ignore clicks inside the dropdown | ||
if (!openRef.current || dropdownRef.current?.contains(event.target as Node)) { | ||
return; | ||
} | ||
|
||
// clicked outside and dropdown open -> close it | ||
setOpen(false); | ||
}; | ||
document.addEventListener("click", listener, true); | ||
return () => { | ||
document.removeEventListener("click", listener, true); | ||
}; | ||
}, []); // attach exactly once | ||
|
||
const handleClearAll = () => { | ||
setSelectedValues([]) | ||
onChange?.([]); | ||
}; | ||
|
||
const allSelected = type === "checkbox" && Array.isArray(selectedValues) && selectedValues.length === options.length; | ||
const noneSelected = !selectedValues || (Array.isArray(selectedValues) && selectedValues.length === 0); | ||
|
||
// --- Button Label Logic --- | ||
const selectedOptionLabel = options.find((o) => | ||
Array.isArray(selectedValues) | ||
? o.value === selectedValues[0] | ||
: o.value === selectedValues | ||
)?.label ?? btnLabel; | ||
|
||
let selectionText: string; | ||
if (type === "checkbox") { | ||
if (allSelected) { | ||
selectionText = 'All'; | ||
} else if (noneSelected) { | ||
selectionText = ''; | ||
} else if (selectedValues.length == 1) { | ||
selectionText = selectedOptionLabel; | ||
} else { | ||
selectionText = `${selectedValues.length} selected`; | ||
} | ||
} else { | ||
selectionText = ((Array.isArray(selectedValues) && selectedValues[0]) || selectedValues) ? selectedOptionLabel : ''; | ||
} | ||
|
||
const buttonLabel = selectionText.length | ||
? `${btnLabel}: ${selectionText}` | ||
: btnLabel; | ||
|
||
return ( | ||
<div className="relative" ref={dropdownRef}> | ||
<Button | ||
variant="primary" | ||
onClick={() => setOpen((prev) => !prev)} | ||
content={buttonLabel} | ||
startIcon={startIcon} | ||
endIcon={endIcon} | ||
/> | ||
|
||
{open && ( | ||
<div | ||
className={`absolute mt-2 w-max bg-containerHigh text-onSurface rounded-md shadow-lg z-1 ${position === 'right' && 'right-0'}`}> | ||
|
||
<div className="max-h-60 overflow-y-auto p-2"> | ||
<ul className="pl-[10px] pr-[10px] flex flex-col gap-2"> | ||
{options.map((option) => | ||
<li key={option.value} className="py-1 flex justify-between" onClick={() => (type === "menuItem") ? handleSelect(option) : null}> | ||
<span className="mr-4"> | ||
{type === 'checkbox' ? ( | ||
<Checkbox | ||
label={option.label} | ||
value={option.value} | ||
checked={selectedValues?.includes(option.value)} | ||
disabled={option.disabled} | ||
onChange={() => handleSelect(option)} | ||
/> | ||
) : ( | ||
type === "radio" ? ( | ||
<RadioButton | ||
label={option.label} | ||
value={option.value} | ||
disabled={option.disabled} | ||
checked={selectedValues === option.value} | ||
onChange={() => handleSelect(option)} | ||
/> | ||
) : option.label | ||
)} | ||
</span> | ||
{option.suffixText && <span className="text-outlineVariant">{option.suffixText}</span>} | ||
</li> | ||
)} | ||
</ul> | ||
|
||
{((type === "checkbox") && (Array.isArray(selectedValues) && selectedValues.length !== 0)) && ( | ||
<> | ||
<hr className="border-outline mt-[15px] mx-0 mb-[5px]" /> | ||
<Button variant="inherit" content="Clear All" onClick={handleClearAll} fullWidth /> | ||
</> | ||
)} | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since floating-ui is no longer being used please make sure it is uninstalled and removed from the package.json file.