ListSelect.tsx 5.4 Ko
Télécharger le fichier
import { Select as BaseSelect } from '@base-ui/react/select';
import { CSSProperties, FC, ReactNode, RefObject, useRef, useState } from 'react';
import { nativeTextChildren } from './native-text/nativeTextChildren';
export interface ListSelectOption {
value: string;
label: ReactNode;
disabled?: boolean;
}
export interface ListSelectProps {
options: ListSelectOption[];
value: string;
onValueChange: (value: string) => void;
'aria-label'?: string;
disabled?: boolean;
/** Class for the trigger button. */
triggerClassName?: string;
/** Class for the popup surface (the skinned list). */
popupClassName?: string;
/** Inline style for the popup surface (merged with the zoom scale). */
popupStyle?: CSSProperties;
/** Rendered inside the popup before the options (a drawn frame behind the list). */
popupDecoration?: ReactNode;
/** Class added to every option. */
itemClassName?: string;
/** Class for the positioner wrapper. */
positionerClassName?: string;
/** What the trigger shows; defaults to the selected label. */
renderTrigger?: (selected: ListSelectOption | undefined) => ReactNode;
/** Mount the popup inside this element instead of `document.body`, so window-scoped css applies. */
container?: RefObject<HTMLElement | null>;
onOpenChange?: (open: boolean) => void;
side?: 'top' | 'bottom';
align?: 'start' | 'center' | 'end';
/** Gap to the trigger; a function gets the anchor size (use `-anchor.height` to overlay the trigger). */
sideOffset?: number | ((data: { anchor: { width: number; height: number } }) => number);
/** Scale the popup like the trigger (the client UI may be zoomed through a css transform). */
scaleWithTrigger?: boolean;
}
const textOf = (node: ReactNode): string => (typeof node === 'string' || typeof node === 'number' ? String(node) : '');
/**
* Base UI select for the bespoke dropdowns (pixel-skinned triggers and lists): same behavior as
* `common/Select` but the trigger content, the classes and the mount point belong to the caller.
*/
export const ListSelect: FC<ListSelectProps> = (props) => {
const {
options,
value,
onValueChange,
disabled,
triggerClassName,
popupClassName,
popupStyle: popupStyleProp,
popupDecoration,
itemClassName,
positionerClassName,
renderTrigger,
container,
onOpenChange,
side = 'bottom',
align = 'start',
sideOffset = 0,
scaleWithTrigger = false,
'aria-label': ariaLabel
} = props;
const triggerRef = useRef<HTMLButtonElement>(null);
const [scale, setScale] = useState(1);
const selected = options.find((option) => option.value === value);
const handleOpenChange = (open: boolean) => {
if (open && scaleWithTrigger && triggerRef.current?.offsetWidth)
setScale(triggerRef.current.getBoundingClientRect().width / triggerRef.current.offsetWidth);
onOpenChange?.(open);
};
const scaledStyle: CSSProperties | undefined =
scaleWithTrigger && scale !== 1
? { transform: `scale(${scale})`, transformOrigin: 'top left', maxHeight: (window.innerHeight - 30) / scale }
: undefined;
const popupStyle: CSSProperties | undefined = popupStyleProp || scaledStyle ? { ...popupStyleProp, ...scaledStyle } : undefined;
return (
<BaseSelect.Root
disabled={disabled}
value={value}
onOpenChange={handleOpenChange}
onValueChange={(next) => {
if (next !== null && next !== undefined) onValueChange(String(next));
}}
>
<BaseSelect.Trigger ref={triggerRef} aria-label={ariaLabel} className={triggerClassName}>
{renderTrigger ? renderTrigger(selected) : nativeTextChildren(selected?.label, `${triggerClassName}|${disabled}`)}
</BaseSelect.Trigger>
<BaseSelect.Portal container={container}>
<BaseSelect.Positioner align={align} alignItemWithTrigger={false} className={positionerClassName} side={side} sideOffset={sideOffset}>
<BaseSelect.Popup className={popupClassName} style={popupStyle}>
{popupDecoration}
<BaseSelect.List>
{options.map((option) => (
<BaseSelect.Item
key={option.value}
className={itemClassName}
disabled={option.disabled}
label={textOf(option.label)}
render={(itemProps, state) => (
<div {...itemProps}>
<BaseSelect.ItemText>
{nativeTextChildren(option.label, `${state.highlighted}|${state.selected}|${state.disabled}`)}
</BaseSelect.ItemText>
</div>
)}
value={option.value}
/>
))}
</BaseSelect.List>
</BaseSelect.Popup>
</BaseSelect.Positioner>
</BaseSelect.Portal>
</BaseSelect.Root>
);
};
