mirror of
https://github.com/langgenius/dify.git
synced 2026-05-24 10:57:52 +08:00
Compare commits
3 Commits
main
...
codex/refi
| Author | SHA1 | Date | |
|---|---|---|---|
| c76599327c | |||
| a78b664841 | |||
| 6bf8758807 |
@ -1,6 +1,6 @@
|
||||
from typing import Any, Union
|
||||
from typing import Union
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.rag.entities import RerankingModelConfig, WeightedScoreConfig
|
||||
from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict
|
||||
@ -101,14 +101,3 @@ class KnowledgeIndexNodeData(BaseNodeData):
|
||||
index_chunk_variable_selector: list[str]
|
||||
indexing_technique: str | None = None
|
||||
summary_index_setting: SummaryIndexSettingDict | None = None
|
||||
|
||||
@field_validator("summary_index_setting", mode="before")
|
||||
@classmethod
|
||||
def normalize_summary_index_setting(cls, v: Any) -> Any:
|
||||
"""Treat dicts with enable=None (or missing enable) as None (#36233)."""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, dict):
|
||||
if v.get("enable") is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
@ -207,6 +207,16 @@ describe('Select wrappers', () => {
|
||||
|
||||
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-popup-open:bg-state-base-hover-alt')
|
||||
})
|
||||
|
||||
it('should include keyboard focus ring classes', async () => {
|
||||
const screen = await renderOpenSelect()
|
||||
|
||||
await expect.element(screen.getByRole('combobox', { name: 'city select' })).toHaveClass(
|
||||
'focus-visible:ring-1',
|
||||
'focus-visible:ring-components-input-border-active',
|
||||
'focus-visible:ring-inset',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SelectContent', () => {
|
||||
|
||||
@ -24,6 +24,7 @@ const selectTriggerVariants = cva(
|
||||
[
|
||||
'group 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-popup-open:bg-state-base-hover-alt',
|
||||
'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
|
||||
'data-placeholder:text-components-input-text-placeholder',
|
||||
'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',
|
||||
|
||||
@ -49,6 +49,19 @@ describe('Switch', () => {
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should work in uncontrolled mode with defaultChecked prop', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch defaultChecked={false} onCheckedChange={onCheckedChange} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
asHTMLElement(switchElement.element()).click()
|
||||
|
||||
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should not call onCheckedChange when disabled', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch checked={false} disabled onCheckedChange={onCheckedChange} />)
|
||||
@ -142,6 +155,24 @@ describe('Switch', () => {
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use checked data attributes to position spinner', async () => {
|
||||
const screen = await render(<Switch checked={false} loading size="md" />)
|
||||
const spinner = screen.container.querySelector('span[aria-hidden="true"]')
|
||||
|
||||
expect(spinner).toHaveClass(
|
||||
'left-[calc(50%+6px)]',
|
||||
'group-data-checked:left-[calc(50%-6px)]',
|
||||
)
|
||||
|
||||
await screen.rerender(<Switch checked={true} loading size="md" />)
|
||||
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"]')).toHaveClass(
|
||||
'left-[calc(50%+6px)]',
|
||||
'group-data-checked:left-[calc(50%-6px)]',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not show spinner for xs and sm sizes', async () => {
|
||||
const screen = await render(<Switch checked={false} loading size="xs" />)
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
|
||||
|
||||
@ -2,6 +2,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState, useTransition } from 'react'
|
||||
import { Switch, SwitchSkeleton } from '.'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../field'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/Switch',
|
||||
@ -10,7 +15,7 @@ const meta = {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Toggle switch built on Base UI with CVA variants, Figma-aligned design tokens, loading spinner, and skeleton placeholder. Import `Switch` and `SwitchSkeleton` from `@langgenius/dify-ui/switch`.',
|
||||
component: 'Toggle switch primitive with controlled and uncontrolled state support, loading state, and skeleton placeholder.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -42,20 +47,27 @@ const meta = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const SwitchDemo = (args: Partial<ComponentProps<typeof Switch>>) => {
|
||||
type SwitchDemoProps = Partial<Omit<ComponentProps<typeof Switch>, 'checked' | 'defaultChecked' | 'onCheckedChange'>> & {
|
||||
checked?: boolean
|
||||
}
|
||||
|
||||
const SwitchDemo = (args: SwitchDemoProps) => {
|
||||
const [enabled, setEnabled] = useState(args.checked ?? false)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Switch
|
||||
{...args}
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{enabled ? 'On' : 'Off'}
|
||||
</span>
|
||||
</div>
|
||||
<FieldRoot name="autoRetry" className="w-72">
|
||||
<FieldLabel className="flex items-center justify-between gap-3">
|
||||
<span>Enable auto retry</span>
|
||||
<Switch
|
||||
{...args}
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{enabled ? 'Failures will retry automatically.' : 'Failures require manual retry.'}
|
||||
</FieldDescription>
|
||||
</FieldRoot>
|
||||
)
|
||||
}
|
||||
|
||||
@ -116,24 +128,24 @@ const AllStatesDemo = () => {
|
||||
<td className="py-3 font-medium text-gray-900">{size}</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} onCheckedChange={() => {}} />
|
||||
<Switch size={size} checked={true} onCheckedChange={() => {}} />
|
||||
<Switch size={size} checked={false} onCheckedChange={() => {}} aria-label={`${size} unchecked switch`} />
|
||||
<Switch size={size} checked={true} onCheckedChange={() => {}} aria-label={`${size} checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} disabled />
|
||||
<Switch size={size} checked={true} disabled />
|
||||
<Switch size={size} checked={false} disabled aria-label={`${size} disabled unchecked switch`} />
|
||||
<Switch size={size} checked={true} disabled aria-label={`${size} disabled checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} loading />
|
||||
<Switch size={size} checked={true} loading />
|
||||
<Switch size={size} checked={false} loading aria-label={`${size} loading unchecked switch`} />
|
||||
<Switch size={size} checked={true} loading aria-label={`${size} loading checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<SwitchSkeleton size={size} />
|
||||
<SwitchSkeleton size={size} aria-hidden="true" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@ -148,7 +160,7 @@ export const AllStates: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Complete variant matrix: all sizes × all states, matching Figma design spec (node 2144:1210).',
|
||||
story: 'Variant matrix for switch sizes and states.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -164,22 +176,30 @@ const SizeComparisonDemo = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
|
||||
<span className="text-sm text-gray-700">Extra Small (xs) — 14×10</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
|
||||
<span className="text-sm text-gray-700">Small (sm) — 20×12</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
|
||||
<span className="text-sm text-gray-700">Regular (md) — 28×16</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
|
||||
<span className="text-sm text-gray-700">Large (lg) — 36×20</span>
|
||||
</div>
|
||||
<FieldRoot name="extraSmallSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
|
||||
Extra Small (xs) - 14x10
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="smallSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
|
||||
Small (sm) - 20x12
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
|
||||
Regular (md) - 28x16
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="largeSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
|
||||
Large (lg) - 36x20
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -200,30 +220,42 @@ const LoadingDemo = () => {
|
||||
{loading ? 'Stop Loading' : 'Start Loading'}
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Large unchecked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={true} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Large checked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Regular unchecked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={true} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Regular checked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Small (no spinner)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Extra Small (no spinner)</span>
|
||||
</div>
|
||||
<FieldRoot name="largeUncheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={false} loading={loading} />
|
||||
Large unchecked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="largeCheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={true} loading={loading} />
|
||||
Large checked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularUncheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={false} loading={loading} />
|
||||
Regular unchecked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularCheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={true} loading={loading} />
|
||||
Regular checked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="smallLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={false} loading={loading} />
|
||||
Small
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="extraSmallLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={false} loading={loading} />
|
||||
Extra Small
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -234,7 +266,7 @@ export const Loading: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Loading state disables interaction and shows a spinning icon (i-ri-loader-2-line) for md/lg sizes. Spinner position mirrors the knob: appears on the opposite side of the checked state.',
|
||||
story: 'Loading state disables interaction and shows a spinner for md and lg sizes.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -242,61 +274,76 @@ export const Loading: Story = {
|
||||
|
||||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const MutationLoadingDemo = () => {
|
||||
function useMockAutoRetrySettingQuery() {
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
|
||||
return {
|
||||
data: {
|
||||
enabled,
|
||||
},
|
||||
setData: setEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
function useMockUpdateAutoRetrySettingMutation({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess: (enabled: boolean) => void
|
||||
}) {
|
||||
const [requestCount, setRequestCount] = useState(0)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleChange = (nextValue: boolean) => {
|
||||
const mutate = (nextValue: boolean) => {
|
||||
if (isPending)
|
||||
return
|
||||
|
||||
startTransition(async () => {
|
||||
setRequestCount(current => current + 1)
|
||||
await wait(1200)
|
||||
setEnabled(nextValue)
|
||||
onSuccess(nextValue)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
requestCount,
|
||||
isPending,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
const MutationLoadingDemo = () => {
|
||||
const autoRetrySetting = useMockAutoRetrySettingQuery()
|
||||
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
|
||||
onSuccess: autoRetrySetting.setData,
|
||||
})
|
||||
const statusText = updateAutoRetrySetting.isPending
|
||||
? 'Saving changes...'
|
||||
: autoRetrySetting.data.enabled
|
||||
? 'Auto retry is enabled.'
|
||||
: 'Auto retry is disabled.'
|
||||
|
||||
return (
|
||||
<div className="w-[340px] space-y-4 rounded-2xl border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-text-primary">Mutation Loading Guard</p>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
Click once to start a simulated mutate call. While the request is pending, the switch enters
|
||||
{' '}
|
||||
<code className="rounded-sm bg-state-base-hover px-1 py-0.5 text-[11px]">loading</code>
|
||||
{' '}
|
||||
and rejects duplicate clicks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid w-90 gap-3 rounded-lg border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
|
||||
<FieldRoot name="autoRetry">
|
||||
<FieldLabel className="flex items-center justify-between gap-4">
|
||||
<span className="system-sm-medium text-text-secondary">Enable auto retry</span>
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={autoRetrySetting.data.enabled}
|
||||
loading={updateAutoRetrySetting.isPending}
|
||||
onCheckedChange={updateAutoRetrySetting.mutate}
|
||||
/>
|
||||
</FieldLabel>
|
||||
<FieldDescription>Retry failed workflow runs without manual intervention.</FieldDescription>
|
||||
</FieldRoot>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-components-panel-border-subtle bg-background-default-dodge px-3 py-2 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-text-primary">Enable Auto Retry</p>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{isPending ? 'Saving…' : enabled ? 'Saved as on' : 'Saved as off'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={enabled}
|
||||
loading={isPending}
|
||||
onCheckedChange={handleChange}
|
||||
aria-label="Enable Auto Retry"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-text-tertiary">
|
||||
<div className="rounded-lg bg-state-base-hover px-3 py-2">
|
||||
<div className="font-medium text-text-secondary">Committed Value</div>
|
||||
<div>{enabled ? 'On' : 'Off'}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-state-base-hover px-3 py-2">
|
||||
<div className="font-medium text-text-secondary">Mutate Count</div>
|
||||
<div>{requestCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-text-tertiary" aria-live="polite">
|
||||
{statusText}
|
||||
{' '}
|
||||
Save attempts:
|
||||
{' '}
|
||||
{updateAutoRetrySetting.requestCount}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -306,7 +353,7 @@ export const MutationLoadingGuard: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a controlled switch backed by an async mutate call. The component keeps its previous committed value, sets `loading` during the request, and blocks duplicate clicks until the mutation resolves.',
|
||||
story: 'Controlled switch that enters loading while the change is saved.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -315,19 +362,19 @@ export const MutationLoadingGuard: Story = {
|
||||
const SkeletonDemo = () => (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="xs" />
|
||||
<SwitchSkeleton size="xs" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Extra Small skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="sm" />
|
||||
<SwitchSkeleton size="sm" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Small skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="md" />
|
||||
<SwitchSkeleton size="md" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Regular skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="lg" />
|
||||
<SwitchSkeleton size="lg" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Large skeleton</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -338,7 +385,7 @@ export const Skeleton: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '`SwitchSkeleton` renders a non-interactive placeholder with `bg-text-quaternary opacity-20`. Exported from `@langgenius/dify-ui/switch` alongside `Switch`.',
|
||||
story: 'Non-interactive placeholders for switch loading layouts.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -45,26 +45,34 @@ const switchThumbVariants = cva(
|
||||
|
||||
export type SwitchSize = NonNullable<VariantProps<typeof switchRootVariants>['size']>
|
||||
|
||||
const spinnerSizeConfig: Partial<Record<SwitchSize, {
|
||||
icon: string
|
||||
uncheckedPosition: string
|
||||
checkedPosition: string
|
||||
}>> = {
|
||||
md: {
|
||||
icon: 'size-2',
|
||||
uncheckedPosition: 'left-[calc(50%+6px)]',
|
||||
checkedPosition: 'left-[calc(50%-6px)]',
|
||||
},
|
||||
lg: {
|
||||
icon: 'size-2.5',
|
||||
uncheckedPosition: 'left-[calc(50%+8px)]',
|
||||
checkedPosition: 'left-[calc(50%-8px)]',
|
||||
const switchSpinnerVariants = cva(
|
||||
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
md: 'size-2 left-[calc(50%+6px)] group-data-checked:left-[calc(50%-6px)]',
|
||||
lg: 'size-2.5 left-[calc(50%+8px)] group-data-checked:left-[calc(50%-8px)]',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type ControlledSwitchProps = {
|
||||
checked: boolean
|
||||
defaultChecked?: never
|
||||
}
|
||||
|
||||
type UncontrolledSwitchProps = {
|
||||
checked?: never
|
||||
defaultChecked?: boolean
|
||||
}
|
||||
|
||||
type SwitchControlProps = ControlledSwitchProps | UncontrolledSwitchProps
|
||||
|
||||
export type SwitchProps
|
||||
= Omit<BaseSwitchNS.Root.Props, 'className' | 'size' | 'onCheckedChange'>
|
||||
= Omit<BaseSwitchNS.Root.Props, 'checked' | 'defaultChecked' | 'className' | 'size' | 'onCheckedChange'>
|
||||
& VariantProps<typeof switchRootVariants>
|
||||
& SwitchControlProps
|
||||
& {
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
loading?: boolean
|
||||
@ -81,7 +89,6 @@ export function Switch({
|
||||
...props
|
||||
}: SwitchProps) {
|
||||
const isDisabled = disabled || loading
|
||||
const spinner = loading && size ? spinnerSizeConfig[size] : undefined
|
||||
|
||||
return (
|
||||
<BaseSwitch.Root
|
||||
@ -95,14 +102,10 @@ export function Switch({
|
||||
<BaseSwitch.Thumb
|
||||
className={switchThumbVariants({ size })}
|
||||
/>
|
||||
{spinner
|
||||
{loading && (size === 'md' || size === 'lg')
|
||||
? (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
spinner.icon,
|
||||
checked ? spinner.checkedPosition : spinner.uncheckedPosition,
|
||||
)}
|
||||
className={switchSpinnerVariants({ size })}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<i className="i-ri-loader-2-line size-full animate-spin text-text-tertiary motion-reduce:animate-none" />
|
||||
@ -131,11 +134,8 @@ const switchSkeletonVariants = cva(
|
||||
)
|
||||
|
||||
export type SwitchSkeletonProps
|
||||
= Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
||||
= HTMLAttributes<HTMLDivElement>
|
||||
& VariantProps<typeof switchSkeletonVariants>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SwitchSkeleton({
|
||||
size = 'md',
|
||||
|
||||
@ -41,6 +41,7 @@ describe('@langgenius/dify-ui/toast', () => {
|
||||
await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveAttribute('aria-live', 'polite')
|
||||
await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveClass('z-60')
|
||||
expect(screen.getByRole('region', { name: 'Notifications' }).element().firstElementChild).toHaveClass('top-4')
|
||||
expect(screen.getByText('Saved').element().closest('[class*="transition-opacity"]')).toHaveClass('motion-reduce:transition-none')
|
||||
expect(screen.getByRole('dialog').element()).not.toHaveClass('outline-hidden')
|
||||
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
|
||||
expect(document.body.querySelector('button[aria-label="Close notification"][aria-hidden="true"]')).toBeInTheDocument()
|
||||
|
||||
@ -171,7 +171,7 @@ function ToastCard({
|
||||
aria-hidden="true"
|
||||
className={cn('absolute -inset-px bg-linear-to-r opacity-40', getToneGradientClasses(toastType))}
|
||||
/>
|
||||
<BaseToast.Content className="relative flex items-start gap-1 overflow-hidden p-3 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100">
|
||||
<BaseToast.Content className="relative flex items-start gap-1 overflow-hidden p-3 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100 motion-reduce:transition-none">
|
||||
<div className="flex shrink-0 items-center justify-center p-0.5">
|
||||
<ToastIcon type={toastType} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user