admin/components/consumer/input/ConsumerSelectField.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

171 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import type { ReactNode } from 'react'
import { Controller, type Control } from 'react-hook-form'
import ConsumerFieldShell from '@/components/consumer/input/ConsumerFieldShell'
import type { ConsumerInputRadius, ConsumerInputSize, ConsumerInputTone } from '@/components/consumer/input/consumerInputTypes'
import {
CONSUMER_FIELD_SELECT_PLACEHOLDER_CLASS,
CONSUMER_FIELD_SELECT_POPOVER_CLASS,
CONSUMER_FIELD_SELECT_VALUE_CLASS,
buildConsumerFieldWrapper,
resolveConsumerTone,
withConsumerFieldError,
} from '@/components/consumer/input/consumerInputStyles'
import { Select, SelectItem } from '@/components/heroui/Select'
import { getOptionValue, type SelectOptionItem } from '@/components/formElements/input/inputUtils'
import { useDialogPortalContainer } from '@/components/formElements/input/useDialogPortalContainer'
import { coerceToString } from '@/helpers'
import { cn } from '@/lib/cn'
interface Props {
ariaLabel?: string
autoFocus?: boolean
className?: string
radius: ConsumerInputRadius
size: ConsumerInputSize
control: Control
description?: ReactNode
disabled?: boolean
iconEnd?: ReactNode
iconStart?: ReactNode
inputWrapper?: string
label?: string
multiple?: boolean
name: string
options: SelectOptionItem[]
placeholder?: string
readonly?: boolean
required?: boolean
selectKey: string
selectValue: string
tone?: ConsumerInputTone
}
export default function ConsumerSelectField({
ariaLabel,
autoFocus,
className,
radius,
size,
control,
description,
disabled,
iconEnd,
iconStart,
label,
multiple,
name,
options,
placeholder,
readonly: _readonly,
required,
selectKey,
selectValue,
tone,
inputWrapper,
}: Props) {
const { portalContainer, setHostEl } = useDialogPortalContainer()
const resolvedTone = resolveConsumerTone('select', tone)
const wrapper = cn(
buildConsumerFieldWrapper({
radius: radius,
size: size,
tone: resolvedTone,
}),
inputWrapper
)
return (
<Controller
control={control}
name={name}
render={({ field, fieldState: { error } }) => {
const invalid = !!error
const triggerWrapper = invalid ? withConsumerFieldError(wrapper) : wrapper
const selectedKeys = multiple
? field.value
: field.value !== undefined && field.value !== null && field.value !== ''
? [String(field.value)]
: []
return (
<div
ref={setHostEl}
className={cn('flex w-full flex-col', className)}
>
<ConsumerFieldShell
description={description}
errorMessage={error?.message}
invalid={invalid}
label={label}
required={required}
>
<Select
ref={field.ref}
aria-label={ariaLabel ?? label ?? name}
autoFocus={autoFocus}
classNames={{
placeholder: CONSUMER_FIELD_SELECT_PLACEHOLDER_CLASS,
trigger: triggerWrapper,
value: CONSUMER_FIELD_SELECT_VALUE_CLASS,
}}
endContent={iconEnd}
isDisabled={disabled}
isInvalid={invalid}
name={field.name}
placeholder={placeholder}
popoverProps={{
placement: 'bottom-start',
portalContainer,
shouldBlockScroll: false,
classNames: { content: CONSUMER_FIELD_SELECT_POPOVER_CLASS },
}}
selectedKeys={selectedKeys}
selectionMode={multiple ? 'multiple' : 'single'}
size={size}
startContent={iconStart}
validationBehavior="aria"
variant="bordered"
onBlur={field.onBlur}
onSelectionChange={(keys) => {
if (multiple) field.onChange(keys)
else if (keys === 'all') field.onChange('')
else field.onChange(String(Array.from(keys as Set<React.Key>)[0] ?? ''))
}}
>
{options.map((item, index) => {
const optionKey = coerceToString(getOptionValue(item, selectKey)) || `select-item-${index}`
const optionLabel = coerceToString(getOptionValue(item, selectValue))
const depth = Number(getOptionValue(item, 'depth') ?? 0)
return (
<SelectItem
key={optionKey}
textValue={optionLabel}
>
{depth > 0 ? (
<div
className="flex items-center gap-2"
style={{ paddingRight: depth * 20 }}
>
<span className="select-none text-tertiary-300"></span>
<span className="truncate">{optionLabel}</span>
</div>
) : (
optionLabel
)}
</SelectItem>
)
})}
</Select>
</ConsumerFieldShell>
</div>
)
}}
/>
)
}