Files
cloudbeaver/common-react/@dbeaver/ui-kit/src/Select/SelectField.tsx
T
Sychev Andrey 584e5bb260 103 create radio and radio group components (#3313)
* refactor: rename css variable in order to be more descriptive and stick to the naming convention

* refactor: unify controls, add new global control css variables

* feat: add radio component

* docs: add radio stories

* docs: add input docs story

* docs: add simple form story

* refactor: change default radio dot size

* refactor: change class name for radio children wrapper

* refactor: don't reexport inner radio elements

* refactor: simplify RadioGroup interface by removing items props, make label(any) required

The radiogroup must have an accessible name either by a visible label referenced by aria-labelledby or has a label specified with aria-label.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/radiogroup_role

* refactor: use label wrapper for input component to simplify structure

* refactor: add classname for radio group container
2025-03-21 18:52:39 +08:00

152 lines
4.6 KiB
TypeScript

/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useState } from 'react';
import { SelectProvider, Select, SelectPopover, SelectItem, SelectLabel } from './Select.js';
import './SelectField.css';
export interface SelectItem<T> {
value: T;
label: string;
disabled?: boolean;
}
type PropertyGetter<ItemType, ValueType> = (item: ItemType) => ValueType;
export interface SelectFieldProps<T, ItemType = SelectItem<T>> {
/** Options array - can be SelectOption objects or arbitrary objects */
items: ItemType[];
/**
* Function to extract value from items
* Example: (item) => item.id
*/
itemValue?: PropertyGetter<ItemType, T>;
/**
* Function to extract label or render content from items
* Example: (item) => item.firstName + ' ' + item.lastName
*/
itemRender?: PropertyGetter<ItemType, React.ReactNode>;
/**
* Function to extract disabled state
* Example: (item) => !item.isActive
*/
itemDisabled?: PropertyGetter<ItemType, boolean>;
value?: T;
onChange?: (value: T) => void;
label?: React.ReactNode;
description?: React.ReactNode;
name?: string;
disabled?: boolean;
required?: boolean;
className?: string;
/**
* Custom renderer for the selected value, overrides itemRenderer for the selected state
* Only needed for special formatting of the selected value different from list items
*/
selectedRender?: (value: T | undefined, item: ItemType | undefined) => React.ReactNode;
/**
* Custom arrow icon React Node that will be rendered instead default one
*/
arrowIcon?: React.ReactNode;
}
// Utility function to get value by it's key or using getter function
function getValueByPath<Item, Value>(item: Item, getter: PropertyGetter<Item, Value> | undefined, defaultGetter: (item: Item) => Value): Value {
return getter ? getter(item) : defaultGetter(item);
}
export function SelectField<T, ItemType extends {} = SelectItem<T>>({
items,
value,
onChange,
itemValue,
itemRender,
itemDisabled,
label,
description,
disabled,
required,
className,
selectedRender,
arrowIcon,
name,
}: SelectFieldProps<T, ItemType>) {
const getItemValue = (item: ItemType): T =>
getValueByPath<ItemType, T>(item, itemValue, i => ('value' in i ? (i as unknown as SelectItem<T>).value : (i as unknown as T)));
const renderItem = (item: ItemType): React.ReactNode =>
getValueByPath<ItemType, React.ReactNode>(item, itemRender, i => ('label' in i ? (i as unknown as SelectItem<T>).label : String(i)));
const isItemDisabled = (item: ItemType): boolean =>
getValueByPath<ItemType, boolean>(item, itemDisabled, i => ('disabled' in i ? Boolean((i as unknown as SelectItem<T>).disabled) : false));
const [selectedValue, setSelectedValue] = useState<T | undefined>(() => {
if (value !== undefined) return value;
const firstEnabledItem = items.find(item => !isItemDisabled(item));
return firstEnabledItem ? getItemValue(firstEnabledItem) : undefined;
});
const handleChange = (newValue: T) => {
setSelectedValue(newValue);
onChange?.(newValue);
};
const currentValue = value !== undefined ? value : selectedValue;
const selectedItem = items.find(item => getItemValue(item) === currentValue);
const displayValue = selectedItem ? (selectedRender ? selectedRender(currentValue, selectedItem) : renderItem(selectedItem)) : '';
const labelClassName = required ? ' dbv-kit-select__label--required ' : undefined;
return (
<div className={`dbv-kit-select-field ${className || ''}`}>
<SelectProvider value={currentValue as any} setValue={val => handleChange(val as T)}>
{label && <SelectLabel className={labelClassName}>{label}</SelectLabel>}
<Select name={name} disabled={disabled}>
{displayValue}
{arrowIcon ?? <Select.Arrow />}
</Select>
{description && <span className="dbv-kit-select__description">{description}</span>}
<SelectPopover gutter={4} unmountOnHide>
{items.length === 0 ? (
<div className="dbv-kit-select__empty">No items</div>
) : (
items.map(item => (
<SelectItem
key={String(getItemValue(item))}
value={getItemValue(item) as any}
disabled={isItemDisabled(item)}
className="dbv-kit-select__item"
>
{renderItem(item)}
</SelectItem>
))
)}
</SelectPopover>
</SelectProvider>
</div>
);
}