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 {
value: string;
label: string;
subtitle?: 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;
subtitle?: string;
}
export interface SearchResultListProps {
options: SearchResultOption[];
selectedValues?: string[];
focusedIndex?: number;
maxHeight?: number;
noResultsText?: string;
onSelect?: (option: SearchResultOption) => void;
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
export interface SearchResultListProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
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 baseClasses = clsx(
"w-full bg-base-canvas",
"border border-base-ink-medium rounded-(--border-radius-md)",
"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({
options,
selectedValues = [],
focusedIndex = -1,
maxHeight = 300,
noResultsText = 'No results found',
onSelect,
itemRefs,
options,
selectedValues = [],
focusedIndex = -1,
maxHeight = 300,
noResultsText = "No results found",
onSelect,
itemRefs,
className,
style,
...props
}: SearchResultListProps) {
const isSelected = (value: string) => selectedValues.includes(value);
const isSelected = (value: string) => selectedValues.includes(value);
return (
<div className={containerClasses} style={{ maxHeight }} onMouseDown={(e) => e.preventDefault()}>
{options.length > 0 ? (
options.map((option, index) => (
<div
key={option.value}
ref={(el) => {
if (itemRefs?.current) {
itemRefs.current[index] = el;
}
}}
className={
index > 0 ? 'border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]' : ''
}
>
<ListItem
title={option.label}
subtitle={option.subtitle}
selected={isSelected(option.value)}
focused={index === focusedIndex}
onClick={() => onSelect?.(option)}
/>
</div>
))
) : (
<div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
);
return (
<div
className={clsx(baseClasses, className)}
style={{ maxHeight, ...style }}
onMouseDown={(e) => e.preventDefault()}
{...props}
>
{options.length > 0 ? (
options.map((option, index) => (
<div
key={option.value}
ref={(el) => {
if (itemRefs?.current) {
itemRefs.current[index] = el;
}
}}
className={index > 0 ? dividerClasses : undefined}
>
<ListItem
title={option.label}
subtitle={option.subtitle}
selected={isSelected(option.value)}
focused={index === focusedIndex}
onClick={() => onSelect?.(option)}
/>
</div>
))
) : (
<div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
);
}