mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix(ui): expose slider values and unify class name resolution (#43293)
This commit is contained in:
26 files changed
+204
-488
No files matched your search
@@ -5,8 +5,6 @@ import {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
AutocompleteInput,
|
||||
AutocompleteInputGroup,
|
||||
AutocompleteItem,
|
||||
@@ -16,7 +14,6 @@ import {
|
||||
AutocompletePopup,
|
||||
AutocompletePortal,
|
||||
AutocompletePositioner,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
AutocompleteTrigger,
|
||||
useAutocompleteFilteredItems,
|
||||
@@ -94,13 +91,13 @@ describe('Autocomplete wrappers', () => {
|
||||
.not.toBe(restingBoxShadow)
|
||||
})
|
||||
|
||||
it('should set input defaults and forward passthrough props', async () => {
|
||||
it('should disable autocomplete and expose placeholder and required state', async () => {
|
||||
const screen = await renderAutocomplete({
|
||||
children: (
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput
|
||||
aria-label="Search suggestions"
|
||||
className="custom-input"
|
||||
|
||||
placeholder="Find a resource"
|
||||
required
|
||||
/>
|
||||
@@ -117,9 +114,6 @@ describe('Autocomplete wrappers', () => {
|
||||
await expect
|
||||
.element(screen.getByRole('combobox', { name: 'Search suggestions' }))
|
||||
.toBeRequired()
|
||||
await expect
|
||||
.element(screen.getByRole('combobox', { name: 'Search suggestions' }))
|
||||
.toHaveClass('custom-input')
|
||||
})
|
||||
|
||||
it('should not inject input-only attributes into a custom textarea', async () => {
|
||||
@@ -245,45 +239,6 @@ describe('Autocomplete wrappers', () => {
|
||||
expect(onPopupClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should forward custom classes to label separator item text and indicator', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Autocomplete open defaultValue="workflow" items={['workflow']}>
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Search suggestions" />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup role="dialog" aria-label="autocomplete popup">
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteGroup items={['workflow']}>
|
||||
<AutocompleteGroupLabel className="custom-label">
|
||||
Resources
|
||||
</AutocompleteGroupLabel>
|
||||
<AutocompleteSeparator className="custom-separator" data-testid="separator" />
|
||||
<AutocompleteItem value="workflow" className="custom-item">
|
||||
<AutocompleteItemText className="custom-text">Workflow</AutocompleteItemText>
|
||||
<AutocompleteItemIndicator
|
||||
className="custom-indicator"
|
||||
data-testid="indicator"
|
||||
/>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteGroup>
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByText('Resources')).toHaveClass('custom-label')
|
||||
await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator')
|
||||
await expect
|
||||
.element(screen.getByRole('option', { name: 'Workflow' }))
|
||||
.toHaveClass('custom-item')
|
||||
await expect.element(screen.getByText('Workflow')).toHaveClass('custom-text')
|
||||
await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator')
|
||||
})
|
||||
|
||||
it('should navigate function-rendered items with arrow keys', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Autocomplete open defaultValue="" items={['workflow', 'dataset', 'app']}>
|
||||
|
||||
@@ -47,15 +47,6 @@ describe('Avatar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('should merge className with avatar variant classes on root', async () => {
|
||||
const screen = await render(<Avatar name="Test" avatar={null} className="custom-class" />)
|
||||
|
||||
const root = screen.container.firstElementChild as HTMLElement
|
||||
expect(root).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onLoadingStatusChange', () => {
|
||||
it('should show fallback until the image loads and forward status changes', async () => {
|
||||
const { images, restore } = stubImageLoader()
|
||||
|
||||
@@ -66,28 +66,4 @@ describe('Button', () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('className merging', () => {
|
||||
it('merges custom className with variant classes', async () => {
|
||||
const screen = await render(<Button className="custom-class">Click me</Button>)
|
||||
const btn = screen.getByRole('button').element()
|
||||
expect(btn).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ref forwarding', () => {
|
||||
it('forwards ref to the button element', async () => {
|
||||
let buttonRef: HTMLButtonElement | null = null
|
||||
await render(
|
||||
<Button
|
||||
ref={(el) => {
|
||||
buttonRef = el
|
||||
}}
|
||||
>
|
||||
Click me
|
||||
</Button>,
|
||||
)
|
||||
expect(buttonRef).toBeInstanceOf(HTMLButtonElement)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
ComboboxCollection,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxGroupLabel,
|
||||
ComboboxInput,
|
||||
ComboboxInputGroup,
|
||||
ComboboxInputTrigger,
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
ComboboxPopup,
|
||||
ComboboxPortal,
|
||||
ComboboxPositioner,
|
||||
ComboboxSeparator,
|
||||
ComboboxStatus,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
@@ -206,13 +204,13 @@ describe('Combobox wrappers', () => {
|
||||
.not.toBe(restingBoxShadow)
|
||||
})
|
||||
|
||||
it('should set input defaults and forward passthrough props', async () => {
|
||||
it('should disable autocomplete and expose placeholder and required state', async () => {
|
||||
const screen = await renderInputCombobox({
|
||||
children: (
|
||||
<ComboboxInputGroup>
|
||||
<ComboboxInput
|
||||
aria-label="Search resources"
|
||||
className="custom-input"
|
||||
|
||||
placeholder="Find a resource"
|
||||
required
|
||||
/>
|
||||
@@ -229,9 +227,6 @@ describe('Combobox wrappers', () => {
|
||||
await expect
|
||||
.element(screen.getByRole('combobox', { name: 'Search resources' }))
|
||||
.toBeRequired()
|
||||
await expect
|
||||
.element(screen.getByRole('combobox', { name: 'Search resources' }))
|
||||
.toHaveClass('custom-input')
|
||||
})
|
||||
|
||||
it('should not inject input-only attributes into a custom textarea', async () => {
|
||||
@@ -403,43 +398,6 @@ describe('Combobox wrappers', () => {
|
||||
expect(status.element().getBoundingClientRect().height).toBe(0)
|
||||
})
|
||||
|
||||
it('should forward custom classes to group label separator item text and indicator', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Combobox open defaultValue="workflow" items={['workflow']}>
|
||||
<ComboboxTrigger aria-label="Resource type">
|
||||
<ComboboxValue />
|
||||
</ComboboxTrigger>
|
||||
<ComboboxPortal>
|
||||
<ComboboxPositioner>
|
||||
<ComboboxPopup aria-label="Choose a resource">
|
||||
<ComboboxInput aria-label="Filter resources" />
|
||||
<ComboboxList data-testid="custom-list">
|
||||
<ComboboxGroup items={['workflow']}>
|
||||
<ComboboxGroupLabel className="custom-label">Resources</ComboboxGroupLabel>
|
||||
<ComboboxSeparator className="custom-separator" data-testid="separator" />
|
||||
<ComboboxItem value="workflow" className="custom-item">
|
||||
<ComboboxItemText className="custom-text">Workflow</ComboboxItemText>
|
||||
<ComboboxItemIndicator className="custom-indicator" data-testid="indicator" />
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</ComboboxList>
|
||||
</ComboboxPopup>
|
||||
</ComboboxPositioner>
|
||||
</ComboboxPortal>
|
||||
</Combobox>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByText('Resources')).toHaveClass('custom-label')
|
||||
await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator')
|
||||
await expect
|
||||
.element(screen.getByRole('option', { name: 'Workflow' }))
|
||||
.toHaveClass('custom-item')
|
||||
await expect
|
||||
.element(screen.getByTestId('custom-list').getByText('Workflow'))
|
||||
.toHaveClass('custom-text')
|
||||
await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator')
|
||||
})
|
||||
|
||||
it('should navigate function-rendered items with arrow keys', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Combobox defaultValue="workflow" items={['workflow', 'dataset', 'app']}>
|
||||
@@ -525,16 +483,16 @@ describe('Combobox wrappers', () => {
|
||||
await expect.element(screen.getByText('No reviewers selected')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render chip wrappers and default remove button label', async () => {
|
||||
it('should give chip remove buttons a default accessible name and non-submit type', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Combobox multiple defaultValue={['maya']} items={['maya', 'nora']}>
|
||||
<ComboboxInputGroup>
|
||||
<ComboboxChips className="custom-chips" data-testid="chips">
|
||||
<ComboboxChips data-testid="chips">
|
||||
<ComboboxValue<string, true>>
|
||||
{(selectedValue) => (
|
||||
<React.Fragment>
|
||||
{selectedValue?.map((item) => (
|
||||
<ComboboxChip key={item} className="custom-chip">
|
||||
<ComboboxChip key={item}>
|
||||
<span>{item}</span>
|
||||
<ComboboxChipRemove data-testid="remove-chip" />
|
||||
</ComboboxChip>
|
||||
@@ -548,10 +506,6 @@ describe('Combobox wrappers', () => {
|
||||
</Combobox>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByTestId('chips')).toHaveClass('custom-chips')
|
||||
await expect
|
||||
.element(screen.getByText('maya').element().parentElement!)
|
||||
.toHaveClass('custom-chip')
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'Remove selected item' }))
|
||||
.toHaveAttribute('type', 'button')
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from '../../cn'
|
||||
import { DirectionProvider, useDirection } from '../../direction-provider'
|
||||
import { formLabelClassName } from '../../form-control-shared'
|
||||
import { IconButton } from '../../icon-button'
|
||||
import { resolveClassName } from '../../internals/resolve-class-name'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../popover'
|
||||
|
||||
type PickerOpenChangeDetails = Omit<
|
||||
@@ -278,7 +279,7 @@ function PickerTrigger({
|
||||
'flex h-8 w-63 max-w-full items-center justify-between gap-0.5 rounded-lg bg-components-input-bg-normal ps-3 pe-2 text-start system-sm-regular text-components-input-text-filled',
|
||||
'hover:bg-state-base-hover-alt focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-state-accent-solid data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-popup-open:bg-state-base-hover-alt',
|
||||
!field.serializedValue && 'text-text-tertiary in-data-[theme=dark]:text-text-secondary',
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
resolveClassName(className, state),
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -364,7 +365,7 @@ function PickerContent({
|
||||
cn(
|
||||
'forced-colors:[&_button:focus-visible]:outline-2 forced-colors:[&_button:focus-visible]:outline-[Highlight] forced-colors:[&_button:focus-visible]:outline-solid',
|
||||
'flex w-63 max-w-(--available-width) flex-col overflow-hidden border-0 p-0 inset-ring-[0.5px] inset-ring-components-panel-border backdrop-blur-[5px]',
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
resolveClassName(className, state),
|
||||
)
|
||||
}
|
||||
initialFocus={() =>
|
||||
|
||||
@@ -346,36 +346,6 @@ describe('dropdown-menu wrapper', () => {
|
||||
})
|
||||
|
||||
describe('DropdownMenuSeparator', () => {
|
||||
it('should forward passthrough props and handlers when separator props are provided', async () => {
|
||||
const handleMouseEnter = vi.fn()
|
||||
|
||||
const screen = await render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSeparator
|
||||
aria-label="actions divider"
|
||||
id="menu-separator"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
screen
|
||||
.getByRole('separator', { name: 'actions divider' })
|
||||
.element()
|
||||
.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
}),
|
||||
)
|
||||
await expect
|
||||
.element(screen.getByRole('separator', { name: 'actions divider' }))
|
||||
.toHaveAttribute('id', 'menu-separator')
|
||||
expect(handleMouseEnter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should keep surrounding menu rows rendered when separator is placed between items', async () => {
|
||||
const screen = await render(
|
||||
<DropdownMenu open>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Fieldset, FieldsetLegend } from '../index'
|
||||
|
||||
describe('Fieldset primitives', () => {
|
||||
it('should forward className to the fieldset and legend', async () => {
|
||||
const screen = await render(
|
||||
<Fieldset className="custom-root">
|
||||
<FieldsetLegend className="custom-legend">Permissions</FieldsetLegend>
|
||||
</Fieldset>,
|
||||
)
|
||||
|
||||
const legend = screen.getByText('Permissions').element() as HTMLElement
|
||||
const fieldset = legend.closest('fieldset') as HTMLElement
|
||||
|
||||
await expect.element(fieldset).toHaveClass('custom-root')
|
||||
await expect.element(legend).toHaveClass('custom-legend')
|
||||
})
|
||||
})
|
||||
@@ -4,21 +4,6 @@ import { Input } from '../../input'
|
||||
import { Form } from '../index'
|
||||
|
||||
describe('Form primitive', () => {
|
||||
it('should render a native named form and merge custom class names', async () => {
|
||||
const screen = await render(
|
||||
<Form aria-label="profile form" className="custom-form">
|
||||
<Field name="name">
|
||||
<FieldLabel>Name</FieldLabel>
|
||||
<Input defaultValue="Ada" />
|
||||
</Field>
|
||||
</Form>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('form', { name: 'profile form' }))
|
||||
.toHaveClass('custom-form')
|
||||
})
|
||||
|
||||
it('should call onFormSubmit with submitted values', async () => {
|
||||
const onFormSubmit = vi.fn()
|
||||
const screen = await render(
|
||||
|
||||
@@ -15,12 +15,6 @@ describe('Kbd', () => {
|
||||
await expect.element(screen.getByText('⌘')).toHaveAttribute('data-disabled')
|
||||
await expect.element(screen.getByText('⌘')).not.toHaveAttribute('aria-disabled')
|
||||
})
|
||||
|
||||
it('merges custom classes with the design-system recipe', async () => {
|
||||
const screen = await render(<Kbd className="custom-key h-5">K</Kbd>)
|
||||
|
||||
await expect.element(screen.getByText('K')).toHaveClass('custom-key')
|
||||
})
|
||||
})
|
||||
|
||||
describe('KbdGroup', () => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type {
|
||||
NumberFieldControlsProps,
|
||||
NumberFieldDecrementProps,
|
||||
NumberFieldGroupProps,
|
||||
NumberFieldIncrementProps,
|
||||
NumberFieldInputProps,
|
||||
NumberFieldUnitProps,
|
||||
} from '../index'
|
||||
import * as React from 'react'
|
||||
import { userEvent } from 'vite-plus/test/browser'
|
||||
@@ -18,14 +16,11 @@ import {
|
||||
NumberFieldGroup,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
NumberFieldUnit,
|
||||
} from '../index'
|
||||
|
||||
type RenderNumberFieldOptions = {
|
||||
defaultValue?: number
|
||||
groupProps?: Partial<NumberFieldGroupProps>
|
||||
inputProps?: Partial<NumberFieldInputProps>
|
||||
unitProps?: Partial<NumberFieldUnitProps> & { children?: React.ReactNode }
|
||||
controlsProps?: Partial<NumberFieldControlsProps>
|
||||
incrementProps?: Partial<NumberFieldIncrementProps>
|
||||
decrementProps?: Partial<NumberFieldDecrementProps>
|
||||
@@ -33,24 +28,15 @@ type RenderNumberFieldOptions = {
|
||||
|
||||
const renderNumberField = ({
|
||||
defaultValue = 8,
|
||||
groupProps,
|
||||
inputProps,
|
||||
unitProps,
|
||||
controlsProps,
|
||||
incrementProps,
|
||||
decrementProps,
|
||||
}: RenderNumberFieldOptions = {}) => {
|
||||
const { children: unitChildren = 'ms', ...restUnitProps } = unitProps ?? {}
|
||||
|
||||
return render(
|
||||
<NumberField defaultValue={defaultValue}>
|
||||
<NumberFieldGroup data-testid="group" {...groupProps}>
|
||||
<NumberFieldGroup data-testid="group">
|
||||
<NumberFieldInput aria-label="Amount" {...inputProps} />
|
||||
{unitProps && (
|
||||
<NumberFieldUnit data-testid="unit" {...restUnitProps}>
|
||||
{unitChildren}
|
||||
</NumberFieldUnit>
|
||||
)}
|
||||
{(controlsProps || incrementProps || decrementProps) && (
|
||||
<NumberFieldControls data-testid="controls" {...controlsProps}>
|
||||
<NumberFieldIncrement data-testid="increment" {...incrementProps} />
|
||||
@@ -78,16 +64,6 @@ describe('NumberField wrapper', () => {
|
||||
.not.toBe(restingBoxShadow)
|
||||
})
|
||||
|
||||
it('should merge custom className on the group', async () => {
|
||||
const screen = await renderNumberField({
|
||||
groupProps: {
|
||||
className: 'custom-group',
|
||||
},
|
||||
})
|
||||
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('custom-group')
|
||||
})
|
||||
|
||||
it('should surface field invalid state on the visual group', async () => {
|
||||
const screen = await render(
|
||||
<Field name="amount" invalid>
|
||||
@@ -106,10 +82,9 @@ describe('NumberField wrapper', () => {
|
||||
.toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('should set input defaults and forward passthrough props', async () => {
|
||||
it('should disable autocomplete and expose placeholder and required state', async () => {
|
||||
const screen = await renderNumberField({
|
||||
inputProps: {
|
||||
className: 'custom-input',
|
||||
placeholder: 'Regular placeholder',
|
||||
required: true,
|
||||
},
|
||||
@@ -125,38 +100,6 @@ describe('NumberField wrapper', () => {
|
||||
.element(screen.getByRole('textbox', { name: 'Amount' }))
|
||||
.toHaveAttribute('placeholder', 'Regular placeholder')
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toBeRequired()
|
||||
await expect
|
||||
.element(screen.getByRole('textbox', { name: 'Amount' }))
|
||||
.toHaveClass('custom-input')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unit and controls', () => {
|
||||
it('should forward passthrough props to the unit', async () => {
|
||||
const screen = await renderNumberField({
|
||||
unitProps: {
|
||||
className: 'custom-unit',
|
||||
title: 'unit-title',
|
||||
},
|
||||
})
|
||||
|
||||
await expect.element(screen.getByTestId('unit')).toHaveTextContent('ms')
|
||||
await expect.element(screen.getByTestId('unit')).toHaveAttribute('title', 'unit-title')
|
||||
await expect.element(screen.getByTestId('unit')).toHaveClass('custom-unit')
|
||||
})
|
||||
|
||||
it('should forward passthrough props to controls', async () => {
|
||||
const screen = await renderNumberField({
|
||||
controlsProps: {
|
||||
className: 'custom-controls',
|
||||
title: 'controls-title',
|
||||
},
|
||||
})
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId('controls'))
|
||||
.toHaveAttribute('title', 'controls-title')
|
||||
await expect.element(screen.getByTestId('controls')).toHaveClass('custom-controls')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -238,25 +181,6 @@ describe('NumberField wrapper', () => {
|
||||
.element(screen.getByRole('button', { name: 'Decrement from label' }))
|
||||
.not.toHaveAttribute('aria-label')
|
||||
})
|
||||
|
||||
it('should forward passthrough props to control buttons', async () => {
|
||||
const screen = await renderNumberField({
|
||||
controlsProps: {},
|
||||
incrementProps: {
|
||||
className: 'custom-increment',
|
||||
},
|
||||
decrementProps: {
|
||||
className: 'custom-decrement',
|
||||
title: 'decrement-title',
|
||||
},
|
||||
})
|
||||
|
||||
await expect.element(screen.getByTestId('increment')).toHaveClass('custom-increment')
|
||||
await expect.element(screen.getByTestId('decrement')).toHaveClass('custom-decrement')
|
||||
await expect
|
||||
.element(screen.getByTestId('decrement'))
|
||||
.toHaveAttribute('title', 'decrement-title')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -344,9 +344,7 @@ describe('Pagination primitive', () => {
|
||||
<PaginationRoot page={3} totalPages={5} onPageChange={onPageChange}>
|
||||
<ol>
|
||||
<li>
|
||||
<PaginationPage page={4} className="custom-page">
|
||||
Four
|
||||
</PaginationPage>
|
||||
<PaginationPage page={4}>Four</PaginationPage>
|
||||
</li>
|
||||
</ol>
|
||||
</PaginationRoot>,
|
||||
@@ -354,9 +352,6 @@ describe('Pagination primitive', () => {
|
||||
|
||||
await screen.getByRole('button', { name: 'Go to page 4' }).click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'Go to page 4' }))
|
||||
.toHaveClass('custom-page')
|
||||
expect(onPageChange).toHaveBeenCalledWith(4)
|
||||
})
|
||||
|
||||
|
||||
@@ -184,21 +184,13 @@ describe('Radio', () => {
|
||||
it('should support custom items with a visual RadioControl', async () => {
|
||||
const screen = await render(
|
||||
<RadioGroup defaultValue="card" aria-label="Card choice">
|
||||
<RadioItem
|
||||
value="card"
|
||||
nativeButton
|
||||
render={<button type="button" className="custom-card" />}
|
||||
>
|
||||
<RadioItem value="card" nativeButton render={<button type="button" />}>
|
||||
<span>Card option</span>
|
||||
<RadioControl className="custom-control" />
|
||||
<RadioControl />
|
||||
</RadioItem>
|
||||
</RadioGroup>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('radio', { name: 'Card option' }))
|
||||
.toHaveClass('custom-card')
|
||||
expect(screen.container.querySelector('.custom-control')).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('radio', { name: 'Card option' }))
|
||||
.toHaveAttribute('data-checked', '')
|
||||
|
||||
@@ -33,47 +33,26 @@ const stubElementMetric = (
|
||||
|
||||
const renderScrollArea = (
|
||||
options: {
|
||||
rootClassName?: string
|
||||
contentStyle?: React.CSSProperties
|
||||
viewportClassName?: string
|
||||
viewportStyle?: React.CSSProperties
|
||||
verticalScrollbarClassName?: string
|
||||
horizontalScrollbarClassName?: string
|
||||
verticalThumbClassName?: string
|
||||
horizontalThumbClassName?: string
|
||||
} = {},
|
||||
) => {
|
||||
return render(
|
||||
<ScrollArea className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport
|
||||
data-testid="scroll-area-viewport"
|
||||
style={options.viewportStyle}
|
||||
className={options.viewportClassName}
|
||||
>
|
||||
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport data-testid="scroll-area-viewport" style={options.viewportStyle}>
|
||||
<ScrollAreaContent data-testid="scroll-area-content" style={options.contentStyle}>
|
||||
<div className="h-48 w-48">Scrollable content</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar
|
||||
keepMounted
|
||||
data-testid="scroll-area-vertical-scrollbar"
|
||||
className={options.verticalScrollbarClassName}
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
data-testid="scroll-area-vertical-thumb"
|
||||
className={options.verticalThumbClassName}
|
||||
/>
|
||||
<ScrollAreaScrollbar keepMounted data-testid="scroll-area-vertical-scrollbar">
|
||||
<ScrollAreaThumb data-testid="scroll-area-vertical-thumb" />
|
||||
</ScrollAreaScrollbar>
|
||||
<ScrollAreaScrollbar
|
||||
keepMounted
|
||||
orientation="horizontal"
|
||||
data-testid="scroll-area-horizontal-scrollbar"
|
||||
className={options.horizontalScrollbarClassName}
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
data-testid="scroll-area-horizontal-thumb"
|
||||
className={options.horizontalThumbClassName}
|
||||
/>
|
||||
<ScrollAreaThumb data-testid="scroll-area-horizontal-thumb" />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>,
|
||||
)
|
||||
@@ -110,14 +89,14 @@ describe('scroll area', () => {
|
||||
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport
|
||||
aria-labelledby="installed-apps-label"
|
||||
className="custom-viewport-class"
|
||||
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className="custom-content-class">
|
||||
<ScrollAreaContent>
|
||||
<div className="h-48 w-20">Scrollable content</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="custom-scrollbar-class">
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
@@ -125,14 +104,11 @@ describe('scroll area', () => {
|
||||
)
|
||||
|
||||
const viewport = screen.getByRole('region', { name: 'Installed apps' })
|
||||
const content = screen.getByText('Scrollable content').element().parentElement
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId('scroll-area-root'))
|
||||
.not.toHaveAttribute('role', 'region')
|
||||
await expect.element(viewport).toHaveClass('custom-viewport-class')
|
||||
await expect.element(viewport).toHaveAccessibleName('Installed apps')
|
||||
expect(content).toHaveClass('custom-content-class')
|
||||
await expect.element(screen.getByText('Scrollable content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -220,32 +196,6 @@ describe('scroll area', () => {
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should forward className to the viewport', async () => {
|
||||
const screen = await renderScrollArea({
|
||||
viewportClassName: 'custom-viewport-class',
|
||||
})
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId('scroll-area-viewport'))
|
||||
.toHaveClass('custom-viewport-class')
|
||||
})
|
||||
|
||||
it('should let callers control scrollbar inset spacing via margin-based className overrides', async () => {
|
||||
const screen = await renderScrollArea({
|
||||
verticalScrollbarClassName:
|
||||
'data-[orientation=vertical]:my-2 data-[orientation=vertical]:-me-3',
|
||||
horizontalScrollbarClassName:
|
||||
'data-[orientation=horizontal]:mx-2 data-[orientation=horizontal]:mb-2',
|
||||
})
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId('scroll-area-vertical-scrollbar'))
|
||||
.toHaveClass('data-[orientation=vertical]:my-2', 'data-[orientation=vertical]:-me-3')
|
||||
await expect
|
||||
.element(screen.getByTestId('scroll-area-horizontal-scrollbar'))
|
||||
.toHaveClass('data-[orientation=horizontal]:mx-2', 'data-[orientation=horizontal]:mb-2')
|
||||
})
|
||||
|
||||
it('should let vertical layouts override the content minimum width without important CSS', async () => {
|
||||
const screen = await renderScrollArea({ contentStyle: { minWidth: 0 } })
|
||||
const content = screen.getByTestId('scroll-area-content').element()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { SliderValueProps } from '@langgenius/dify-ui/slider'
|
||||
import { Slider, SliderValue } from '@langgenius/dify-ui/slider'
|
||||
import { expectTypeOf } from 'vite-plus/test'
|
||||
|
||||
function sliderTypeContracts() {
|
||||
const single = (
|
||||
<Slider
|
||||
defaultValue={0.9}
|
||||
onValueChange={(value) => {
|
||||
expectTypeOf(value).toEqualTypeOf<number>()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const initialRange: readonly [number, number] = [25, 75]
|
||||
const range = (
|
||||
<Slider
|
||||
defaultValue={initialRange}
|
||||
onValueChange={(value) => {
|
||||
expectTypeOf(value).toEqualTypeOf<readonly [number, number]>()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderValue: NonNullable<SliderValueProps['children']> = (formattedValues) =>
|
||||
formattedValues.join(' – ')
|
||||
const output = <SliderValue>{renderValue}</SliderValue>
|
||||
|
||||
// @ts-expect-error Slider only supports numeric values and ranges.
|
||||
const invalid = <Slider defaultValue="0.9" />
|
||||
|
||||
return [single, range, output, invalid]
|
||||
}
|
||||
|
||||
void sliderTypeContracts
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { SliderProps } from '.'
|
||||
import * as React from 'react'
|
||||
import { Slider, SliderControl, SliderIndicator, SliderLabel, SliderThumb, SliderTrack } from '.'
|
||||
import { expect } from 'storybook/test'
|
||||
import {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderLabel,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
SliderValue,
|
||||
} from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/Slider',
|
||||
@@ -43,21 +51,17 @@ function SliderDemo({
|
||||
defaultValue: _defaultValue,
|
||||
...args
|
||||
}: SliderProps<number>) {
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
return (
|
||||
<div className="w-[320px] space-y-3">
|
||||
<Slider {...args} value={value} onValueChange={setValue}>
|
||||
<SliderLabel className="sr-only">Demo slider</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb />
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
</Slider>
|
||||
<div className="text-center system-sm-medium text-text-secondary">{value}</div>
|
||||
</div>
|
||||
<Slider {...args} defaultValue={initialValue} className="w-80 flex-col gap-3">
|
||||
<SliderLabel className="sr-only">Demo slider</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb />
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
<SliderValue className="text-center system-sm-medium text-text-secondary" />
|
||||
</Slider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,6 +73,16 @@ export const Default: Story = {
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const slider = canvas.getByRole('slider', { name: 'Demo slider' })
|
||||
const output = canvas.getByRole('status')
|
||||
await expect(output).toHaveTextContent('50')
|
||||
await expect(output).toHaveAttribute('for', slider.id)
|
||||
await userEvent.tab()
|
||||
await expect(slider).toHaveFocus()
|
||||
await userEvent.keyboard('{ArrowRight}')
|
||||
await expect(output).toHaveTextContent('51')
|
||||
},
|
||||
}
|
||||
|
||||
export const Decimal: Story = {
|
||||
@@ -123,33 +137,30 @@ export const ComposedWithLabel: Story = {
|
||||
type PriceRange = readonly [number, number]
|
||||
|
||||
function RangeSliderDemo() {
|
||||
const [range, setRange] = React.useState<PriceRange>([25, 75])
|
||||
|
||||
return (
|
||||
<div className="w-[320px] space-y-3">
|
||||
<Slider<PriceRange>
|
||||
value={range}
|
||||
onValueChange={setRange}
|
||||
min={0}
|
||||
max={100}
|
||||
className="flex-col gap-1"
|
||||
>
|
||||
<SliderLabel>Price range</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb index={0} aria-label="Minimum price" />
|
||||
<SliderThumb index={1} aria-label="Maximum price" />
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
</Slider>
|
||||
<div className="text-center system-sm-medium text-text-secondary">
|
||||
{range[0]} – {range[1]}
|
||||
</div>
|
||||
</div>
|
||||
<Slider<PriceRange> defaultValue={[25, 75]} min={0} max={100} className="w-80 flex-col gap-1">
|
||||
<SliderLabel>Price range</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb index={0} aria-label="Minimum price" />
|
||||
<SliderThumb index={1} aria-label="Maximum price" />
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
<SliderValue className="mt-2 text-center system-sm-medium text-text-secondary" />
|
||||
</Slider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Range: Story = {
|
||||
render: () => <RangeSliderDemo />,
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const slider = canvas.getByRole('slider', { name: 'Minimum price' })
|
||||
const output = canvas.getByRole('status')
|
||||
await expect(output).toHaveTextContent('25 – 75')
|
||||
await userEvent.tab()
|
||||
await expect(slider).toHaveFocus()
|
||||
await userEvent.keyboard('{ArrowRight}')
|
||||
await expect(output).toHaveTextContent('26 – 75')
|
||||
},
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import { cn } from '../cn'
|
||||
import { formLabelClassName } from '../form-control-shared'
|
||||
import { resolveClassName } from '../internals/resolve-class-name'
|
||||
|
||||
type SliderValue = number | readonly number[]
|
||||
type SliderProps<Value extends SliderValue = SliderValue> = BaseSlider.Root.Props<Value>
|
||||
type SliderRootValue = NonNullable<BaseSlider.Root.Props['value']>
|
||||
type SliderProps<Value extends SliderRootValue = SliderRootValue> = BaseSlider.Root.Props<Value>
|
||||
|
||||
function Slider<Value extends SliderValue = SliderValue>({
|
||||
function Slider<Value extends SliderRootValue = SliderRootValue>({
|
||||
className,
|
||||
...props
|
||||
}: SliderProps<Value>) {
|
||||
@@ -37,6 +37,9 @@ function SliderLabel({ className, ...props }: SliderLabelProps) {
|
||||
)
|
||||
}
|
||||
|
||||
type SliderValueProps = BaseSlider.Value.Props
|
||||
const SliderValue = BaseSlider.Value
|
||||
|
||||
type SliderControlProps = BaseSlider.Control.Props
|
||||
|
||||
function SliderControl({ className, ...props }: SliderControlProps) {
|
||||
@@ -109,7 +112,15 @@ function SliderThumb({ className, ...props }: SliderThumbProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider, SliderControl, SliderIndicator, SliderLabel, SliderThumb, SliderTrack }
|
||||
export {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderLabel,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
SliderValue,
|
||||
}
|
||||
|
||||
export type {
|
||||
SliderControlProps,
|
||||
@@ -118,4 +129,5 @@ export type {
|
||||
SliderProps,
|
||||
SliderThumbProps,
|
||||
SliderTrackProps,
|
||||
SliderValueProps,
|
||||
}
|
||||
@@ -14,11 +14,6 @@ describe('Switch', () => {
|
||||
await expect.element(switchElement).toHaveAttribute('data-disabled', '')
|
||||
})
|
||||
|
||||
it('should apply custom className', async () => {
|
||||
const screen = await render(<Switch checked={false} className="custom-test-class" />)
|
||||
await expect.element(screen.getByRole('switch')).toHaveClass('custom-test-class')
|
||||
})
|
||||
|
||||
it('should reflect checked state on the root and thumb', async () => {
|
||||
const screen = await render(<Switch checked={false} />)
|
||||
const switchElement = screen.getByRole('switch').element()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '../index'
|
||||
|
||||
describe('Tabs wrappers', () => {
|
||||
it('forwards className to composable parts', async () => {
|
||||
const screen = await render(
|
||||
<Tabs defaultValue="first">
|
||||
<TabsList className="custom-list">
|
||||
<TabsTab value="first" className="custom-tab">
|
||||
First
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel value="first" className="custom-panel">
|
||||
Panel
|
||||
</TabsPanel>
|
||||
</Tabs>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('tablist')).toHaveClass('custom-list')
|
||||
await expect.element(screen.getByRole('tab', { name: 'First' })).toHaveClass('custom-tab')
|
||||
expect(screen.getByText('Panel').element()).toHaveClass('custom-panel')
|
||||
})
|
||||
})
|
||||
+11
-18
@@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { toast } from '@/app/notifications'
|
||||
import ConfigParamModal from '../config-param-modal'
|
||||
|
||||
@@ -46,18 +47,6 @@ vi.mock('@/config', () => ({
|
||||
ANNOTATION_DEFAULT: { score_threshold: 0.9 },
|
||||
}))
|
||||
|
||||
vi.mock('../score-slider', () => ({
|
||||
default: ({ value, onChange }: { value: number; onChange: (value: number) => void }) => (
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number((e.target as HTMLInputElement).value))}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
const defaultAnnotationConfig = {
|
||||
id: 'test-id',
|
||||
enabled: false,
|
||||
@@ -182,7 +171,7 @@ describe('ConfigParamModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('slider')).toHaveValue('90')
|
||||
expect(screen.getByRole('slider')).toHaveValue('0.9')
|
||||
})
|
||||
|
||||
it('should render configConfirmBtn when isInit is false', () => {
|
||||
@@ -287,8 +276,8 @@ describe('ConfigParamModal', () => {
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('min', '0')
|
||||
expect(slider).toHaveAttribute('max', '100')
|
||||
expect(slider).toHaveValue('90')
|
||||
expect(slider).toHaveAttribute('max', '1')
|
||||
expect(slider).toHaveValue('0.9')
|
||||
})
|
||||
|
||||
it('should update embedding model when model selector is used', () => {
|
||||
@@ -404,7 +393,7 @@ describe('ConfigParamModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('slider')).toHaveValue('90')
|
||||
expect(screen.getByRole('slider')).toHaveValue('0.9')
|
||||
})
|
||||
|
||||
it('should preserve zero score threshold instead of falling back to default', async () => {
|
||||
@@ -473,6 +462,7 @@ describe('ConfigParamModal', () => {
|
||||
})
|
||||
|
||||
it('should save updated score after slider changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSave = vi.fn().mockResolvedValue(undefined)
|
||||
render(
|
||||
<ConfigParamModal
|
||||
@@ -484,7 +474,10 @@ describe('ConfigParamModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByRole('slider'), { target: { value: '96' } })
|
||||
const slider = screen.getByRole('slider')
|
||||
await user.click(slider)
|
||||
await user.keyboard('{ArrowRight}')
|
||||
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('0.91'))
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const saveBtn = buttons.find((b) => b.textContent?.includes('initSetup'))
|
||||
@@ -493,7 +486,7 @@ describe('ConfigParamModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ embedding_provider_name: 'openai' }),
|
||||
0.96,
|
||||
0.91,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+2
-2
@@ -99,11 +99,11 @@ const ConfigParamModal: FC<Props> = ({
|
||||
>
|
||||
<ScoreSlider
|
||||
className="mt-1"
|
||||
value={(annotationConfig.score_threshold ?? ANNOTATION_DEFAULT.score_threshold) * 100}
|
||||
value={annotationConfig.score_threshold ?? ANNOTATION_DEFAULT.score_threshold}
|
||||
onChange={(val) => {
|
||||
setAnnotationConfig({
|
||||
...annotationConfig,
|
||||
score_threshold: val / 100,
|
||||
score_threshold: val,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
+34
-5
@@ -1,15 +1,44 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ScoreSlider from '..'
|
||||
|
||||
describe('ScoreSlider', () => {
|
||||
it('should expose the business score instead of the internal percentage', () => {
|
||||
render(<ScoreSlider value={90} onChange={vi.fn()} />)
|
||||
it('should display and update the score with two decimal places', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
const { rerender } = render(<ScoreSlider value={0.9} onChange={onChange} />)
|
||||
|
||||
const slider = screen.getByRole('slider', {
|
||||
name: 'appDebug.feature.annotation.scoreThreshold.title',
|
||||
})
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '90')
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '0.9')
|
||||
expect(slider).toHaveAttribute('aria-valuetext', '0.90')
|
||||
expect(screen.getByText('0.90')).toBeInTheDocument()
|
||||
expect(screen.getByRole('status')).toHaveTextContent('0.90')
|
||||
expect(screen.getByRole('status')).toHaveAttribute('for', slider.id)
|
||||
act(() => slider.focus())
|
||||
await user.keyboard('{ArrowRight}')
|
||||
expect(onChange).toHaveBeenCalledWith(0.91, expect.anything())
|
||||
rerender(<ScoreSlider value={0.91} onChange={onChange} />)
|
||||
expect(screen.getByRole('status')).toHaveTextContent('0.91')
|
||||
expect(slider).toHaveAttribute('aria-valuetext', '0.91')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['{PageUp}', 0.6],
|
||||
['{PageDown}', 0.4],
|
||||
['{Shift>}{ArrowRight}{/Shift}', 0.6],
|
||||
['{Shift>}{ArrowLeft}{/Shift}', 0.4],
|
||||
])('should adjust the score by 0.10 with %s', async (key, expectedValue) => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<ScoreSlider value={0.5} onChange={onChange} />)
|
||||
|
||||
const slider = screen.getByRole('slider', {
|
||||
name: 'appDebug.feature.annotation.scoreThreshold.title',
|
||||
})
|
||||
act(() => slider.focus())
|
||||
await user.keyboard(key)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(expectedValue, expect.anything())
|
||||
})
|
||||
})
|
||||
+24
-32
@@ -7,6 +7,7 @@ import {
|
||||
SliderLabel,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
SliderValue,
|
||||
} from '@langgenius/dify-ui/slider'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -24,7 +25,7 @@ const clamp = (value: number, min: number, max: number) => {
|
||||
}
|
||||
|
||||
const SCORE_MIN = 0
|
||||
const SCORE_MAX = 100
|
||||
const SCORE_MAX = 1
|
||||
|
||||
const ScoreSlider: FC<Props> = ({ className, value, onChange }) => {
|
||||
const { t } = useTranslation(['appDebug'])
|
||||
@@ -32,37 +33,28 @@ const ScoreSlider: FC<Props> = ({ className, value, onChange }) => {
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="relative mt-3.5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
value={safeValue}
|
||||
min={SCORE_MIN}
|
||||
max={SCORE_MAX}
|
||||
step={1}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<SliderLabel className="sr-only">
|
||||
{t(($) => $['feature.annotation.scoreThreshold.title'], { ns: 'appDebug' })}
|
||||
</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb
|
||||
getAriaValueText={(_formattedValue, sliderValue) => (sliderValue / 100).toFixed(2)}
|
||||
/>
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
</Slider>
|
||||
<div
|
||||
className="pointer-events-none absolute -top-4 system-sm-semibold text-text-primary"
|
||||
style={{
|
||||
left: `calc(4px + ${safeValue / SCORE_MAX} * (100% - 8px))`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{(safeValue / 100).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
className="mt-3.5 flex w-full"
|
||||
value={safeValue}
|
||||
min={SCORE_MIN}
|
||||
max={SCORE_MAX}
|
||||
step={0.01}
|
||||
largeStep={0.1}
|
||||
format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<SliderLabel className="sr-only">
|
||||
{t(($) => $['feature.annotation.scoreThreshold.title'], { ns: 'appDebug' })}
|
||||
</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
<SliderThumb>
|
||||
<SliderValue className="pointer-events-none absolute bottom-full left-1/2 mb-1.5 -translate-x-1/2 system-sm-semibold whitespace-nowrap text-text-primary" />
|
||||
</SliderThumb>
|
||||
</SliderTrack>
|
||||
</SliderControl>
|
||||
</Slider>
|
||||
<div className="mt-2.5 flex items-center justify-between system-xs-semibold-uppercase">
|
||||
<div className="flex space-x-1 text-util-colors-cyan-cyan-500">
|
||||
<div>0.0</div>
|
||||
|
||||
+5
-1
@@ -7,14 +7,18 @@ describe('trigger-schedule/on-minute-selector', () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<OnMinuteSelector value={15} onChange={onChange} />)
|
||||
const { rerender } = render(<OnMinuteSelector value={15} onChange={onChange} />)
|
||||
|
||||
const slider = screen.getByRole('slider', {
|
||||
name: 'workflowIntegrations.nodes.triggerSchedule.onMinute',
|
||||
})
|
||||
expect(screen.getByRole('status')).toHaveTextContent('15')
|
||||
expect(screen.getByRole('status')).toHaveAttribute('for', slider.id)
|
||||
slider.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(16, expect.objectContaining({ activeThumbIndex: 0 }))
|
||||
rerender(<OnMinuteSelector value={16} onChange={onChange} />)
|
||||
expect(screen.getByRole('status')).toHaveTextContent('16')
|
||||
})
|
||||
})
|
||||
+2
-3
@@ -5,6 +5,7 @@ import {
|
||||
SliderLabel,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
SliderValue,
|
||||
} from '@langgenius/dify-ui/slider'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -30,9 +31,7 @@ const OnMinuteSelector = ({ value = 0, onChange }: OnMinuteSelectorProps) => {
|
||||
{t(($) => $['nodes.triggerSchedule.onMinute'], { ns: 'workflowIntegrations' })}
|
||||
</SliderLabel>
|
||||
<div className="relative flex h-8 items-center rounded-lg bg-components-input-bg-normal">
|
||||
<div className="flex h-full w-12 shrink-0 items-center justify-center text-[13px] text-components-input-text-filled">
|
||||
{value}
|
||||
</div>
|
||||
<SliderValue className="flex h-full w-12 shrink-0 items-center justify-center text-[13px] text-components-input-text-filled" />
|
||||
<div className="absolute top-0 left-12 h-full w-px bg-components-panel-bg"></div>
|
||||
<div className="flex h-full grow items-center pr-3 pl-4">
|
||||
<SliderControl>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
|
||||
vi.mock('react-i18next', () =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { useOAuthCallback } from '../use-oauth'
|
||||
|
||||
const originalOpener = window.opener
|
||||
|
||||
Reference in new issue
Block a user