Селекты приложения использовали alignItemWithTrigger по умолчанию: Base UI накладывает выбранный пункт списка на триггер, а у селектов с плейсхолдером (без значения, например «Ревизия A/B» на странице операций) попап целиком накрывал триггер — выглядел как сломанный список вместо выпадающего меню. В обёртке SelectMenu/SelectField выставлен alignItemWithTrigger=false — список раскрывается под триггером у всех селектов приложения, как это делает и вендорский data-grid-pagination.
105 lines
3.1 KiB
TypeScript
105 lines
3.1 KiB
TypeScript
import type { ComponentProps, ReactNode } from 'react'
|
|
|
|
import { Field, FieldDescription, FieldLabel } from '@evobgp/ui/components/field'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@evobgp/ui/components/select'
|
|
import { cn } from '@evobgp/ui/lib/utils'
|
|
|
|
export type SelectMenuItem<T extends string = string> = {
|
|
value: T
|
|
label: ReactNode
|
|
disabled?: boolean
|
|
}
|
|
|
|
type SelectRootProps = ComponentProps<typeof Select>
|
|
|
|
export interface SelectMenuProps<T extends string = string>
|
|
extends Omit<SelectRootProps, 'children' | 'onValueChange' | 'value' | 'defaultValue'> {
|
|
items: ReadonlyArray<SelectMenuItem<T>>
|
|
value?: T | null
|
|
defaultValue?: T | null
|
|
onValueChange?: (value: T | null) => void
|
|
placeholder?: string
|
|
id?: string
|
|
triggerClassName?: string
|
|
contentClassName?: string
|
|
size?: 'sm' | 'default'
|
|
side?: ComponentProps<typeof SelectContent>['side']
|
|
}
|
|
|
|
/** ReUI c-select-4: `items` на Root, опции в `SelectGroup`. */
|
|
export function SelectMenu<T extends string = string>({
|
|
items,
|
|
placeholder,
|
|
id,
|
|
triggerClassName,
|
|
contentClassName,
|
|
size = 'default',
|
|
side,
|
|
value,
|
|
defaultValue,
|
|
onValueChange,
|
|
...selectProps
|
|
}: SelectMenuProps<T>) {
|
|
return (
|
|
<Select
|
|
items={items}
|
|
value={value}
|
|
defaultValue={defaultValue}
|
|
onValueChange={(next) => onValueChange?.(next as T | null)}
|
|
{...selectProps}
|
|
>
|
|
<SelectTrigger id={id} className={cn('w-full', triggerClassName)} size={size}>
|
|
<SelectValue placeholder={placeholder} />
|
|
</SelectTrigger>
|
|
{/* alignItemWithTrigger=false: список раскрывается ПОД триггером. Режим
|
|
по умолчанию (true) накладывает выбранный пункт на триггер, а у
|
|
селектов с плейсхолдером (без значения) попап накрывает сам триггер.
|
|
Так же делает вендорский data-grid-pagination. */}
|
|
<SelectContent side={side} alignItemWithTrigger={false} className={contentClassName}>
|
|
<SelectGroup>
|
|
{items.map((item) => (
|
|
<SelectItem key={item.value} value={item.value} disabled={item.disabled}>
|
|
{item.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
)
|
|
}
|
|
|
|
export interface SelectFieldProps<T extends string = string> extends SelectMenuProps<T> {
|
|
label?: ReactNode
|
|
description?: ReactNode
|
|
fieldClassName?: string
|
|
}
|
|
|
|
export function SelectField<T extends string = string>({
|
|
label,
|
|
description,
|
|
fieldClassName,
|
|
id,
|
|
...menuProps
|
|
}: SelectFieldProps<T>) {
|
|
return (
|
|
<Field className={fieldClassName}>
|
|
{label ? <FieldLabel htmlFor={id}>{label}</FieldLabel> : null}
|
|
<SelectMenu id={id} {...menuProps} />
|
|
{description ? (
|
|
typeof description === 'string' ? (
|
|
<FieldDescription className="font-mono text-xs">{description}</FieldDescription>
|
|
) : (
|
|
description
|
|
)
|
|
) : null}
|
|
</Field>
|
|
)
|
|
}
|