List components #31

Merged
stne3960 merged 68 commits from list_item into main 2025-12-18 12:41:13 +01:00
Showing only changes of commit 8ccf57eb16 - Show all commits

View File

@ -1,66 +1,80 @@
import ListItem from '../ListItem/ListItem'; import type { HTMLAttributes } from "react";
import clsx from "clsx";
import ListItem from "../ListItem/ListItem";
export interface SearchResultOption { export interface SearchResultOption {
value: string; value: string;
ansv7779 marked this conversation as resolved
Review

This is limiting. Often times you want to select some more complex object as you do in your examples in ComponentLibrary. Forcing all users of the component to do their own lookup when it should be handled by the Combobox/this component.

This is limiting. Often times you want to select some more complex object as you do in your examples in `ComponentLibrary`. Forcing all users of the component to do their own lookup when it should be handled by the `Combobox`/this component.
label: string; label: string;
subtitle?: string; subtitle?: string;
} }
export interface SearchResultListProps { export interface SearchResultListProps
options: SearchResultOption[]; extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
selectedValues?: string[]; options: SearchResultOption[];
focusedIndex?: number; selectedValues?: string[];
maxHeight?: number; focusedIndex?: number;
noResultsText?: string; maxHeight?: number;
onSelect?: (option: SearchResultOption) => void; noResultsText?: string;
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>; onSelect?: (option: SearchResultOption) => void;
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
} }
const containerClasses = [ const baseClasses = clsx(
'w-full bg-base-canvas border border-base-ink-medium rounded-(--border-radius-md)', "w-full bg-base-canvas",
'overflow-y-auto', "border border-base-ink-medium rounded-(--border-radius-md)",
].join(' '); "overflow-y-auto",
);
const noResultsClasses = 'px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center'; const noResultsClasses =
"px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center";
const dividerClasses =
"border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]";
export default function SearchResultList({ export default function SearchResultList({
options, options,
selectedValues = [], selectedValues = [],
focusedIndex = -1, focusedIndex = -1,
maxHeight = 300, maxHeight = 300,
noResultsText = 'No results found', noResultsText = "No results found",
onSelect, onSelect,
itemRefs, itemRefs,
className,
style,
...props
}: SearchResultListProps) { }: SearchResultListProps) {
const isSelected = (value: string) => selectedValues.includes(value); const isSelected = (value: string) => selectedValues.includes(value);
return ( return (
<div className={containerClasses} style={{ maxHeight }} onMouseDown={(e) => e.preventDefault()}> <div
{options.length > 0 ? ( className={clsx(baseClasses, className)}
options.map((option, index) => ( style={{ maxHeight, ...style }}
<div onMouseDown={(e) => e.preventDefault()}
key={option.value} {...props}
ref={(el) => { >
if (itemRefs?.current) { {options.length > 0 ? (
itemRefs.current[index] = el; options.map((option, index) => (
} <div
}} key={option.value}
className={ ref={(el) => {
index > 0 ? 'border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]' : '' if (itemRefs?.current) {
} itemRefs.current[index] = el;
> }
<ListItem }}
title={option.label} className={index > 0 ? dividerClasses : undefined}
subtitle={option.subtitle} >
selected={isSelected(option.value)} <ListItem
focused={index === focusedIndex} title={option.label}
onClick={() => onSelect?.(option)} subtitle={option.subtitle}
/> selected={isSelected(option.value)}
</div> focused={index === focusedIndex}
)) onClick={() => onSelect?.(option)}
) : ( />
<div className={noResultsClasses}>{noResultsText}</div> </div>
)} ))
</div> ) : (
); <div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
);
} }