diff --git a/packages/frontend/src/components/shared/Select/Select.theme.ts b/packages/frontend/src/components/shared/Select/Select.theme.ts index a0324fd..43d4b0f 100644 --- a/packages/frontend/src/components/shared/Select/Select.theme.ts +++ b/packages/frontend/src/components/shared/Select/Select.theme.ts @@ -34,7 +34,7 @@ export const selectTheme = tv({ ], icon: ['text-elements-mid-em'], helperIcon: [], - helperText: ['flex', 'gap-2', 'items-center', 'text-elements-danger'], + helperText: ['flex', 'gap-2', 'items-center', 'text-elements-low-em'], popover: [ 'z-20', 'absolute', @@ -70,12 +70,8 @@ export const selectTheme = tv({ container: [], }, }, - state: { - default: { - inputWrapper: '', - helperText: ['text-elements-low-em'], - }, - error: { + error: { + true: { inputWrapper: [ 'outline', 'outline-offset-0', @@ -121,6 +117,11 @@ export const selectTheme = tv({ input: ['cursor-pointer'], }, }, + hideValues: { + true: { + input: ['placeholder:text-elements-mid-em'], + }, + }, }, compoundVariants: [ { @@ -143,6 +144,7 @@ export const selectTheme = tv({ variant: 'default', size: 'md', state: 'default', + error: false, isOpen: false, hasValue: false, }, diff --git a/packages/frontend/src/components/shared/Select/Select.tsx b/packages/frontend/src/components/shared/Select/Select.tsx index a8debbb..02e1eab 100644 --- a/packages/frontend/src/components/shared/Select/Select.tsx +++ b/packages/frontend/src/components/shared/Select/Select.tsx @@ -91,6 +91,10 @@ interface SelectProps * The helper text of the select */ helperText?: string; + /** + * Show the values of the select if it's multiple + */ + hideValues?: boolean; } export const Select = ({ @@ -99,7 +103,7 @@ export const Select = ({ searchable = false, clearable, size, - state, + error, orientation = 'horizontal', variant, label, @@ -107,9 +111,10 @@ export const Select = ({ leftIcon, rightIcon, helperText, + hideValues = false, placeholder: placeholderProp = 'Select an option', }: SelectProps) => { - const theme = selectTheme({ size, state, variant, orientation }); + const theme = selectTheme({ size, error, variant, orientation }); const [inputValue, setInputValue] = useState(''); const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null); @@ -273,20 +278,28 @@ export const Select = ({ const renderHelperText = useMemo( () => ( <div className={theme.helperText()}> - {state === 'error' && + {error && cloneIcon(<WarningIcon className={theme.helperIcon()} />, { 'aria-hidden': true, })} <p>{helperText}</p> </div> ), - [cloneIcon, state, theme, helperText], + [cloneIcon, error, theme, helperText], ); const isMultipleHasValue = multiple && selectedItems.length > 0; const isMultipleHasValueButNotSearchable = multiple && !searchable && selectedItems.length > 0; - const placeholder = isMultipleHasValueButNotSearchable ? '' : placeholderProp; + const displayPlaceholder = useMemo(() => { + if (hideValues && isMultipleHasValue) { + return `${selectedItems.length} selected`; + } + if (isMultipleHasValueButNotSearchable) { + return ''; + } + return placeholderProp; + }, [hideValues, multiple, selectedItems.length, placeholderProp]); return ( <div className={theme.container()}> @@ -297,7 +310,7 @@ export const Select = ({ <div ref={inputWrapperRef} className={theme.inputWrapper({ - hasValue: isMultipleHasValue, + hasValue: isMultipleHasValue && !hideValues, })} onClick={() => !dropdownOpen && openMenu()} > @@ -306,6 +319,7 @@ export const Select = ({ {/* Multiple input values */} {isMultipleHasValue && + !hideValues && selectedItems.map((item, index) => ( <SelectValue key={`selected-item-${index}`} @@ -319,15 +333,21 @@ export const Select = ({ {/* Single input value or searchable area */} <input {...getInputProps(getDropdownProps())} - placeholder={placeholder} + placeholder={displayPlaceholder} // Control readOnly based on searchable - readOnly={!searchable} - className={cn(theme.input({ searchable }), { - // Make the input width smaller because we don't need it (not searchable) - 'w-6': isMultipleHasValueButNotSearchable, - // Add margin to the X icon - 'ml-6': isMultipleHasValueButNotSearchable && clearable, - })} + readOnly={!searchable || hideValues} + className={cn( + theme.input({ + searchable, + hideValues: hideValues && selectedItems.length > 0, + }), + { + // Make the input width smaller because we don't need it (not searchable) + 'w-6': isMultipleHasValueButNotSearchable && !hideValues, + // Add margin to the X icon + 'ml-6': isMultipleHasValueButNotSearchable && clearable, + }, + )} /> {/* Right icon */} @@ -339,12 +359,12 @@ export const Select = ({ {/* Popover */} <ul - {...getMenuProps()} + {...getMenuProps({ ref: popoverRef }, { suppressRefError: true })} id="popover" ref={popoverRef} className={cn(theme.popover({ isOpen }), { // Position the popover based on the dropdown position - 'top-[12.5%]': dropdownPosition === 'bottom' && !label, + 'top-[27.5%]': dropdownPosition === 'bottom' && !label, 'top-[35%]': dropdownPosition === 'bottom' && label, 'top-[42.5%]': dropdownPosition === 'bottom' && label && description, 'bottom-[92.5%]': dropdownPosition === 'top' && !label, diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 28f4572..e2cd964 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -31,6 +31,7 @@ import { import { renderDefaultTag, renderMinimalTag } from './renders/tag'; import { renderToast, renderToastsWithCta } from './renders/toast'; import { renderTooltips } from './renders/tooltip'; +import { renderDropdowns } from './renders/dropdown'; type ValuePiece = Date | null; type Value = ValuePiece | [ValuePiece, ValuePiece]; @@ -270,6 +271,14 @@ const Page: React.FC = () => { <div className="w-full h border border-gray-200 px-20 my-10" /> + {/* Dropdown */} + <div className="flex flex-col gap-10 items-center justify-between"> + <h1 className="text-2xl font-bold">Dropdown / Select</h1> + {renderDropdowns()} + </div> + + <div className="w-full h border border-gray-200 px-20 my-10" /> + {/* Inline notification */} <div className="flex flex-col gap-10 items-center justify-between"> <h1 className="text-2xl font-bold">Inline Notification</h1> diff --git a/packages/frontend/src/pages/components/renders/dropdown.tsx b/packages/frontend/src/pages/components/renders/dropdown.tsx new file mode 100644 index 0000000..127cc4f --- /dev/null +++ b/packages/frontend/src/pages/components/renders/dropdown.tsx @@ -0,0 +1,240 @@ +import React from 'react'; +import { PencilIcon } from 'components/shared/CustomIcon'; +import { SelectOption, Select } from 'components/shared/Select'; + +export const DROPDOWN_ITEMS: SelectOption[] = [ + { + value: 'apple', + label: 'Apple', + description: 'Apple is fruit', + leftIcon: <PencilIcon />, + }, + { + value: 'banana', + label: 'Banana', + description: 'Banana is fruit', + leftIcon: <PencilIcon />, + }, + { + value: 'orange', + label: 'Orange', + description: 'Orange is fruit', + leftIcon: <PencilIcon />, + }, + { + value: 'watermelon', + label: 'Watermelon', + description: 'Watermelon is fruit', + disabled: true, + leftIcon: <PencilIcon />, + }, +]; + +export const renderDropdowns = () => ( + <> + <p className="text-sm text-center text-gray-500 -mb-8">Single – Small</p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select size="sm" placeholder="Default" options={DROPDOWN_ITEMS} /> + <Select + size="sm" + placeholder="Clearable" + clearable + options={DROPDOWN_ITEMS} + /> + <Select + size="sm" + searchable + placeholder="Searchable" + options={DROPDOWN_ITEMS} + /> + <Select + size="sm" + placeholder="Vertical" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-8">Single – Medium</p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select placeholder="Default" options={DROPDOWN_ITEMS} /> + <Select placeholder="Clearable" clearable options={DROPDOWN_ITEMS} /> + <Select searchable placeholder="Searchable" options={DROPDOWN_ITEMS} /> + <Select + placeholder="Vertical" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-8">Multiple – Small</p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select + multiple + size="sm" + placeholder="Default" + options={DROPDOWN_ITEMS} + /> + <Select + multiple + size="sm" + placeholder="Clearable" + clearable + options={DROPDOWN_ITEMS} + /> + <Select + searchable + multiple + size="sm" + placeholder="Searchable" + options={DROPDOWN_ITEMS} + /> + <Select + multiple + size="sm" + placeholder="Vertical" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + <Select + multiple + hideValues + size="sm" + orientation="vertical" + placeholder="Hide values" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-8">Multiple – Medium</p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select multiple placeholder="Default" options={DROPDOWN_ITEMS} /> + <Select + multiple + placeholder="Clearable" + clearable + options={DROPDOWN_ITEMS} + /> + <Select + searchable + multiple + placeholder="Searchable" + options={DROPDOWN_ITEMS} + /> + <Select + multiple + placeholder="Vertical" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + <Select + multiple + hideValues + orientation="vertical" + placeholder="Hide values" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-4"> + Single – With label, description, and helper text + </p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select + label="Default" + description="Single select component" + helperText="This is a helper text" + options={DROPDOWN_ITEMS} + /> + <Select + label="Clearable" + description="Single select component" + helperText="This is a helper text" + clearable + options={DROPDOWN_ITEMS} + /> + <Select + searchable + label="Searchable" + description="Single select component" + helperText="This is a helper text" + options={DROPDOWN_ITEMS} + /> + <Select + label="Vertical" + description="Single select component" + helperText="This is a helper text" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-4"> + Multiple – With label, description, and helper text + </p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select + label="Default" + description="Multiple select component" + helperText="This is a helper text" + multiple + options={DROPDOWN_ITEMS} + /> + <Select + label="Clearable" + description="Multiple select component" + helperText="This is a helper text" + multiple + clearable + options={DROPDOWN_ITEMS} + /> + <Select + searchable + label="Searchable" + description="Multiple select component" + helperText="This is a helper text" + multiple + options={DROPDOWN_ITEMS} + /> + <Select + label="Vertical" + description="Multiple select component" + helperText="This is a helper text" + multiple + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + </div> + <p className="text-sm text-center text-gray-500 -mb-4"> + Error – With label, description, and helper text + </p> + <div className="flex gap-4 flex-wrap justify-center"> + <Select + error + label="Default" + description="Multiple select component" + helperText="This is a helper text" + options={DROPDOWN_ITEMS} + /> + <Select + error + label="Clearable" + description="Multiple select component" + helperText="This is a helper text" + clearable + options={DROPDOWN_ITEMS} + /> + <Select + error + searchable + label="Searchable" + description="Multiple select component" + helperText="This is a helper text" + options={DROPDOWN_ITEMS} + /> + <Select + error + label="Vertical" + description="Multiple select component" + helperText="This is a helper text" + orientation="vertical" + options={DROPDOWN_ITEMS} + /> + </div> + </> +);