diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c81176a3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "CHA-react-FE-template", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/starter_app/src/app/components/Button.tsx b/starter_app/src/app/components/Button.tsx index ca489128..22a8cdd6 100644 --- a/starter_app/src/app/components/Button.tsx +++ b/starter_app/src/app/components/Button.tsx @@ -32,7 +32,7 @@ const buttonVariants = cva('inline-flex items-center justify-center gap-2 whites text: 'bg-transparent text-primary hover:bg-primary/10 active:bg-primary/15', icon: 'text-sm bg-primary/10 hover:bg-primary/15 active:bg-primary/20', embedded: 'text-sm', - inherit: 'bg-inherit text-inherit', + inherit: 'bg-inherit text-inherit' }, shape: { pill: 'rounded-full', diff --git a/starter_app/src/app/components/Dropdown.tsx b/starter_app/src/app/components/Dropdown.tsx new file mode 100644 index 00000000..7ab447a8 --- /dev/null +++ b/starter_app/src/app/components/Dropdown.tsx @@ -0,0 +1,173 @@ +'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 { + options: Option[]; + type?: "checkbox" | "radio" | "menuItem"; + btnLabel: string; + startIcon?: IconProps; + endIcon?: IconProps; + position?: 'left' | 'right'; + size?: 'small' | 'medium'; + selected?: string[] | string | null; + onChange?: (selected: string[] | string | null) => void; +} + +const Dropdown = ({ + options, + type = "menuItem", + btnLabel, + startIcon, + endIcon, + position = 'left', + size = 'small', + selected = null, + onChange +}: DropdownProps) => { + const [open, setOpen] = useState(false); + const openRef = useRef(false); + const dropdownRef = useRef(null); + + // 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(selected) + ? selected.includes(value) + ? selected.filter((v) => v !== value) + : [...selected, value] + : [value]; + onChange?.(newValues); + } else { + // For radio and menuItem, set the single selected 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 = () => { + onChange?.([]); + }; + + const allSelected = type === "checkbox" && Array.isArray(selected) && selected.length === options.length; + const noneSelected = !selected || (Array.isArray(selected) && selected.length === 0); + + // --- Button Label Logic --- + const selectedOptionLabel = options.find((o) => + Array.isArray(selected) + ? o.value === selected[0] + : o.value === selected + )?.label ?? btnLabel; + + let selectionText: string; + if (type === "checkbox") { + if (allSelected) { + selectionText = 'All'; + } else if (noneSelected) { + selectionText = ''; + } else if (selected.length == 1) { + selectionText = selectedOptionLabel; + } else { + selectionText = `${selected.length} selected`; + } + } else { + selectionText = ((Array.isArray(selected) && selected[0]) || selected) ? selectedOptionLabel : ''; + } + + const buttonLabel = selectionText.length + ? `${btnLabel}: ${selectionText}` + : btnLabel; + + return ( +
+
+ + )} + + ); +} + +export default Dropdown; \ No newline at end of file diff --git a/starter_app/src/app/globals.css b/starter_app/src/app/globals.css index f18cb4be..35cba655 100644 --- a/starter_app/src/app/globals.css +++ b/starter_app/src/app/globals.css @@ -53,6 +53,7 @@ input[type=number]::-webkit-inner-spin-button:hover { --h4-height: 36px; --h5-height: 32px; --h6-height: 28px; + } @theme inline { diff --git a/starter_app/src/app/page.tsx b/starter_app/src/app/page.tsx index f3f03338..e832230b 100644 --- a/starter_app/src/app/page.tsx +++ b/starter_app/src/app/page.tsx @@ -9,25 +9,47 @@ import UncontrolledTextField from './components/UncontrolledTextfield'; import Chip from './components/Chip'; import SearchBar from './components/SearchBox'; import Tabs from './components/Tabs'; -import Dropdown from './components/DropdownMenu'; +import Dropdown from './components/Dropdown'; + import { BoltIcon as Bolt } from '@heroicons/react/24/solid'; import { CheckCircleIcon as OutlineCheck } from '@heroicons/react/24/outline'; import { FunnelIcon as Filter } from '@heroicons/react/24/solid'; import { ArrowsUpDownIcon as Sort } from '@heroicons/react/24/solid'; -const dropdownOptions = [ - { itemLabel: 'Option 1', value: 'option1', suffixText: '34' }, - { itemLabel: 'Option 2', value: 'option2', suffixText: '34' }, - { itemLabel: 'Option 3', value: 'option3', suffixText: '34' }, - ] +const sortDropdownOptions = [ + { label: 'Option 1', value: 'option1', suffixText: '34', defaultChecked: true }, + { label: 'Option 2', value: 'option2', suffixText: '34' }, + { label: 'Option 3', value: 'option3', suffixText: '34' }, +] + +const filterDropdownOptions = [ + { label: 'Option 1', value: 'option1', suffixText: '34', defaultChecked: true }, + { label: 'Option 2', value: 'option2', suffixText: '34' }, + { label: 'Option 3', value: 'option3', suffixText: '34', defaultChecked: true }, +] + +const menuDropdownOptions = [ + { label: 'Option 1', value: 'option1'}, + { label: 'Option 2', value: 'option2'}, + { label: 'Option 3', value: 'option3'}, + { label: 'Option 4', value: 'option4'} +] export default function Home() { + + const [sortValue, setSortValue] = useState(sortDropdownOptions.filter(opt => opt.defaultChecked).map(opt => opt.value)[0]); + const [filterValues, setFilterValues] = useState(filterDropdownOptions.filter(opt => opt.defaultChecked).map(opt => opt.value)); + const [settingsMenuValue, setSettingsMenuValue] = useState(null); + + const handleFilterChange = (val: string[] | string | null) => { setFilterValues(val as string[]) } + const handleSortChange = (val: string[] | string | null) => { setSortValue(val as string | null) } + const handleSettingsMenuChange = (val: string[] | string | null) => { setSettingsMenuValue(val as string | null) } + const [checkValues, setCheckValues] = useState({smallCheck: true, mediumCheck: false, disabledCheck: false}); const [radioValues, setRadioValue] = useState({smallRadio: '1', mediumRadio: '3'}); const [tabValues, setTabValues] = useState({defaultTab: 'Tab 1', iconTab: 'Tab 2'}); - const [sortValue, setSortValue] = useState('option1'); - const [filterValues, setFilterValues] = useState(['option2']); + return (
@@ -41,9 +63,11 @@ export default function Home() {
}, ]} /> + }} - listItems={dropdownOptions} - type='checkbox' + startIcon={{ svg: }} + options={filterDropdownOptions} + type="checkbox" selected={filterValues} - onChange={(val) => setFilterValues(val as string[])} - /> - + + }} listItems={dropdownOptions} - type='radio' + startIcon={{ svg: }} + options={sortDropdownOptions} + type="radio" selected={sortValue} - onChange={(val) => setSortValue(val as string)} - /> + onChange={handleSortChange} + /> + + }} + options={menuDropdownOptions} + type="menuItem" + selected={settingsMenuValue} + onChange={handleSettingsMenuChange} + />