List components #31
@ -2,17 +2,16 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import TextInput from "../TextInput/TextInput";
|
import TextInput from "../TextInput/TextInput";
|
||||||
import { SearchIcon } from "../Icon/Icon";
|
import { SearchIcon } from "../Icon/Icon";
|
||||||
import SearchResultList, {
|
import SearchResultList from "../SearchResultList/SearchResultList";
|
||||||
type SearchResultOption,
|
|
||||||
} from "../SearchResultList/SearchResultList";
|
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||||
|
|
||||||
export type ComboboxOption = SearchResultOption;
|
|
||||||
|
|
||||||
export type ComboboxSize = "sm" | "md" | "lg";
|
export type ComboboxSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
export interface ComboboxProps {
|
export interface ComboboxProps<T> {
|
||||||
options: ComboboxOption[];
|
options: T[];
|
||||||
|
getOptionValue: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
getOptionSubtitle?: (option: T) => string | undefined;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
label: string;
|
label: string;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
@ -52,18 +51,6 @@ function useScrollIntoView(
|
|||||||
}, [index, refs]);
|
}, [index, refs]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterOptions(
|
|
||||||
options: ComboboxOption[],
|
|
||||||
searchTerm: string,
|
|
||||||
): ComboboxOption[] {
|
|
||||||
const term = searchTerm.toLowerCase();
|
|
||||||
return options.filter(
|
|
||||||
(opt) =>
|
|
||||||
opt.label.toLowerCase().includes(term) ||
|
|
||||||
opt.subtitle?.toLowerCase().includes(term),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextIndex(
|
function getNextIndex(
|
||||||
currentIndex: number,
|
currentIndex: number,
|
||||||
direction: "up" | "down",
|
direction: "up" | "down",
|
||||||
@ -73,31 +60,11 @@ function getNextIndex(
|
|||||||
return Math.max(0, Math.min(next, maxIndex));
|
return Math.max(0, Math.min(next, maxIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default function Combobox<T>({
|
||||||
* Determines what text to display in the input field.
|
|
||||||
* - If user is typing, show the search term
|
|
||||||
* - If multi-select mode, show nothing (selections appear as ListCards below)
|
|
||||||
* - If single-select with a selection, show the selected option's label
|
|
||||||
*/
|
|
||||||
function getDisplayValue(
|
|
||||||
options: ComboboxOption[],
|
|
||||||
selectedValues: string[],
|
|
||||||
multiple: boolean,
|
|
||||||
searchTerm: string,
|
|
||||||
): string {
|
|
||||||
if (searchTerm) return searchTerm;
|
|
||||||
|
|
||||||
if (multiple) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedValues.length === 0) return "";
|
|
||||||
|
|
||||||
return options.find((opt) => opt.value === selectedValues[0])?.label || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Combobox({
|
|
||||||
options,
|
options,
|
||||||
|
getOptionValue,
|
||||||
|
getOptionLabel,
|
||||||
|
getOptionSubtitle,
|
||||||
placeholder = "Search...",
|
placeholder = "Search...",
|
||||||
label,
|
label,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
@ -110,7 +77,7 @@ export default function Combobox({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
}: ComboboxProps) {
|
}: ComboboxProps<T>) {
|
||||||
// Convert value (undefined | string | string[]) to always be an array
|
// Convert value (undefined | string | string[]) to always be an array
|
||||||
const selectedValues: string[] =
|
const selectedValues: string[] =
|
||||||
value === undefined ? [] : Array.isArray(value) ? value : [value];
|
value === undefined ? [] : Array.isArray(value) ? value : [value];
|
||||||
@ -122,23 +89,47 @@ export default function Combobox({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters options by matching search term against label and subtitle.
|
||||||
|
* Skipped when onSearchChange is provided (API handles filtering).
|
||||||
|
*/
|
||||||
|
const filterOptions = (options: T[], searchTerm: string): T[] => {
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
return options.filter((opt) => {
|
||||||
|
const label = getOptionLabel(opt).toLowerCase();
|
||||||
|
const subtitle = getOptionSubtitle?.(opt)?.toLowerCase() || "";
|
||||||
|
return label.includes(term) || subtitle.includes(term);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines what text to display in the input field.
|
||||||
|
* - If user is typing, show the search term
|
||||||
|
* - If multi-select mode, show nothing (selections appear as ListCards below)
|
||||||
|
* - If single-select with a selection, show the selected option's label
|
||||||
|
*/
|
||||||
|
const getDisplayValue = (): string => {
|
||||||
|
if (searchTerm) return searchTerm;
|
||||||
|
if (multiple) return "";
|
||||||
|
if (selectedValues.length === 0) return "";
|
||||||
|
|
||||||
|
const selected = options.find(
|
||||||
|
(opt) => getOptionValue(opt) === selectedValues[0],
|
||||||
|
);
|
||||||
|
return selected ? getOptionLabel(selected) : "";
|
||||||
|
};
|
||||||
|
|
||||||
// Derived state - skip local filtering when onSearchChange is provided (API handles filtering)
|
// Derived state - skip local filtering when onSearchChange is provided (API handles filtering)
|
||||||
const filteredOptions = onSearchChange
|
const filteredOptions = onSearchChange
|
||||||
? options
|
? options
|
||||||
: filterOptions(options, searchTerm);
|
: filterOptions(options, searchTerm);
|
||||||
const displayValue = getDisplayValue(
|
const displayValue = getDisplayValue();
|
||||||
options,
|
|
||||||
selectedValues,
|
|
||||||
multiple,
|
|
||||||
searchTerm,
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeDropdown = () => setIsOpen(false);
|
const closeDropdown = () => setIsOpen(false);
|
||||||
|
|
||||||
useClickOutside(containerRef, closeDropdown);
|
useClickOutside(containerRef, closeDropdown);
|
||||||
useScrollIntoView(focusedIndex, itemRefs);
|
useScrollIntoView(focusedIndex, itemRefs);
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setSearchTerm(value);
|
setSearchTerm(value);
|
||||||
@ -156,16 +147,17 @@ export default function Combobox({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (option: ComboboxOption) => {
|
const handleSelect = (option: T) => {
|
||||||
|
const optionValue = getOptionValue(option);
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
// Toggle selection in multi-select mode
|
// Toggle selection in multi-select mode
|
||||||
const newValues = selectedValues.includes(option.value)
|
const newValues = selectedValues.includes(optionValue)
|
||||||
? selectedValues.filter((v) => v !== option.value)
|
? selectedValues.filter((v) => v !== optionValue)
|
||||||
: [...selectedValues, option.value];
|
: [...selectedValues, optionValue];
|
||||||
onChange?.(newValues);
|
onChange?.(newValues);
|
||||||
} else {
|
} else {
|
||||||
// Replace selection in single-select mode
|
// Replace selection in single-select mode
|
||||||
onChange?.(option.value);
|
onChange?.(optionValue);
|
||||||
}
|
}
|
||||||
setSearchTerm(""); // Clear search so selected label shows
|
setSearchTerm(""); // Clear search so selected label shows
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
@ -240,6 +232,9 @@ export default function Combobox({
|
|||||||
<div className={dropdownWrapperClasses}>
|
<div className={dropdownWrapperClasses}>
|
||||||
<SearchResultList
|
<SearchResultList
|
||||||
options={filteredOptions}
|
options={filteredOptions}
|
||||||
|
getOptionValue={getOptionValue}
|
||||||
|
getOptionLabel={getOptionLabel}
|
||||||
|
getOptionSubtitle={getOptionSubtitle}
|
||||||
selectedValues={selectedValues}
|
selectedValues={selectedValues}
|
||||||
focusedIndex={focusedIndex}
|
focusedIndex={focusedIndex}
|
||||||
maxHeight={dropdownHeight}
|
maxHeight={dropdownHeight}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import type { HTMLAttributes } from "react";
|
import type { HTMLAttributes } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Combobox, {
|
import Combobox, { type ComboboxSize } from "../Combobox/Combobox";
|
||||||
type ComboboxOption,
|
|
||||||
type ComboboxSize,
|
|
||||||
} from "../Combobox/Combobox";
|
|
||||||
import ListCard from "../ListCard/ListCard";
|
import ListCard from "../ListCard/ListCard";
|
||||||
|
|
||||||
export interface ParticipantPickerProps
|
export interface ParticipantPickerProps<T>
|
||||||
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||||
options: ComboboxOption[];
|
options: T[];
|
||||||
|
getOptionValue: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
getOptionSubtitle?: (option: T) => string | undefined;
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -28,8 +28,11 @@ const widthClasses: Record<ComboboxSize, string> = {
|
|||||||
lg: "w-(--text-input-default-width-lg)",
|
lg: "w-(--text-input-default-width-lg)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ParticipantPicker({
|
export default function ParticipantPicker<T>({
|
||||||
options,
|
options,
|
||||||
|
getOptionValue,
|
||||||
|
getOptionLabel,
|
||||||
|
getOptionSubtitle,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = "Sök...",
|
placeholder = "Sök...",
|
||||||
@ -43,12 +46,14 @@ export default function ParticipantPicker({
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}: ParticipantPickerProps) {
|
}: ParticipantPickerProps<T>) {
|
||||||
const handleRemove = (valueToRemove: string) => {
|
const handleRemove = (valueToRemove: string) => {
|
||||||
onChange(value.filter((v) => v !== valueToRemove));
|
onChange(value.filter((v) => v !== valueToRemove));
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedOptions = options.filter((opt) => value.includes(opt.value));
|
const selectedOptions = options.filter((opt) =>
|
||||||
|
value.includes(getOptionValue(opt)),
|
||||||
|
);
|
||||||
|
|
||||||
const containerClasses = clsx(
|
const containerClasses = clsx(
|
||||||
"flex flex-col gap-(--spacing-sm)",
|
"flex flex-col gap-(--spacing-sm)",
|
||||||
@ -62,6 +67,9 @@ export default function ParticipantPicker({
|
|||||||
<div className={containerClasses} style={widthStyle} {...props}>
|
<div className={containerClasses} style={widthStyle} {...props}>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={options}
|
options={options}
|
||||||
|
getOptionValue={getOptionValue}
|
||||||
|
getOptionLabel={getOptionLabel}
|
||||||
|
getOptionSubtitle={getOptionSubtitle}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(v) => onChange(v as string[])}
|
onChange={(v) => onChange(v as string[])}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@ -77,10 +85,10 @@ export default function ParticipantPicker({
|
|||||||
<div className="flex flex-col gap-(--spacing-sm)">
|
<div className="flex flex-col gap-(--spacing-sm)">
|
||||||
{selectedOptions.map((option) => (
|
{selectedOptions.map((option) => (
|
||||||
<ListCard
|
<ListCard
|
||||||
key={option.value}
|
key={getOptionValue(option)}
|
||||||
title={option.label}
|
title={getOptionLabel(option)}
|
||||||
subtitle={option.subtitle}
|
subtitle={getOptionSubtitle?.(option)}
|
||||||
onRemove={() => handleRemove(option.value)}
|
onRemove={() => handleRemove(getOptionValue(option))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,20 +2,17 @@ import type { HTMLAttributes } from "react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import ListItem from "../ListItem/ListItem";
|
import ListItem from "../ListItem/ListItem";
|
||||||
|
|
||||||
export interface SearchResultOption {
|
export interface SearchResultListProps<T>
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
subtitle?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResultListProps
|
|
||||||
extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
||||||
|
ansv7779 marked this conversation as resolved
|
|||||||
options: SearchResultOption[];
|
options: T[];
|
||||||
|
getOptionValue: (option: T) => string;
|
||||||
|
getOptionLabel: (option: T) => string;
|
||||||
|
getOptionSubtitle?: (option: T) => string | undefined;
|
||||||
selectedValues?: string[];
|
selectedValues?: string[];
|
||||||
focusedIndex?: number;
|
focusedIndex?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
noResultsText?: string;
|
noResultsText?: string;
|
||||||
onSelect?: (option: SearchResultOption) => void;
|
onSelect?: (option: T) => void;
|
||||||
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
|
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,8 +28,11 @@ const noResultsClasses =
|
|||||||
const dividerClasses =
|
const dividerClasses =
|
||||||
"border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]";
|
"border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]";
|
||||||
|
|
||||||
export default function SearchResultList({
|
export default function SearchResultList<T>({
|
||||||
options,
|
options,
|
||||||
|
getOptionValue,
|
||||||
|
getOptionLabel,
|
||||||
|
getOptionSubtitle,
|
||||||
selectedValues = [],
|
selectedValues = [],
|
||||||
focusedIndex = -1,
|
focusedIndex = -1,
|
||||||
maxHeight = 300,
|
maxHeight = 300,
|
||||||
@ -42,8 +42,9 @@ export default function SearchResultList({
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}: SearchResultListProps) {
|
}: SearchResultListProps<T>) {
|
||||||
const isSelected = (value: string) => selectedValues.includes(value);
|
const isSelected = (option: T) =>
|
||||||
|
selectedValues.includes(getOptionValue(option));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -55,7 +56,7 @@ export default function SearchResultList({
|
|||||||
{options.length > 0 ? (
|
{options.length > 0 ? (
|
||||||
options.map((option, index) => (
|
options.map((option, index) => (
|
||||||
<div
|
<div
|
||||||
key={option.value}
|
key={getOptionValue(option)}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (itemRefs?.current) {
|
if (itemRefs?.current) {
|
||||||
itemRefs.current[index] = el;
|
itemRefs.current[index] = el;
|
||||||
@ -64,9 +65,9 @@ export default function SearchResultList({
|
|||||||
className={index > 0 ? dividerClasses : undefined}
|
className={index > 0 ? dividerClasses : undefined}
|
||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={option.label}
|
title={getOptionLabel(option)}
|
||||||
subtitle={option.subtitle}
|
subtitle={getOptionSubtitle?.(option)}
|
||||||
selected={isSelected(option.value)}
|
selected={isSelected(option)}
|
||||||
focused={index === focusedIndex}
|
focused={index === focusedIndex}
|
||||||
onClick={() => onSelect?.(option)}
|
onClick={() => onSelect?.(option)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,7 +8,13 @@ import Combobox from "../components/Combobox/Combobox";
|
|||||||
import ListCard from "../components/ListCard/ListCard";
|
import ListCard from "../components/ListCard/ListCard";
|
||||||
import ParticipantPicker from "../components/ParticipantPicker/ParticipantPicker";
|
import ParticipantPicker from "../components/ParticipantPicker/ParticipantPicker";
|
||||||
|
|
||||||
const peopleOptions = [
|
interface Person {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
subtitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peopleOptions: Person[] = [
|
||||||
{ value: "1", label: "Lennart Johansson", subtitle: "lejo1891" },
|
{ value: "1", label: "Lennart Johansson", subtitle: "lejo1891" },
|
||||||
{ value: "2", label: "Mats Rubarth", subtitle: "matsrub1891" },
|
{ value: "2", label: "Mats Rubarth", subtitle: "matsrub1891" },
|
||||||
{ value: "3", label: "Daniel Tjernström", subtitle: "datj1891" },
|
{ value: "3", label: "Daniel Tjernström", subtitle: "datj1891" },
|
||||||
@ -17,6 +23,10 @@ const peopleOptions = [
|
|||||||
{ value: "6", label: "Kurre Hamrin", subtitle: "kuha1891" },
|
{ value: "6", label: "Kurre Hamrin", subtitle: "kuha1891" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getPersonValue = (person: Person) => person.value;
|
||||||
|
const getPersonLabel = (person: Person) => person.label;
|
||||||
|
const getPersonSubtitle = (person: Person) => person.subtitle;
|
||||||
|
|
||||||
export default function ComponentLibrary() {
|
export default function ComponentLibrary() {
|
||||||
const [darkMode, setDarkMode] = useState(() => {
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
return document.documentElement.classList.contains("dark");
|
return document.documentElement.classList.contains("dark");
|
||||||
@ -206,6 +216,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="max-w-96">
|
<div className="max-w-96">
|
||||||
<SearchResultList
|
<SearchResultList
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
selectedValues={["3"]}
|
selectedValues={["3"]}
|
||||||
focusedIndex={1}
|
focusedIndex={1}
|
||||||
noResultsText="Inga resultat"
|
noResultsText="Inga resultat"
|
||||||
@ -216,7 +229,12 @@ export default function ComponentLibrary() {
|
|||||||
<section className="mt-lg">
|
<section className="mt-lg">
|
||||||
<h2 className="mb-md">SearchResultList - Empty</h2>
|
<h2 className="mb-md">SearchResultList - Empty</h2>
|
||||||
<div className="max-w-96">
|
<div className="max-w-96">
|
||||||
<SearchResultList options={[]} noResultsText="Inga resultat" />
|
<SearchResultList
|
||||||
|
options={[]}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
noResultsText="Inga resultat"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -225,6 +243,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-col gap-md">
|
<div className="flex flex-col gap-md">
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={selectedPerson}
|
value={selectedPerson}
|
||||||
onChange={(v) => setSelectedPerson(v as string)}
|
onChange={(v) => setSelectedPerson(v as string)}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
@ -244,6 +265,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-col gap-md">
|
<div className="flex flex-col gap-md">
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={selectedPeople}
|
value={selectedPeople}
|
||||||
onChange={(v) => setSelectedPeople(v as string[])}
|
onChange={(v) => setSelectedPeople(v as string[])}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
@ -266,6 +290,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-wrap items-start gap-md">
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
placeholder="Small"
|
placeholder="Small"
|
||||||
size="sm"
|
size="sm"
|
||||||
label="Small"
|
label="Small"
|
||||||
@ -273,6 +300,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
placeholder="Medium"
|
placeholder="Medium"
|
||||||
size="md"
|
size="md"
|
||||||
label="Medium"
|
label="Medium"
|
||||||
@ -280,6 +310,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
placeholder="Large"
|
placeholder="Large"
|
||||||
size="lg"
|
size="lg"
|
||||||
label="Large"
|
label="Large"
|
||||||
@ -293,6 +326,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-col gap-md">
|
<div className="flex flex-col gap-md">
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
customWidth="350px"
|
customWidth="350px"
|
||||||
label="Custom width"
|
label="Custom width"
|
||||||
@ -300,6 +336,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
label="Full width"
|
label="Full width"
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -320,6 +359,9 @@ export default function ComponentLibrary() {
|
|||||||
<h2 className="mb-md">ParticipantPicker</h2>
|
<h2 className="mb-md">ParticipantPicker</h2>
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Sök deltagare..."
|
placeholder="Sök deltagare..."
|
||||||
@ -332,6 +374,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-wrap items-start gap-md">
|
<div className="flex flex-wrap items-start gap-md">
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Small"
|
placeholder="Small"
|
||||||
@ -341,6 +386,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Medium"
|
placeholder="Medium"
|
||||||
@ -350,6 +398,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Large"
|
placeholder="Large"
|
||||||
@ -365,6 +416,9 @@ export default function ComponentLibrary() {
|
|||||||
<div className="flex flex-col gap-md">
|
<div className="flex flex-col gap-md">
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
@ -374,6 +428,9 @@ export default function ComponentLibrary() {
|
|||||||
/>
|
/>
|
||||||
<ParticipantPicker
|
<ParticipantPicker
|
||||||
options={peopleOptions}
|
options={peopleOptions}
|
||||||
|
getOptionValue={getPersonValue}
|
||||||
|
getOptionLabel={getPersonLabel}
|
||||||
|
getOptionSubtitle={getPersonSubtitle}
|
||||||
value={participants}
|
value={participants}
|
||||||
onChange={setParticipants}
|
onChange={setParticipants}
|
||||||
placeholder="Sök..."
|
placeholder="Sök..."
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user
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 theCombobox/this component.