List components #31

Open
stne3960 wants to merge 57 commits from list_item into main
2 changed files with 77 additions and 39 deletions
Showing only changes of commit be9b621737 - Show all commits

View File

@ -1,12 +1,8 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import TextInput from '../TextInput/TextInput';
import ListItem from '../ListItem/ListItem';
import SearchResultList, { type SearchResultOption } from '../SearchResultList/SearchResultList';
export interface ComboboxOption {
value: string;
label: string;
subtitle?: string;
}
export type ComboboxOption = SearchResultOption;
export type ComboboxSize = 'sm' | 'md' | 'lg';
@ -30,13 +26,7 @@ const widthClasses: Record<ComboboxSize, string> = {
lg: 'w-(--text-input-default-width-lg)',
};
const dropdownClasses = [
'absolute top-full left-0 z-50 w-full',
'bg-base-canvas border border-base-ink-medium rounded-(--border-radius-md)',
'overflow-y-auto mt-(--spacing-sm)',
].join(' ');
const noResultsClasses = 'px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center';
const dropdownWrapperClasses = 'absolute top-full left-0 z-50 w-full mt-(--spacing-sm)';
function SearchIcon({ style }: { style?: React.CSSProperties }) {
return (
@ -102,10 +92,6 @@ function findNextSelectableIndex(
return currentIndex;
}
function isSelected(value: string, selectedValues: string[]): boolean {
return selectedValues.includes(value);
}
function getDisplayValue(
options: ComboboxOption[],
selectedValues: string[],
@ -179,7 +165,7 @@ export default function Combobox({
const handleSelect = (option: ComboboxOption) => {
if (multiple) {
// Toggle selection in multi-select mode
const newValues = isSelected(option.value, selectedValues)
const newValues = selectedValues.includes(option.value)
? selectedValues.filter((v) => v !== option.value)
: [...selectedValues, option.value];
onChange?.(newValues);
@ -245,28 +231,17 @@ export default function Combobox({
/>
{isOpen && (
<div className={dropdownClasses} style={{ maxHeight: dropdownHeight }}>
{filteredOptions.length > 0 ? (
filteredOptions.map((option, index) => (
<div
key={option.value}
ref={(el) => {
itemRefs.current[index] = el;
}}
>
<ListItem
title={option.label}
subtitle={option.subtitle}
selected={isSelected(option.value, selectedValues)}
focused={index === focusedIndex}
onClick={() => handleSelect(option)}
<div className={dropdownWrapperClasses}>
<SearchResultList
options={filteredOptions}
selectedValues={selectedValues}
focusedIndex={focusedIndex}
maxHeight={dropdownHeight}
noResultsText={noResultsText}
onSelect={handleSelect}
itemRefs={itemRefs}
/>
</div>
))
) : (
<div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
)}
</div>
);

View File

@ -0,0 +1,63 @@
import ListItem from '../ListItem/ListItem';
export interface SearchResultOption {
value: string;
label: string;
subtitle?: string;
}
export interface SearchResultListProps {
options: SearchResultOption[];
selectedValues?: string[];
focusedIndex?: number;
maxHeight?: number;
noResultsText?: string;
onSelect?: (option: SearchResultOption) => void;
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
}
const containerClasses = [
'w-full bg-base-canvas border border-base-ink-medium rounded-(--border-radius-md)',
'overflow-y-auto',
].join(' ');
const noResultsClasses = 'px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center';
export default function SearchResultList({
options,
selectedValues = [],
focusedIndex = -1,
maxHeight = 300,
noResultsText = 'No results found',
onSelect,
itemRefs,
}: SearchResultListProps) {
const isSelected = (value: string) => selectedValues.includes(value);
return (
<div className={containerClasses} style={{ maxHeight }}>
{options.length > 0 ? (
options.map((option, index) => (
<div
key={option.value}
ref={(el) => {
if (itemRefs?.current) {
itemRefs.current[index] = el;
}
}}
>
<ListItem
title={option.label}
subtitle={option.subtitle}
selected={isSelected(option.value)}
focused={index === focusedIndex}
onClick={() => onSelect?.(option)}
/>
</div>
))
) : (
<div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
);
}