mirror of
https://github.com/langgenius/dify.git
synced 2026-04-19 18:27:27 +08:00
refactor(web): redesign SelectTrigger as pure re-export and migrate WorkplaceSelector to Select
SelectTrigger was a heavily styled function coupling ARIA trigger behavior with form-input appearance (input bg, size variants, built-in chevron, clearable/loading). This made it unusable for non-form contexts like WorkplaceSelector and inconsistent with DropdownMenuTrigger which is a pure re-export. - SelectTrigger is now a pure re-export of BaseSelect.Trigger (aligned with DropdownMenuTrigger pattern) - Added SelectIcon re-export for composed triggers needing chevron state - Removed SelectPrimitiveTrigger, selectTriggerVariants, contentPadding, and SelectTriggerProps - 3 form callers migrated to inline composition with className - WorkplaceSelector migrated from @headlessui/react Menu to base-ui Select with correct listbox semantics, fixing the focus ring issue Made-with: Cursor
This commit is contained in:
@ -11,6 +11,7 @@ import Textarea from '@/app/components/base/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectIcon,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
@ -134,8 +135,11 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
|
||||
{type === InputVarType.checkbox && (
|
||||
<Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}>
|
||||
<Select value={checkboxDefaultSelectValue} onValueChange={value => onPayloadChange('default')(value === CHECKBOX_DEFAULT_TRUE_VALUE)}>
|
||||
<SelectTrigger size="large" className="w-full">
|
||||
<SelectValue placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />
|
||||
<SelectTrigger className="group flex h-9 w-full items-center gap-0.5 rounded-[10px] bg-components-input-bg-normal px-2.5 py-1 system-md-regular text-components-input-text-filled outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-placeholder:text-components-input-text-placeholder">
|
||||
<SelectValue className="min-w-0 grow truncate px-1.5 py-1" placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />
|
||||
<SelectIcon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary data-open:text-text-secondary">
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
<SelectItem value={CHECKBOX_DEFAULT_TRUE_VALUE}>{t('variableConfig.startChecked', { ns: 'appDebug' })}</SelectItem>
|
||||
@ -157,8 +161,11 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
|
||||
value={tempPayload.default ? String(tempPayload.default) : EMPTY_SELECT_VALUE}
|
||||
onValueChange={value => onPayloadChange('default')(value === EMPTY_SELECT_VALUE ? undefined : value)}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-full">
|
||||
<SelectValue placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />
|
||||
<SelectTrigger className="group flex h-9 w-full items-center gap-0.5 rounded-[10px] bg-components-input-bg-normal px-2.5 py-1 system-md-regular text-components-input-text-filled outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-placeholder:text-components-input-text-placeholder">
|
||||
<SelectValue className="min-w-0 grow truncate px-1.5 py-1" placeholder={t('variableConfig.selectDefaultValue', { ns: 'appDebug' })} />
|
||||
<SelectIcon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary data-open:text-text-secondary">
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
<SelectItem value={EMPTY_SELECT_VALUE}>{t('variableConfig.noDefaultValue', { ns: 'appDebug' })}</SelectItem>
|
||||
|
||||
@ -10,7 +10,7 @@ import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-tim
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { Button } from '@/app/components/base/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { Select, SelectContent, SelectIcon, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
|
||||
enum DATA_FORMAT {
|
||||
TEXT = 'text',
|
||||
@ -311,8 +311,11 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
|
||||
defaultValue={formValues[name] as string | undefined}
|
||||
onValueChange={val => updateValue(name, val as string)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
<SelectTrigger className="group flex h-8 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 system-sm-regular text-components-input-text-filled outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-placeholder:text-components-input-text-placeholder">
|
||||
<SelectValue className="min-w-0 grow truncate p-1" />
|
||||
<SelectIcon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary data-open:text-text-secondary">
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map(option => (
|
||||
|
||||
@ -1,127 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { Select as BaseSelect } from '@base-ui/react/select'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
|
||||
export const Select = BaseSelect.Root
|
||||
export const SelectValue = BaseSelect.Value
|
||||
export const SelectTrigger = BaseSelect.Trigger
|
||||
export const SelectIcon = BaseSelect.Icon
|
||||
/** @public */
|
||||
export const SelectGroup = BaseSelect.Group
|
||||
/** @public */
|
||||
export const SelectGroupLabel = BaseSelect.GroupLabel
|
||||
/** @public */
|
||||
export const SelectSeparator = BaseSelect.Separator
|
||||
|
||||
const selectTriggerVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
small: 'h-6 gap-px rounded-md px-[5px] py-0 system-xs-regular',
|
||||
regular: 'h-8 gap-0.5 rounded-lg px-2 py-1 system-sm-regular',
|
||||
large: 'h-9 gap-0.5 rounded-[10px] px-2.5 py-1 system-md-regular',
|
||||
},
|
||||
variant: {
|
||||
default: '',
|
||||
destructive: 'border border-components-input-border-destructive bg-components-input-bg-destructive shadow-xs hover:border-components-input-border-destructive hover:bg-components-input-bg-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'regular',
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
export const SelectPrimitiveItem = BaseSelect.Item
|
||||
export const SelectItemText = BaseSelect.ItemText
|
||||
|
||||
const contentPadding: Record<string, string> = {
|
||||
small: 'px-[3px] py-1',
|
||||
regular: 'p-1',
|
||||
large: 'px-1.5 py-1',
|
||||
export function SelectGroupLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof BaseSelect.GroupLabel>) {
|
||||
return (
|
||||
<BaseSelect.GroupLabel
|
||||
className={cn('px-3 pt-1 pb-0.5 system-xs-medium-uppercase text-text-tertiary', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectTriggerProps = React.ComponentPropsWithoutRef<typeof BaseSelect.Trigger> & {
|
||||
clearable?: boolean
|
||||
onClear?: () => void
|
||||
loading?: boolean
|
||||
} & VariantProps<typeof selectTriggerVariants>
|
||||
|
||||
export function SelectTrigger({
|
||||
export function SelectItemIndicator({
|
||||
className,
|
||||
children,
|
||||
size = 'regular',
|
||||
variant = 'default',
|
||||
clearable = false,
|
||||
onClear,
|
||||
loading = false,
|
||||
...props
|
||||
}: SelectTriggerProps) {
|
||||
const paddingClass = contentPadding[size ?? 'regular']
|
||||
const isDestructive = variant === 'destructive'
|
||||
|
||||
let trailingIcon: React.ReactNode = null
|
||||
if (loading) {
|
||||
trailingIcon = (
|
||||
<span className="shrink-0 text-text-quaternary" aria-hidden="true">
|
||||
<span className="i-ri-loader-4-line h-3.5 w-3.5 animate-spin" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
else if (isDestructive) {
|
||||
trailingIcon = (
|
||||
<span className="shrink-0 text-text-destructive-secondary" aria-hidden="true">
|
||||
<span className="i-ri-error-warning-line h-4 w-4" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
else if (clearable) {
|
||||
trailingIcon = (
|
||||
<span
|
||||
role="button"
|
||||
aria-label="Clear selection"
|
||||
tabIndex={-1}
|
||||
className="shrink-0 cursor-pointer text-text-quaternary group-data-disabled:hidden group-data-readonly:hidden hover:text-text-secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClear?.()
|
||||
}}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill h-3.5 w-3.5" aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
else {
|
||||
trailingIcon = (
|
||||
<BaseSelect.Icon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary group-data-readonly:hidden data-open:text-text-secondary">
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden="true" />
|
||||
</BaseSelect.Icon>
|
||||
)
|
||||
}
|
||||
|
||||
}: Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.ItemIndicator>, 'children'>) {
|
||||
return (
|
||||
<BaseSelect.Trigger
|
||||
className={cn(
|
||||
'group relative flex w-full items-center border-0 bg-components-input-bg-normal text-left text-components-input-text-filled outline-hidden',
|
||||
'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt',
|
||||
'data-placeholder:text-components-input-text-placeholder',
|
||||
selectTriggerVariants({ size, variant }),
|
||||
'data-readonly:cursor-default data-readonly:bg-transparent data-readonly:hover:bg-transparent',
|
||||
'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled',
|
||||
'data-disabled:data-placeholder:text-components-input-text-disabled',
|
||||
className,
|
||||
)}
|
||||
<BaseSelect.ItemIndicator
|
||||
className={cn('flex shrink-0 items-center text-text-accent', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className={cn('min-w-0 grow truncate', paddingClass)}>
|
||||
{children}
|
||||
</span>
|
||||
{trailingIcon}
|
||||
</BaseSelect.Trigger>
|
||||
<span className="i-ri-check-line h-4 w-4" aria-hidden />
|
||||
</BaseSelect.ItemIndicator>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
import type { Plan } from '@/app/components/billing/type'
|
||||
import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectPrimitiveItem,
|
||||
SelectTrigger,
|
||||
} from '@/app/components/base/ui/select'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import PlanBadge from '@/app/components/header/plan-badge'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
@ -27,49 +34,58 @@ const WorkplaceSelector = () => {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Menu as="div" className="min-w-0">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton className={cn(`
|
||||
group flex w-full cursor-pointer items-center
|
||||
p-0.5 hover:bg-state-base-hover ${open && 'bg-state-base-hover'} rounded-[10px]
|
||||
`)}
|
||||
>
|
||||
<div className="mr-1.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px] max-[800px]:mr-0">
|
||||
<span className="h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center">
|
||||
<div className="max-w-[149px] min-w-0 truncate system-sm-medium text-text-secondary max-[800px]:hidden">{currentWorkspace?.name}</div>
|
||||
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</MenuButton>
|
||||
<Transition as={Fragment} enter="transition ease-out duration-100" enterFrom="transform opacity-0 scale-95" enterTo="transform opacity-100 scale-100" leave="transition ease-in duration-75" leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95">
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className={cn(`
|
||||
shadows-shadow-lg absolute left-[-15px] z-[1000] mt-1 flex max-h-[400px] w-[280px] flex-col items-start overflow-y-auto
|
||||
rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px]
|
||||
`)}
|
||||
<Select
|
||||
value={currentWorkspace?.id ?? ''}
|
||||
onValueChange={(value) => {
|
||||
if (value)
|
||||
void handleSwitchWorkspace(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'group flex min-w-0 cursor-pointer items-center rounded-[10px] p-0.5 outline-hidden',
|
||||
'hover:bg-state-base-hover data-popup-open:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="mr-1.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px] max-[800px]:mr-0">
|
||||
<span className="h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center">
|
||||
<div className="max-w-[149px] min-w-0 truncate system-sm-medium text-text-secondary max-[800px]:hidden">{currentWorkspace?.name}</div>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[280px] max-h-[400px]"
|
||||
>
|
||||
<SelectGroup>
|
||||
<SelectGroupLabel>
|
||||
{t('userProfile.workspace', { ns: 'common' })}
|
||||
</SelectGroupLabel>
|
||||
{workspaces.map(workspace => (
|
||||
<SelectPrimitiveItem
|
||||
key={workspace.id}
|
||||
value={workspace.id}
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer items-center gap-1 rounded-lg px-2 outline-hidden',
|
||||
'data-highlighted:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start self-stretch rounded-xl border-[0.5px] border-components-panel-border p-1 pb-2 shadow-lg">
|
||||
<div className="flex items-start self-stretch px-3 pt-1 pb-0.5">
|
||||
<span className="flex-1 system-xs-medium-uppercase text-text-tertiary">{t('userProfile.workspace', { ns: 'common' })}</span>
|
||||
</div>
|
||||
{workspaces.map(workspace => (
|
||||
<div className="flex items-center gap-2 self-stretch rounded-lg py-1 pr-2 pl-3 hover:bg-state-base-hover" key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]">
|
||||
<span className="h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">{workspace?.name[0]?.toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<div className="line-clamp-1 grow cursor-pointer overflow-hidden system-md-regular text-ellipsis text-text-secondary">{workspace.name}</div>
|
||||
<PlanBadge plan={workspace.plan as Plan} />
|
||||
</div>
|
||||
))}
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]">
|
||||
<span className="h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">{workspace.name[0]?.toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<SelectItemText className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary">
|
||||
{workspace.name}
|
||||
</SelectItemText>
|
||||
<PlanBadge plan={workspace.plan as Plan} />
|
||||
<SelectItemIndicator />
|
||||
</SelectPrimitiveItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
export default WorkplaceSelector
|
||||
|
||||
@ -10,7 +10,7 @@ import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { Select, SelectContent, SelectIcon, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { Slider } from '@/app/components/base/ui/slider'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
@ -294,8 +294,11 @@ function ParameterItem({
|
||||
value={renderValue as string}
|
||||
onValueChange={v => handleInputChange(v ?? undefined)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
<SelectTrigger className="group flex h-8 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 system-sm-regular text-components-input-text-filled outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-placeholder:text-components-input-text-placeholder">
|
||||
<SelectValue className="min-w-0 grow truncate p-1" />
|
||||
<SelectIcon className="shrink-0 text-text-quaternary transition-colors group-hover:text-text-secondary data-open:text-text-secondary">
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parameterRule.options!.map(option => (
|
||||
|
||||
Reference in New Issue
Block a user