refactor: normalize search input and dify-ui focus states (#37413)

This commit is contained in:
yyh 2026-06-15 09:03:31 +08:00 committed by GitHub
parent fbfbbda245
commit 8eb6a19784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 374 additions and 278 deletions

View File

@ -2579,11 +2579,6 @@
"count": 3
}
},
"web/app/components/base/search-input/index.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
}
},
"web/app/components/base/svg-gallery/index.tsx": {
"node/prefer-global/buffer": {
"count": 1
@ -3511,11 +3506,6 @@
"count": 1
}
},
"web/app/components/datasets/list/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1

View File

@ -6,6 +6,7 @@ import type { Placement } from '../placement'
import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlCompoundFocusClassName } from '../form-control-shared'
import {
overlayIndicatorClassName,
overlayLabelClassName,
@ -49,7 +50,7 @@ const autocompleteInputGroupVariants = cva(
[
'group/autocomplete flex w-full min-w-0 items-center border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
textControlCompoundFocusClassName,
'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',
'data-disabled:hover:border-transparent data-disabled:hover:bg-components-input-bg-disabled',

View File

@ -6,7 +6,7 @@ import type { Placement } from '../placement'
import { Combobox as BaseCombobox } from '@base-ui/react/combobox'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { formLabelClassName } from '../form-control-shared'
import { formLabelClassName, textControlCompoundFocusClassName } from '../form-control-shared'
import {
overlayIndicatorClassName,
overlayLabelClassName,
@ -113,7 +113,7 @@ const comboboxInputGroupVariants = cva(
[
'group/combobox flex w-full min-w-0 items-center border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
textControlCompoundFocusClassName,
'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
'data-popup-open:border-components-input-border-active data-popup-open:bg-components-input-bg-active',
'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',

View File

@ -2,12 +2,16 @@ import { cva } from 'class-variance-authority'
export const formLabelClassName = 'w-fit py-1 text-text-secondary system-sm-medium data-disabled:cursor-not-allowed'
export const textControlFocusClassName = 'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
export const textControlCompoundFocusClassName = 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs'
export const textControlVariants = cva(
[
'w-full appearance-none border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
'placeholder:text-components-input-text-placeholder',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
textControlFocusClassName,
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',

View File

@ -7,6 +7,10 @@ import type {
} from '../index'
import * as React from 'react'
import { render } from 'vitest-browser-react'
import {
FieldLabel,
FieldRoot,
} from '../../field'
import {
NumberField,
NumberFieldControls,
@ -44,11 +48,7 @@ const renderNumberField = ({
return render(
<NumberField defaultValue={defaultValue}>
<NumberFieldGroup data-testid="group" {...groupProps}>
<NumberFieldInput
aria-label="Amount"
data-testid="input"
{...inputProps}
/>
<NumberFieldInput aria-label="Amount" {...inputProps} />
{unitProps && (
<NumberFieldUnit data-testid="unit" {...restUnitProps}>
{unitChildren}
@ -75,6 +75,7 @@ describe('NumberField wrapper', () => {
})
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-lg')
await expect.element(screen.getByTestId('group')).toHaveClass('focus-within:border-components-input-border-active')
await expect.element(screen.getByTestId('group')).toHaveClass('custom-group')
})
@ -89,8 +90,25 @@ describe('NumberField wrapper', () => {
})
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-[10px]')
await expect.element(screen.getByTestId('input')).toHaveClass('px-4')
await expect.element(screen.getByTestId('input')).toHaveClass('py-2')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('px-4')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('py-2')
})
it('should surface field invalid state on the visual group', async () => {
const screen = await render(
<FieldRoot name="amount" invalid>
<FieldLabel>Amount</FieldLabel>
<NumberField defaultValue={8}>
<NumberFieldGroup data-testid="group">
<NumberFieldInput />
</NumberFieldGroup>
</NumberField>
</FieldRoot>,
)
await expect.element(screen.getByTestId('group')).toHaveAttribute('data-invalid')
await expect.element(screen.getByTestId('group')).toHaveClass('data-invalid:border-components-input-border-destructive')
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveAttribute('aria-invalid', 'true')
})
it('should set input defaults and forward passthrough props', async () => {

View File

@ -1,5 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { Button } from '../button'
import {
FieldDescription,
FieldError,
FieldLabel,
FieldRoot,
} from '../field'
import { Form } from '../form'
import {
NumberField,
NumberFieldControls,
@ -8,104 +16,7 @@ import {
NumberFieldIncrement,
NumberFieldInput,
NumberFieldUnit,
} from '.'
import { cn } from '../cn'
type DemoFieldProps = {
label: string
helperText: string
placeholder: string
size: 'medium' | 'large'
unit?: string
defaultValue?: number | null
min?: number
max?: number
step?: number
disabled?: boolean
readOnly?: boolean
showCurrentValue?: boolean
widthClassName?: string
formatValue?: (value: number | null) => string
}
const formatNumericValue = (value: number | null, unit?: string) => {
if (value === null)
return 'Empty'
if (!unit)
return String(value)
return `${value} ${unit}`
}
const FieldLabel = ({
inputId,
label,
helperText,
}: Pick<DemoFieldProps, 'label' | 'helperText'> & { inputId: string }) => (
<div className="space-y-1">
<label htmlFor={inputId} className="system-sm-medium text-text-secondary">
{label}
</label>
<p className="system-xs-regular text-text-tertiary">{helperText}</p>
</div>
)
const DemoField = ({
label,
helperText,
placeholder,
size,
unit,
defaultValue,
min,
max,
step,
disabled,
readOnly,
showCurrentValue,
widthClassName,
formatValue,
}: DemoFieldProps) => {
const inputId = React.useId()
const [value, setValue] = React.useState<number | null>(defaultValue ?? null)
return (
<div className={cn('flex w-full max-w-80 flex-col gap-2', widthClassName)}>
<FieldLabel inputId={inputId} label={label} helperText={helperText} />
<NumberField
value={value}
min={min}
max={max}
step={step}
disabled={disabled}
readOnly={readOnly}
onValueChange={setValue}
>
<NumberFieldGroup size={size}>
<NumberFieldInput
id={inputId}
aria-label={label}
placeholder={placeholder}
size={size}
/>
{unit && <NumberFieldUnit size={size}>{unit}</NumberFieldUnit>}
<NumberFieldControls>
<NumberFieldIncrement size={size} />
<NumberFieldDecrement size={size} />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
{showCurrentValue && (
<p className="system-xs-regular text-text-quaternary">
Current value:
{' '}
{formatValue ? formatValue(value) : formatNumericValue(value, unit)}
</p>
)}
</div>
)
}
} from './index'
const meta = {
title: 'Base/Form/NumberField',
@ -114,7 +25,7 @@ const meta = {
layout: 'centered',
docs: {
description: {
component: 'Compound numeric input built on Base UI NumberField. Stories explicitly enumerate the shipped CVA variants, then cover realistic numeric-entry cases such as decimals, empty values, range limits, read-only, and disabled states.',
component: 'Compound numeric input built on Base UI NumberField. Use it with FieldRoot for labelled, described, and validated form fields.',
},
},
},
@ -122,164 +33,312 @@ const meta = {
} satisfies Meta<typeof NumberField>
export default meta
type Story = StoryObj<typeof meta>
export const VariantMatrix: Story = {
type NumberFieldExampleProps = {
id: string
label: string
name: string
defaultValue?: number
min?: number
max?: number
step?: number | 'any'
placeholder?: string
unit?: string
size?: 'medium' | 'large'
disabled?: boolean
readOnly?: boolean
}
function NumberFieldExample({
id,
label,
name,
defaultValue,
min,
max,
step,
placeholder,
unit,
size = 'medium',
disabled,
readOnly,
}: NumberFieldExampleProps) {
return (
<div className="grid w-80 gap-1">
<label htmlFor={id} className="text-text-secondary system-sm-medium">
{label}
</label>
<NumberField
id={id}
name={name}
defaultValue={defaultValue}
min={min}
max={max}
step={step}
disabled={disabled}
readOnly={readOnly}
>
<NumberFieldGroup size={size}>
<NumberFieldInput placeholder={placeholder} size={size} />
{unit && <NumberFieldUnit size={size}>{unit}</NumberFieldUnit>}
<NumberFieldControls>
<NumberFieldIncrement size={size} />
<NumberFieldDecrement size={size} />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</div>
)
}
export const Basic: Story = {
render: () => (
<div className="grid w-[720px] gap-6 md:grid-cols-2">
<DemoField
label="Top K"
helperText="Regular size without suffix. Covers the regular group, input, and control button spacing."
placeholder="Set top K"
size="medium"
defaultValue={3}
min={1}
max={10}
step={1}
/>
<DemoField
label="Score threshold"
helperText="Regular size with a suffix so the regular unit variant is visible."
placeholder="Set threshold"
size="medium"
unit="%"
defaultValue={85}
min={0}
max={100}
step={1}
/>
<DemoField
label="Chunk overlap"
helperText="Large size without suffix. Matches the larger dataset form treatment."
placeholder="Set overlap"
size="large"
<NumberFieldExample
id="top-k-basic"
name="topK"
label="Top K"
defaultValue={3}
min={1}
max={10}
step={1}
placeholder="Set top K…"
/>
),
}
export const Sizes: Story = {
render: () => (
<div className="grid w-80 gap-3">
<NumberFieldExample
id="number-field-medium"
name="mediumNumber"
label="Medium"
defaultValue={64}
min={0}
max={512}
step={16}
placeholder="Set value…"
/>
<DemoField
label="Max segment length"
helperText="Large size with suffix so the large unit variant is also enumerated."
placeholder="Set length"
size="large"
unit="tokens"
<NumberFieldExample
id="number-field-large"
name="largeNumber"
label="Large with unit"
defaultValue={512}
min={1}
max={4000}
step={32}
placeholder="Set length…"
unit="tokens"
size="large"
/>
</div>
),
}
export const DecimalInputs: Story = {
export const States: Story = {
render: () => (
<div className="grid w-[720px] gap-6 md:grid-cols-2">
<DemoField
label="Score threshold"
helperText="Two-decimal precision with a 0.01 step, like retrieval tuning fields."
placeholder="0.00"
size="medium"
defaultValue={0.82}
<div className="grid w-80 gap-3">
<FieldRoot name="placeholderState">
<FieldLabel>Placeholder</FieldLabel>
<NumberField min={0} max={100}>
<NumberFieldGroup>
<NumberFieldInput placeholder="Set threshold…" />
<NumberFieldUnit>%</NumberFieldUnit>
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
<FieldRoot name="filledState">
<FieldLabel>Filled</FieldLabel>
<NumberField defaultValue={85} min={0} max={100}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldUnit>%</NumberFieldUnit>
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
<FieldRoot name="invalidState" invalid>
<FieldLabel>Invalid</FieldLabel>
<NumberField defaultValue={120} min={0} max={100}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldUnit>%</NumberFieldUnit>
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
<FieldError match>Use a value from 0 to 100.</FieldError>
</FieldRoot>
<FieldRoot name="disabledState">
<FieldLabel>Disabled</FieldLabel>
<NumberField defaultValue={5} min={0} max={10} disabled>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
<FieldRoot name="readonlyState">
<FieldLabel>Read-only</FieldLabel>
<NumberField defaultValue={92} min={0} max={100} readOnly>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldUnit>%</NumberFieldUnit>
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
</div>
),
}
function ControlledDemo() {
const [value, setValue] = React.useState<number | null>(0.82)
return (
<FieldRoot name="controlledThreshold">
<FieldLabel>Score threshold</FieldLabel>
<NumberField
value={value}
min={0}
max={1}
step={0.01}
showCurrentValue
formatValue={value => value === null ? 'Empty' : value.toFixed(2)}
/>
<DemoField
label="Temperature"
helperText="One-decimal stepping for generation parameters."
placeholder="0.0"
size="large"
defaultValue={0.7}
min={0}
max={2}
step={0.1}
showCurrentValue
formatValue={value => value === null ? 'Empty' : value.toFixed(1)}
/>
<DemoField
label="Penalty"
helperText="Starts empty so the placeholder and empty numeric state are both visible."
placeholder="Optional"
size="medium"
defaultValue={null}
min={0}
max={2}
step={0.05}
showCurrentValue
formatValue={value => value === null ? 'Empty' : value.toFixed(2)}
/>
<DemoField
label="Latency budget"
helperText="Decimal input with a unit suffix and larger spacing."
placeholder="0.0"
size="large"
unit="s"
defaultValue={1.5}
min={0.5}
max={10}
step={0.5}
showCurrentValue
formatValue={value => value === null ? 'Empty' : `${value.toFixed(1)} s`}
/>
format={{
maximumFractionDigits: 2,
}}
onValueChange={setValue}
>
<NumberFieldGroup>
<NumberFieldInput placeholder="0.00" />
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
<FieldDescription>
Current value:
{' '}
{value === null ? 'Empty' : value.toFixed(2)}
</FieldDescription>
</FieldRoot>
)
}
export const Controlled: Story = {
render: () => (
<div className="w-80">
<ControlledDemo />
</div>
),
}
export const BoundariesAndStates: Story = {
function FormDemo() {
const [savedValue, setSavedValue] = React.useState<string | null>(null)
return (
<Form
aria-label="Retrieval settings"
className="grid w-80 gap-4"
onFormSubmit={(values) => {
setSavedValue(String(values.topK ?? ''))
}}
>
<FieldRoot name="topK">
<FieldLabel>Top K</FieldLabel>
<NumberField required defaultValue={3} min={1} max={10} step={1}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
<FieldDescription>Choose how many chunks are returned.</FieldDescription>
<FieldError match="valueMissing">Top K is required.</FieldError>
<FieldError match="rangeUnderflow">Use at least 1.</FieldError>
<FieldError match="rangeOverflow">Use 10 or fewer.</FieldError>
</FieldRoot>
<div className="flex justify-end">
<Button type="submit" variant="primary">Save Settings</Button>
</div>
{savedValue && (
<div className="rounded-lg bg-background-section px-3 py-2 text-text-secondary system-xs-regular">
Saved:
{' '}
{savedValue}
</div>
)}
</Form>
)
}
export const WithField: Story = {
render: () => <FormDemo />,
}
export const Formatting: Story = {
render: () => (
<div className="grid w-[720px] gap-6 md:grid-cols-2">
<DemoField
label="HTTP status code"
helperText="Integer-only style usage with tighter bounds from 100 to 599."
placeholder="200"
size="medium"
defaultValue={200}
min={100}
max={599}
step={1}
showCurrentValue
/>
<DemoField
label="Request timeout"
helperText="Bounded regular input with suffix, common in system settings."
placeholder="Set timeout"
size="medium"
unit="ms"
defaultValue={1200}
min={100}
max={10000}
step={100}
showCurrentValue
/>
<DemoField
label="Retry count"
helperText="Disabled state preserves the layout while switching to disabled tokens."
placeholder="Retry count"
size="large"
defaultValue={5}
min={0}
max={10}
step={1}
disabled
showCurrentValue
/>
<DemoField
label="Archived score threshold"
helperText="Read-only state keeps the same structure but removes interactive affordances."
placeholder="0.00"
size="large"
unit="%"
defaultValue={92}
min={0}
max={100}
step={1}
readOnly
showCurrentValue
/>
<div className="grid w-80 gap-3">
<FieldRoot name="currencyBudget">
<FieldLabel>Budget</FieldLabel>
<NumberField
defaultValue={1200}
min={0}
step={100}
format={{
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}}
>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
<FieldRoot name="temperature">
<FieldLabel>Temperature</FieldLabel>
<NumberField
defaultValue={0.7}
min={0}
max={2}
step={0.1}
format={{
maximumFractionDigits: 1,
}}
>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldControls>
<NumberFieldIncrement />
<NumberFieldDecrement />
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</FieldRoot>
</div>
),
}

View File

@ -5,6 +5,7 @@ import type * as React from 'react'
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlCompoundFocusClassName } from '../form-control-shared'
export const NumberField = BaseNumberField.Root
export type NumberFieldRootProps = BaseNumberField.Root.Props
@ -13,7 +14,9 @@ export const numberFieldGroupVariants = cva(
[
'group/number-field flex w-full min-w-0 items-stretch overflow-hidden border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
textControlCompoundFocusClassName,
'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',
'data-disabled:hover:border-transparent data-disabled:hover:bg-components-input-bg-disabled',
'data-readonly:shadow-none data-readonly:hover:border-transparent data-readonly:hover:bg-components-input-bg-normal motion-reduce:transition-none',
@ -32,7 +35,12 @@ export const numberFieldGroupVariants = cva(
)
export type NumberFieldSize = NonNullable<VariantProps<typeof numberFieldGroupVariants>['size']>
export type NumberFieldGroupProps = BaseNumberField.Group.Props & VariantProps<typeof numberFieldGroupVariants>
export type NumberFieldGroupProps
= Omit<BaseNumberField.Group.Props, 'className'>
& VariantProps<typeof numberFieldGroupVariants>
& {
className?: string
}
export function NumberFieldGroup({
className,
@ -67,7 +75,12 @@ export const numberFieldInputVariants = cva(
},
)
export type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'size'> & VariantProps<typeof numberFieldInputVariants>
export type NumberFieldInputProps
= Omit<BaseNumberField.Input.Props, 'className' | 'size'>
& VariantProps<typeof numberFieldInputVariants>
& {
className?: string
}
export function NumberFieldInput({
className,
@ -185,7 +198,12 @@ type NumberFieldButtonVariantProps = Omit<
'direction'
>
export type NumberFieldButtonProps = BaseNumberField.Increment.Props & NumberFieldButtonVariantProps
export type NumberFieldButtonProps
= Omit<BaseNumberField.Increment.Props, 'className'>
& NumberFieldButtonVariantProps
& {
className?: string
}
const incrementAriaLabel = 'Increment value'
const decrementAriaLabel = 'Decrement value'

View File

@ -6,13 +6,14 @@ import type * as React from 'react'
import { Field as BaseField } from '@base-ui/react/field'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import { textControlFocusClassName } from '../form-control-shared'
const textareaVariants = cva(
[
'min-h-20 w-full appearance-none overflow-auto border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
'placeholder:text-components-input-text-placeholder',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
textControlFocusClassName,
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',

View File

@ -217,16 +217,6 @@ function List({
placeholder={t('operation.search', { ns: 'common' })}
aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })}
/>
{!!keywords && (
<button
type="button"
aria-label={t('operation.clear', { ns: 'common' })}
className="absolute top-1/2 right-2 flex size-4 -translate-y-1/2 items-center justify-center text-components-input-text-placeholder hover:text-components-input-text-filled"
onClick={() => setKeywords('')}
>
<span aria-hidden className="i-ri-close-circle-fill size-4" />
</button>
)}
</div>
</div>
<Link

View File

@ -71,7 +71,7 @@ export function Infotip({
aria-label={ariaLabel}
onClick={handleClick}
className={cn(
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
className,
)}
>

View File

@ -35,6 +35,23 @@ describe('SearchInput', () => {
const clearButton = screen.getByLabelText('common.operation.clear')
expect(clearButton).toBeInTheDocument()
})
it('uses the design-system focus treatment for the clear button', () => {
render(<SearchInput value="has value" onValueChange={() => {}} />)
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
expect(clearButton).toHaveClass(
'right-1.5',
'size-5',
'focus-visible:bg-components-input-bg-hover',
'focus-visible:ring-2',
'focus-visible:ring-state-accent-solid',
'focus-visible:ring-inset',
)
expect(clearButton).not.toHaveClass('size-4')
expect(clearButton).not.toHaveClass('focus-visible:ring-1')
expect(clearButton).not.toHaveClass('focus-visible:ring-components-input-border-active')
})
})
describe('Interaction', () => {

View File

@ -84,6 +84,7 @@ export function SearchInput({
onValueChange(e.currentTarget.value)
}}
autoComplete="off"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
enterKeyHint="search"
/>
@ -91,7 +92,7 @@ export function SearchInput({
<button
type="button"
aria-label={t('operation.clear', { ns: 'common' })}
className="group/clear absolute top-1/2 right-2 flex size-4 -translate-y-1/2 cursor-pointer touch-manipulation items-center justify-center rounded-md border-none bg-transparent p-0 outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset"
className="group/clear absolute top-1/2 right-1.5 flex size-5 -translate-y-1/2 cursor-pointer touch-manipulation items-center justify-center rounded-md border-none bg-transparent p-0 outline-hidden focus-visible:bg-components-input-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
onClick={handleClear}
>
<span className="i-ri-close-circle-fill size-4 text-text-quaternary group-hover/clear:text-text-tertiary" aria-hidden="true" />

View File

@ -146,7 +146,7 @@ describe('List', () => {
it('should render the search input', () => {
render(<List />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByRole('searchbox')).toBeInTheDocument()
})
it('should render tag filter', () => {
@ -195,7 +195,7 @@ describe('List', () => {
it('should update search input value', () => {
render(<List />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('searchbox')
fireEvent.change(input, { target: { value: 'test search' } })
expect(input).toHaveValue('test search')
@ -259,7 +259,7 @@ describe('List', () => {
it('should clear search input when onClear is called', () => {
render(<List />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('searchbox')
// First set a value
fireEvent.change(input, { target: { value: 'test search' } })
expect(input).toHaveValue('test search')

View File

@ -7,7 +7,7 @@ import { useBoolean, useDebounceFn } from 'ahooks'
// Libraries
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { SearchInput } from '@/app/components/base/search-input'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
@ -69,13 +69,10 @@ const List = () => {
/>
)}
<TagFilter type="knowledge" value={tagFilterValue} onChange={handleTagsChange} onOpenTagManagement={() => setShowTagManagementModal(true)} />
<Input
showLeftIcon
showClearIcon
wrapperClassName="w-[200px]"
<SearchInput
className="w-50"
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
onValueChange={handleKeywordsChange}
/>
{
isCurrentWorkspaceManager && (