Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
60 changes: 60 additions & 0 deletions starter_app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions starter_app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@floating-ui/react": "^0.27.16",
Copy link
Collaborator

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.

"@heroicons/react": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion starter_app/src/app/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
191 changes: 191 additions & 0 deletions starter_app/src/app/components/Dropdown.tsx
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
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>
);
}
1 change: 1 addition & 0 deletions starter_app/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ input[type=number]::-webkit-inner-spin-button:hover {
--h4-height: 36px;
--h5-height: 32px;
--h6-height: 28px;

}

@theme inline {
Expand Down
Loading