Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/news/7344.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Multiselect widget seven @dobri1408
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ export default {
control: 'boolean',
description: 'Whether the user can create new options not in the list.',
},
isDisabled: {
control: 'boolean',
description: 'Whether the field is disabled.',
},
isRequired: {
control: 'boolean',
description: 'Whether the field is required.',
},
errorMessage: {
control: 'text',
description: 'Error message to display below the field.',
},
},
};

Expand Down Expand Up @@ -92,3 +104,87 @@ NonCreatable.args = {
creatable: false,
label: 'Non-Creatable Fruits',
};

export const Disabled = Default.bind({});

Disabled.args = {
...Default.args,
isDisabled: true,
label: 'Disabled Fruits',
};

export const Required = Default.bind({});

Required.args = {
...Default.args,
isRequired: true,
label: 'Required Fruits',
description: 'This field is required',
};

export const WithError = (args: MultiSelectProps<Option>) => {
const selectedItems = useListData({
initialItems: [],
});

return (
<div className="w-full max-w-[400px]">
<MultiSelect {...args} selectedItems={selectedItems} />
</div>
);
};

WithError.args = {
...Default.args,
errorMessage: 'Please select at least one fruit',
label: 'Fruits with Error',
};

export const Controlled = (args: MultiSelectProps<Option>) => {
const selectedItems = useListData({
initialItems: [items[1]],
});

const handleChange = (newSelectedItems: Option[]) => {
// onChange este apelat cu starea actualizată
console.log('onChange called with:', newSelectedItems);
};

return (
<div className="w-full max-w-[400px] space-y-4">
<MultiSelect
{...args}
selectedItems={selectedItems}
onChange={handleChange}
/>
<div className="text-sm">
<strong>Currently Selected:</strong>
<div className="mt-2 rounded border bg-gray-50 p-2">
{selectedItems.items.length === 0 ? (
<div className="text-gray-500">Nothing selected</div>
) : (
<div>
{selectedItems.items.map((item) => (
<span
key={item.id}
className="mr-1 mb-1 inline-block rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
>
{item.name}
</span>
))}
<div className="mt-1 text-xs text-gray-600">
Total: {selectedItems.items.length} items
</div>
</div>
)}
</div>
</div>
</div>
);
};

Controlled.args = {
...Default.args,
label: 'Controlled Fruits',
description: 'This component demonstrates onChange callback',
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TagGroup,
TagList,
Tag,
type ValidationResult,
} from 'react-aria-components';
import { Description, Label } from '../Field/Field.quanta';
import { useListData } from 'react-stately';
Expand All @@ -32,9 +33,15 @@ export interface MultiSelectProps<T extends object> {
placeholder?: string;
label?: string;
description?: string;
errorMessage?: string | ((validation: ValidationResult) => string);
creatable?: boolean;
onChange?: (selectedItems: Array<T>) => void;
onBlur?: () => void;
onFocus?: () => void;
onItemInserted?: (key: Key) => void;
onItemCleared?: (key: Key) => void;
isDisabled?: boolean;
isRequired?: boolean;
renderEmptyState?: (inputValue: string) => React.ReactNode;
}

Expand All @@ -44,11 +51,17 @@ export function MultiSelect({
items,
className = '',
description,
errorMessage,
onChange,
onBlur,
onFocus,
onItemCleared,
onItemInserted,
renderEmptyState,
selectedItems,
creatable = true,
isDisabled = false,
isRequired = false,
...props
}: MultiSelectProps<Option>) {
const triggerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -80,20 +93,21 @@ export function MultiSelect({
const onRemove = useCallback(
(keys: Set<Key>) => {
const key = keys.values().next().value;
if (key) {
if (key && !isDisabled) {
selectedItems.remove(key);
setFieldState({
inputValue: '',
selectedKey: null,
});
onItemCleared?.(key);
onChange?.(selectedItems.items);
}
},
[selectedItems, onItemCleared],
[selectedItems, onItemCleared, onChange, isDisabled],
);

const onSelectionChange = (id: Key | null) => {
if (!id) {
if (!id || isDisabled) {
return;
}

Expand All @@ -110,6 +124,8 @@ export function MultiSelect({
selectedKey: id,
});
onItemInserted?.(id);
// Call onChange with the updated array
setTimeout(() => onChange?.(selectedItems.items), 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

}

accessibleList.setFilterText('');
Expand Down Expand Up @@ -140,6 +156,8 @@ export function MultiSelect({
}, []);

const onCreateTag = useCallback(() => {
if (isDisabled) return;

const inputValue = fieldState.inputValue.trim();
const id = inputValue.toLocaleLowerCase();

Expand All @@ -159,11 +177,19 @@ export function MultiSelect({

accessibleList.append(item);
selectedItems.append(item);
onChange?.(selectedItems.items);
}
}, [fieldState.inputValue, accessibleList, selectedItems, setFieldState]);
}, [
fieldState.inputValue,
accessibleList,
selectedItems,
setFieldState,
onChange,
isDisabled,
]);

const popLast = useCallback(() => {
if (selectedItems.items.length === 0) {
if (selectedItems.items.length === 0 || isDisabled) {
return;
}

Expand All @@ -172,13 +198,14 @@ export function MultiSelect({
if (endKey) {
selectedItems.remove(endKey.id);
onItemCleared?.(endKey.id);
onChange?.(selectedItems.items);
}

setFieldState({
inputValue: '',
selectedKey: null,
});
}, [selectedItems, onItemCleared]);
}, [selectedItems, onItemCleared, onChange, isDisabled]);

const onKeyDownCapture = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -202,7 +229,14 @@ export function MultiSelect({
<div className={twMerge('w-full', className)}>
{label && <Label>{label}</Label>}
<div ref={triggerRef} className="relative">
<div className="relative flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 shadow-sm focus-within:border-gray-500 focus-within:ring-2 focus-within:ring-gray-400">
<div
className={twMerge(
'relative flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border bg-white px-3 py-1.5 shadow-sm focus-within:ring-2',
errorMessage
? 'border-red-500 focus-within:border-red-500 focus-within:ring-red-400'
: 'border-gray-300 focus-within:border-gray-500 focus-within:ring-gray-400',
)}
>
<TagGroup
id={tagGroupIdentifier}
aria-label="Selected items"
Expand Down Expand Up @@ -243,13 +277,17 @@ export function MultiSelect({
>
<Input
placeholder={placeholder}
disabled={isDisabled}
required={isRequired}
onBlur={() => {
setFieldState({
inputValue: '',
selectedKey: null,
});
accessibleList.setFilterText('');
onBlur?.();
}}
onFocus={onFocus}
onKeyDownCapture={onKeyDownCapture}
className="min-w-0 flex-1 bg-white px-2 py-1.5 text-sm text-gray-800 outline-0 disabled:text-gray-200"
/>
Expand Down Expand Up @@ -317,6 +355,13 @@ export function MultiSelect({
</div>
</div>
{description && <Description>{description}</Description>}
{errorMessage && (
<div className="mt-1 text-sm text-red-600">
{typeof errorMessage === 'function'
? errorMessage({} as ValidationResult)
: errorMessage}
</div>
)}
</div>
);
}
Loading