chore(dify-ui): align authoring rules and dependency optimization (#43233)

This commit is contained in:
yyh authored and GitHub committed 2026-09-29 11:58:49 +00:00
1 parent 3f0032f20b
commit 0ed392fb62
38 files changed
+359 -316

No files matched your search

+14
View File
@@ -1193,15 +1193,29 @@ export const lintConfig = {
{ {
files: ['packages/dify-ui/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'], files: ['packages/dify-ui/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
rules: { rules: {
'func-names': ['error', 'as-needed'],
'id-denylist': ['error', 'e'],
'unicorn/import-style': [ 'unicorn/import-style': [
'error', 'error',
{ {
extendDefaultStyles: false, extendDefaultStyles: false,
styles: { styles: {
react: { namespace: true }, react: { namespace: true },
'react-dom': { namespace: true },
'react-dom/client': { namespace: true },
'react-dom/server': { namespace: true },
}, },
}, },
], ],
'react/function-component-definition': [
'error',
{
namedComponents: ['function-declaration', 'function-expression'],
unnamedComponents: ['arrow-function', 'function-expression'],
},
],
'react/jsx-fragments': ['error', 'element'],
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'react/exhaustive-deps': [ 'react/exhaustive-deps': [
'error', 'error',
{ {
+9
View File
@@ -13,6 +13,15 @@ Use React namespace imports throughout this package, including stories and tests
import also covers React types; do not add separate named type imports. JSX alone does not import also covers React types; do not add separate named type imports. JSX alone does not
require an explicit React import. require an explicit React import.
Use namespace imports for `react-dom`, `react-dom/client`, and `react-dom/server` as well.
Write grouping fragments as `<React.Fragment>` and remove unnecessary fragment wrappers.
## Component writing style
Define named components with function declarations or function expressions. Arrow functions
remain appropriate for event handlers, render props, and Storybook render callbacks. Name function
expressions when JavaScript cannot infer their name. Use `event` or `error` instead of `e`.
## Subpaths and names ## Subpaths and names
Every public primitive needs a matching `package.json#exports` subpath. Import relatively between Every public primitive needs a matching `package.json#exports` subpath. Import relatively between
@@ -104,7 +104,7 @@ export const NonDestructive: Story = {
), ),
} }
const ControlledDemo = () => { function ControlledDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const [count, setCount] = React.useState(0) const [count, setCount] = React.useState(0)
@@ -149,7 +149,7 @@ export const Controlled: Story = {
render: () => <ControlledDemo />, render: () => <ControlledDemo />,
} }
const LoadingConfirmDemo = () => { function LoadingConfirmDemo() {
const [pending, setPending] = React.useState(false) const [pending, setPending] = React.useState(false)
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const confirmLabelId = React.useId() const confirmLabelId = React.useId()
@@ -293,27 +293,29 @@ async function searchSuggestions(
} }
} }
const SuggestionItem = ({ item, dense }: { item: Suggestion; dense?: boolean }) => ( function SuggestionItem({ item, dense }: { item: Suggestion; dense?: boolean }) {
<AutocompleteItem value={item}> return (
{item.icon && ( <AutocompleteItem value={item}>
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" /> {item.icon && (
)} <span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
<div className="flex min-w-0 grow flex-col">
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
{!dense && item.description && (
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
)} )}
</div> <div className="flex min-w-0 grow flex-col">
{item.meta && ( <AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary"> {!dense && item.description && (
{item.meta} <span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
</span> )}
)} </div>
</AutocompleteItem> {item.meta && (
) <span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
{item.meta}
</span>
)}
</AutocompleteItem>
)
}
// Only virtualized items receive an explicit index; ordinary lists must let Base UI register items by DOM order for keyboard navigation. // Only virtualized items receive an explicit index; ordinary lists must let Base UI register items by DOM order for keyboard navigation.
const VirtualizedSuggestionItem = ({ function VirtualizedSuggestionItem({
item, item,
index, index,
dense, dense,
@@ -321,70 +323,76 @@ const VirtualizedSuggestionItem = ({
item: Suggestion item: Suggestion
index: number index: number
dense?: boolean dense?: boolean
}) => ( }) {
<AutocompleteItem value={item} index={index}> return (
{item.icon && ( <AutocompleteItem value={item} index={index}>
<span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" /> {item.icon && (
)} <span className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')} aria-hidden="true" />
<div className="flex min-w-0 grow flex-col">
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
{!dense && item.description && (
<span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
)} )}
</div> <div className="flex min-w-0 grow flex-col">
{item.meta && ( <AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
<span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary"> {!dense && item.description && (
{item.meta} <span className="truncate system-xs-regular text-text-tertiary">{item.description}</span>
</span> )}
)} </div>
</AutocompleteItem> {item.meta && (
) <span className="shrink-0 rounded-md bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium text-text-tertiary">
{item.meta}
</span>
)}
</AutocompleteItem>
)
}
const TagSuggestionItem = ({ item }: { item: Suggestion }) => ( function TagSuggestionItem({ item }: { item: Suggestion }) {
<AutocompleteItem value={item}> return (
<AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText> <AutocompleteItem value={item}>
{item.description && ( <AutocompleteItemText className="px-0">{item.label}</AutocompleteItemText>
<span className="ml-auto max-w-36 truncate system-xs-regular text-text-tertiary"> {item.description && (
{item.description} <span className="ml-auto max-w-36 truncate system-xs-regular text-text-tertiary">
</span> {item.description}
)} </span>
</AutocompleteItem> )}
) </AutocompleteItem>
)
}
const BasicTagAutocomplete = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => ( function BasicTagAutocomplete({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
<Autocomplete return (
items={tagSuggestions} <Autocomplete
itemToStringValue={getSuggestionLabel} items={tagSuggestions}
mode="list" itemToStringValue={getSuggestionLabel}
openOnInputClick mode="list"
> openOnInputClick
<AutocompleteInputGroup size={size}> >
<span <AutocompleteInputGroup size={size}>
className="ml-2 i-ri-search-line size-4 shrink-0 text-text-tertiary" <span
aria-hidden="true" className="ml-2 i-ri-search-line size-4 shrink-0 text-text-tertiary"
/> aria-hidden="true"
<AutocompleteInput />
size={size} <AutocompleteInput
placeholder="Search tags or type a new one…" size={size}
aria-label="Search tags or type a new one" placeholder="Search tags or type a new one…"
/> aria-label="Search tags or type a new one"
<AutocompleteClear size={size} /> />
<AutocompleteTrigger size={size} /> <AutocompleteClear size={size} />
</AutocompleteInputGroup> <AutocompleteTrigger size={size} />
<AutocompletePortal> </AutocompleteInputGroup>
<AutocompletePositioner> <AutocompletePortal>
<AutocompletePopup> <AutocompletePositioner>
<AutocompleteList<Suggestion>> <AutocompletePopup>
{(item) => <TagSuggestionItem key={item.value} item={item} />} <AutocompleteList<Suggestion>>
</AutocompleteList> {(item) => <TagSuggestionItem key={item.value} item={item} />}
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty> </AutocompleteList>
</AutocompletePopup> <AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
</AutocompletePositioner> </AutocompletePopup>
</AutocompletePortal> </AutocompletePositioner>
</Autocomplete> </AutocompletePortal>
) </Autocomplete>
)
}
const GroupedSuggestionList = () => { function GroupedSuggestionList() {
const groups = useAutocompleteFilteredItems<SuggestionGroup>() const groups = useAutocompleteFilteredItems<SuggestionGroup>()
return ( return (
@@ -402,7 +410,7 @@ const GroupedSuggestionList = () => {
) )
} }
const CommandPaletteList = () => { function CommandPaletteList() {
const groups = useAutocompleteFilteredItems<SuggestionGroup>() const groups = useAutocompleteFilteredItems<SuggestionGroup>()
return ( return (
@@ -450,7 +458,7 @@ const LimitedStatus = ({ total }: { total: number }) => {
: `${items.length} suggestions available.` : `${items.length} suggestions available.`
} }
const AsyncSearchDemo = () => { function AsyncSearchDemo() {
const [searchValue, setSearchValue] = React.useState('') const [searchValue, setSearchValue] = React.useState('')
const [searchResults, setSearchResults] = React.useState<Suggestion[]>([]) const [searchResults, setSearchResults] = React.useState<Suggestion[]>([])
const [error, setError] = React.useState<string | null>(null) const [error, setError] = React.useState<string | null>(null)
@@ -532,11 +540,11 @@ const AsyncSearchDemo = () => {
) )
} }
const VirtualizedSuggestionList = ({ function VirtualizedSuggestionList({
virtualizerRef, virtualizerRef,
}: { }: {
virtualizerRef: React.RefObject<StoryVirtualizer | null> virtualizerRef: React.RefObject<StoryVirtualizer | null>
}) => { }) {
const scrollRef = React.useRef<HTMLDivElement | null>(null) const scrollRef = React.useRef<HTMLDivElement | null>(null)
const filteredItems = useAutocompleteFilteredItems<Suggestion>() const filteredItems = useAutocompleteFilteredItems<Suggestion>()
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
@@ -586,7 +594,7 @@ const VirtualizedSuggestionList = ({
) )
} }
const VirtualizedStatus = () => { function VirtualizedStatus() {
const filteredItems = useAutocompleteFilteredItems<Suggestion>() const filteredItems = useAutocompleteFilteredItems<Suggestion>()
return ( return (
@@ -596,7 +604,7 @@ const VirtualizedStatus = () => {
) )
} }
const FuzzyHighlight = ({ text, query }: { text: string; query: string }) => { function FuzzyHighlight({ text, query }: { text: string; query: string }) {
const parts = React.useMemo(() => { const parts = React.useMemo(() => {
const trimmed = query.trim() const trimmed = query.trim()
@@ -622,7 +630,7 @@ const FuzzyHighlight = ({ text, query }: { text: string; query: string }) => {
) )
} }
const FuzzyMatchingDemo = () => { function FuzzyMatchingDemo() {
const [value, setValue] = React.useState('retr') const [value, setValue] = React.useState('retr')
const { contains } = useAutocompleteFilter({ sensitivity: 'base' }) const { contains } = useAutocompleteFilter({ sensitivity: 'base' })
@@ -863,7 +871,7 @@ export const CommandPalette: Story = {
), ),
} }
const VirtualizedLongSuggestionsDemo = () => { function VirtualizedLongSuggestionsDemo() {
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null) const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
return ( return (
+1 -1
View File
@@ -74,7 +74,7 @@ function AvatarImage({ className, ...props }: AvatarImageProps) {
) )
} }
const Avatar = ({ name, avatar, size = 'md', className, onLoadingStatusChange }: AvatarProps) => { function Avatar({ name, avatar, size = 'md', className, onLoadingStatusChange }: AvatarProps) {
return ( return (
<AvatarRoot size={size} className={className}> <AvatarRoot size={size} className={className}>
{avatar && ( {avatar && (
@@ -50,7 +50,7 @@ function TriggerIcon() {
function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hiddenUntilFound'>) { function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hiddenUntilFound'>) {
return ( return (
<> <React.Fragment>
<CollapsibleTrigger className={cn(triggerClassName, 'justify-between gap-2')}> <CollapsibleTrigger className={cn(triggerClassName, 'justify-between gap-2')}>
Recovery keys Recovery keys
<TriggerIcon /> <TriggerIcon />
@@ -62,7 +62,7 @@ function RecoveryKeys(props: Pick<CollapsiblePanelProps, 'keepMounted' | 'hidden
<div>horse-battery-staple</div> <div>horse-battery-staple</div>
</div> </div>
</CollapsiblePanel> </CollapsiblePanel>
</> </React.Fragment>
) )
} }
+26 -24
View File
@@ -353,23 +353,25 @@ const renderVirtualizedOptionItem = (option: Option, index: number, itemCount: n
</ComboboxItem> </ComboboxItem>
) )
const PopupSearchInput = ({ label, placeholder }: { label: string; placeholder: string }) => ( function PopupSearchInput({ label, placeholder }: { label: string; placeholder: string }) {
<div className="p-1 pb-0"> return (
<ComboboxInputGroup className="h-8 min-h-8 px-2"> <div className="p-1 pb-0">
<span <ComboboxInputGroup className="h-8 min-h-8 px-2">
aria-hidden <span
className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" aria-hidden
/> className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder"
<ComboboxInput />
aria-label={label} <ComboboxInput
placeholder={`${placeholder}…`} aria-label={label}
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled" placeholder={`${placeholder}…`}
/> className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
</ComboboxInputGroup> />
</div> </ComboboxInputGroup>
) </div>
)
}
const GroupedToolList = () => { function GroupedToolList() {
const groups = useComboboxFilteredItems<OptionGroup>() const groups = useComboboxFilteredItems<OptionGroup>()
return ( return (
@@ -385,11 +387,11 @@ const GroupedToolList = () => {
) )
} }
const VirtualizedModelList = ({ function VirtualizedModelList({
virtualizerRef, virtualizerRef,
}: { }: {
virtualizerRef: React.RefObject<StoryVirtualizer | null> virtualizerRef: React.RefObject<StoryVirtualizer | null>
}) => { }) {
const scrollRef = React.useRef<HTMLDivElement | null>(null) const scrollRef = React.useRef<HTMLDivElement | null>(null)
const filteredItems = useComboboxFilteredItems<Option>() const filteredItems = useComboboxFilteredItems<Option>()
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
@@ -451,7 +453,7 @@ const VirtualizedModelList = ({
) )
} }
const FilteredModelStatus = () => { function FilteredModelStatus() {
const filteredItems = useComboboxFilteredItems<Option>() const filteredItems = useComboboxFilteredItems<Option>()
return ( return (
@@ -461,7 +463,7 @@ const FilteredModelStatus = () => {
) )
} }
const VirtualizedLongListDemo = () => { function VirtualizedLongListDemo() {
const [value, setValue] = React.useState<Option | null>(modelCatalogOptions[137]!) const [value, setValue] = React.useState<Option | null>(modelCatalogOptions[137]!)
const virtualizerRef = React.useRef<StoryVirtualizer | null>(null) const virtualizerRef = React.useRef<StoryVirtualizer | null>(null)
@@ -495,7 +497,7 @@ const VirtualizedLongListDemo = () => {
) )
} }
const AsyncDirectoryDemo = () => { function AsyncDirectoryDemo() {
const [searchResults, setSearchResults] = React.useState<Option[]>([]) const [searchResults, setSearchResults] = React.useState<Option[]>([])
const [selectedValue, setSelectedValue] = React.useState<Option | null>(null) const [selectedValue, setSelectedValue] = React.useState<Option | null>(null)
const [searchValue, setSearchValue] = React.useState('') const [searchValue, setSearchValue] = React.useState('')
@@ -599,7 +601,7 @@ const AsyncDirectoryDemo = () => {
) )
} }
const InlinePopoverDemo = () => { function InlinePopoverDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState<Option | null>(null) const [value, setValue] = React.useState<Option | null>(null)
const [inputValue, setInputValue] = React.useState('') const [inputValue, setInputValue] = React.useState('')
@@ -829,7 +831,7 @@ export const Grouped: Story = {
), ),
} }
const MultipleChipsDemo = () => { function MultipleChipsDemo() {
const [value, setValue] = React.useState<Option[]>(defaultReviewers) const [value, setValue] = React.useState<Option[]>(defaultReviewers)
return ( return (
@@ -1004,7 +1006,7 @@ export const ReadOnly: Story = {
}, },
} }
const ControlledDemo = () => { function ControlledDemo() {
const [value, setValue] = React.useState<string | null>(defaultTag.value) const [value, setValue] = React.useState<string | null>(defaultTag.value)
return ( return (
@@ -19,14 +19,16 @@ import {
ContextMenuTrigger, ContextMenuTrigger,
} from '.' } from '.'
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => ( function TriggerArea({ label = 'Right-click inside this area' }: { label?: string }) {
<ContextMenuTrigger return (
aria-label="context menu trigger area" <ContextMenuTrigger
className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none" aria-label="context menu trigger area"
> className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none"
{label} >
</ContextMenuTrigger> {label}
) </ContextMenuTrigger>
)
}
const meta = { const meta = {
title: 'Base/UI/ContextMenu', title: 'Base/UI/ContextMenu',
@@ -102,7 +104,7 @@ export const WithGroupLabel: Story = {
type Density = 'compact' | 'comfortable' | 'spacious' type Density = 'compact' | 'comfortable' | 'spacious'
const WithRadioItemsDemo = () => { function WithRadioItemsDemo() {
const [density, setDensity] = React.useState<Density>('comfortable') const [density, setDensity] = React.useState<Density>('comfortable')
return ( return (
@@ -132,7 +134,7 @@ export const WithRadioItems: Story = {
render: () => <WithRadioItemsDemo />, render: () => <WithRadioItemsDemo />,
} }
const WithCheckboxItemsDemo = () => { function WithCheckboxItemsDemo() {
const [showToolbar, setShowToolbar] = React.useState(true) const [showToolbar, setShowToolbar] = React.useState(true)
const [showSidebar, setShowSidebar] = React.useState(false) const [showSidebar, setShowSidebar] = React.useState(false)
const [showStatusBar, setShowStatusBar] = React.useState(true) const [showStatusBar, setShowStatusBar] = React.useState(true)
@@ -1,4 +1,3 @@
import type * as React from 'react'
import { import {
DatePicker, DatePicker,
DatePickerContent, DatePickerContent,
@@ -17,7 +16,8 @@ import {
TimePickerLabel, TimePickerLabel,
TimePickerTrigger, TimePickerTrigger,
} from '@langgenius/dify-ui/time-picker' } from '@langgenius/dify-ui/time-picker'
import { createPortal } from 'react-dom' import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { userEvent } from 'vite-plus/test/browser' import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
@@ -51,7 +51,7 @@ it.each(pickers)(
const help = vi.fn() const help = vi.fn()
const screen = await render( const screen = await render(
wrap( wrap(
<> <React.Fragment>
<Label> <Label>
<span>Departure</span> <span>Departure</span>
<button type="button" onClick={help}> <button type="button" onClick={help}>
@@ -60,7 +60,7 @@ it.each(pickers)(
</Label> </Label>
<Trigger /> <Trigger />
<Content /> <Content />
</>, </React.Fragment>,
), ),
) )
const trigger = screen.getByRole('button', { name: 'Departure Help' }) const trigger = screen.getByRole('button', { name: 'Departure Help' })
@@ -113,7 +113,7 @@ it('does not forward clicks from portalled label content to the trigger', async
<DatePicker> <DatePicker>
<DatePickerLabel> <DatePickerLabel>
Departure Departure
{createPortal(<input aria-label="Help search" />, document.body)} {ReactDOM.createPortal(<input aria-label="Help search" />, document.body)}
</DatePickerLabel> </DatePickerLabel>
<DatePickerTrigger /> <DatePickerTrigger />
<DatePickerContent /> <DatePickerContent />
@@ -127,14 +127,14 @@ it('does not forward clicks from portalled label content to the trigger', async
it('allows a consumer to cancel label focus forwarding', async () => { it('allows a consumer to cancel label focus forwarding', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
<button type="button">Previous control</button> <button type="button">Previous control</button>
<DatePicker> <DatePicker>
<DatePickerLabel onClick={(event) => event.preventDefault()}>Departure</DatePickerLabel> <DatePickerLabel onClick={(event) => event.preventDefault()}>Departure</DatePickerLabel>
<DatePickerTrigger /> <DatePickerTrigger />
<DatePickerContent /> <DatePickerContent />
</DatePicker> </DatePicker>
</>, </React.Fragment>,
) )
const previous = screen.getByRole('button', { name: 'Previous control' }) const previous = screen.getByRole('button', { name: 'Previous control' })
await previous.click() await previous.click()
@@ -148,10 +148,10 @@ it.each(pickers)(
async ({ wrap, Trigger, Content }) => { async ({ wrap, Trigger, Content }) => {
const screen = await render( const screen = await render(
wrap( wrap(
<> <React.Fragment>
<Trigger aria-label="Departure" /> <Trigger aria-label="Departure" />
<Content aria-label="Choose departure" /> <Content aria-label="Choose departure" />
</>, </React.Fragment>,
), ),
) )
const trigger = screen.getByRole('button', { name: 'Departure' }) const trigger = screen.getByRole('button', { name: 'Departure' })
@@ -164,7 +164,7 @@ it.each(pickers)(
it('combines external label, value and descriptions without losing validation feedback', async () => { it('combines external label, value and descriptions without losing validation feedback', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
<span id="departure-label">Departure</span> <span id="departure-label">Departure</span>
<p id="departure-help">Choose a working day.</p> <p id="departure-help">Choose a working day.</p>
<DatePicker <DatePicker
@@ -176,7 +176,7 @@ it('combines external label, value and descriptions without losing validation fe
<DatePickerTrigger aria-labelledby="departure-label" aria-describedby="departure-help" /> <DatePickerTrigger aria-labelledby="departure-label" aria-describedby="departure-help" />
<DatePickerContent aria-labelledby="departure-label" /> <DatePickerContent aria-labelledby="departure-label" />
</DatePicker> </DatePicker>
</>, </React.Fragment>,
) )
const trigger = screen.getByRole('button', { name: 'Departure Jan 15, 2025' }) const trigger = screen.getByRole('button', { name: 'Departure Jan 15, 2025' })
await expect await expect
@@ -14,11 +14,11 @@ import {
function Parts() { function Parts() {
return ( return (
<> <React.Fragment>
<DatePickerTrigger /> <DatePickerTrigger />
<DatePickerClear aria-label="Clear date" /> <DatePickerClear aria-label="Clear date" />
<DatePickerContent /> <DatePickerContent />
</> </React.Fragment>
) )
} }
@@ -27,7 +27,7 @@ describe('DatePicker keyboard and form contract', () => {
function Example() { function Example() {
const [visible, setVisible] = React.useState(true) const [visible, setVisible] = React.useState(true)
return ( return (
<> <React.Fragment>
<button type="button" onClick={() => setVisible(!visible)}> <button type="button" onClick={() => setVisible(!visible)}>
Toggle label Toggle label
</button> </button>
@@ -36,7 +36,7 @@ describe('DatePicker keyboard and form contract', () => {
<DatePickerTrigger id="custom-departure" aria-label={visible ? undefined : 'Date'} /> <DatePickerTrigger id="custom-departure" aria-label={visible ? undefined : 'Date'} />
<DatePickerContent aria-label={visible ? undefined : 'Choose date'} /> <DatePickerContent aria-label={visible ? undefined : 'Choose date'} />
</DatePicker> </DatePicker>
</> </React.Fragment>
) )
} }
const screen = await render(<Example />) const screen = await render(<Example />)
@@ -162,7 +162,7 @@ function CalendarPanel({
} }
footer={ footer={
choosingMonth ? ( choosingMonth ? (
<> <React.Fragment>
<Button <Button
size="small" size="small"
variant="secondary" variant="secondary"
@@ -183,7 +183,7 @@ function CalendarPanel({
> >
{labels.apply} {labels.apply}
</Button> </Button>
</> </React.Fragment>
) : ( ) : (
footer footer
) )
@@ -265,7 +265,7 @@ function Session() {
isDateUnavailable={isDateUnavailable} isDateUnavailable={isDateUnavailable}
labels={text} labels={text}
footer={ footer={
<> <React.Fragment>
<Button <Button
size="small" size="small"
variant="secondary-accent" variant="secondary-accent"
@@ -285,14 +285,14 @@ function Session() {
</Button> </Button>
{apply} {apply}
</div> </div>
</> </React.Fragment>
} }
/> />
) : ( ) : (
<PickerPanel <PickerPanel
header={<PickerPanelHeader>{text.pickTime}</PickerPanelHeader>} header={<PickerPanelHeader>{text.pickTime}</PickerPanelHeader>}
footer={ footer={
<> <React.Fragment>
<Button <Button
size="small" size="small"
variant="secondary-accent" variant="secondary-accent"
@@ -311,7 +311,7 @@ function Session() {
</Button> </Button>
{apply} {apply}
</div> </div>
</> </React.Fragment>
} }
> >
<TimePanel <TimePanel
@@ -283,13 +283,13 @@ function PickerTrigger({
} }
> >
{children ?? ( {children ?? (
<> <React.Fragment>
<PickerValue /> <PickerValue />
<span <span
className="i-ri-calendar-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none" className="i-ri-calendar-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
aria-hidden="true" aria-hidden="true"
/> />
</> </React.Fragment>
)} )}
</PopoverTrigger> </PopoverTrigger>
) )
@@ -296,9 +296,9 @@ it('announces the complete custom displayed value including its timezone', async
<TimePickerTrigger> <TimePickerTrigger>
<TimePickerValue> <TimePickerValue>
{(displayValue) => ( {(displayValue) => (
<> <React.Fragment>
<span>{displayValue}</span> <span>UTC+08:00</span> <span>{displayValue}</span> <span>UTC+08:00</span>
</> </React.Fragment>
)} )}
</TimePickerValue> </TimePickerValue>
</TimePickerTrigger> </TimePickerTrigger>
@@ -174,9 +174,9 @@ export const Schedule: Story = {
<TimePickerTrigger> <TimePickerTrigger>
<TimePickerValue> <TimePickerValue>
{(value) => ( {(value) => (
<> <React.Fragment>
{value} <span className="text-text-tertiary">UTC+0</span> {value} <span className="text-text-tertiary">UTC+0</span>
</> </React.Fragment>
)} )}
</TimePickerValue> </TimePickerValue>
</TimePickerTrigger> </TimePickerTrigger>
@@ -136,13 +136,13 @@ function TimePickerTrigger({ children, ...props }: TimePickerTriggerProps) {
return ( return (
<PickerTrigger {...props}> <PickerTrigger {...props}>
{children ?? ( {children ?? (
<> <React.Fragment>
<PickerValue /> <PickerValue />
<span <span
aria-hidden="true" aria-hidden="true"
className="i-ri-time-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none" className="i-ri-time-line size-4 shrink-0 text-text-tertiary forced-colors:text-[ButtonText] forced-colors:forced-color-adjust-none"
/> />
</> </React.Fragment>
)} )}
</PickerTrigger> </PickerTrigger>
) )
@@ -196,7 +196,7 @@ function Session() {
) )
} }
footer={ footer={
<> <React.Fragment>
<Button <Button
size="small" size="small"
variant="ghost-accent" variant="ghost-accent"
@@ -224,7 +224,7 @@ function Session() {
> >
{text.apply} {text.apply}
</Button> </Button>
</> </React.Fragment>
} }
> >
<TimePanel <TimePanel
@@ -75,14 +75,14 @@ describe('Dialog wrapper', () => {
it('should connect a detached trigger to the dialog', async () => { it('should connect a detached trigger to the dialog', async () => {
const handle = createDialogHandle() const handle = createDialogHandle()
const screen = await render( const screen = await render(
<> <React.Fragment>
<DialogTrigger handle={handle}>Open dialog</DialogTrigger> <DialogTrigger handle={handle}>Open dialog</DialogTrigger>
<Dialog handle={handle}> <Dialog handle={handle}>
<DialogContent> <DialogContent>
<DialogTitle>Detached dialog</DialogTitle> <DialogTitle>Detached dialog</DialogTitle>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</>, </React.Fragment>,
) )
await screen.getByRole('button', { name: 'Open dialog' }).click() await screen.getByRole('button', { name: 'Open dialog' }).click()
@@ -160,7 +160,7 @@ export const WithoutCloseButton: Story = {
), ),
} }
const ControlledDemo = () => { function ControlledDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
return ( return (
@@ -274,7 +274,7 @@ type ApiExtensionFormValues = {
apiKey: string apiKey: string
} }
const FormDialogDemo = () => { function FormDialogDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const nameInputRef = React.useRef<HTMLInputElement>(null) const nameInputRef = React.useRef<HTMLInputElement>(null)
@@ -369,7 +369,7 @@ export const FormDialog: Story = {
}, },
} }
const OutsideScrollingContentDemo = () => { function OutsideScrollingContentDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const popupRef = React.useRef<HTMLDivElement>(null) const popupRef = React.useRef<HTMLDivElement>(null)
@@ -465,7 +465,7 @@ export const OutsidePopupElements: Story = {
), ),
} }
const InsideScrollingContentDemo = () => { function InsideScrollingContentDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
return ( return (
@@ -19,18 +19,20 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '.' } from '.'
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => ( function TriggerButton({ label = 'Open Menu' }: { label?: string }) {
<DropdownMenuTrigger return (
render={ <DropdownMenuTrigger
<button render={
type="button" <button
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid" type="button"
/> className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
} />
> }
{label} >
</DropdownMenuTrigger> {label}
) </DropdownMenuTrigger>
)
}
const meta = { const meta = {
title: 'Base/UI/DropdownMenu', title: 'Base/UI/DropdownMenu',
@@ -140,7 +142,7 @@ export const WithSubmenu: Story = {
type Density = 'compact' | 'comfortable' | 'spacious' type Density = 'compact' | 'comfortable' | 'spacious'
const WithRadioItemsDemo = () => { function WithRadioItemsDemo() {
const [density, setDensity] = React.useState<Density>('comfortable') const [density, setDensity] = React.useState<Density>('comfortable')
return ( return (
@@ -170,7 +172,7 @@ export const WithRadioItems: Story = {
render: () => <WithRadioItemsDemo />, render: () => <WithRadioItemsDemo />,
} }
const WithCheckboxItemsDemo = () => { function WithCheckboxItemsDemo() {
const [showToolbar, setShowToolbar] = React.useState(true) const [showToolbar, setShowToolbar] = React.useState(true)
const [showSidebar, setShowSidebar] = React.useState(false) const [showSidebar, setShowSidebar] = React.useState(false)
const [showStatusBar, setShowStatusBar] = React.useState(true) const [showStatusBar, setShowStatusBar] = React.useState(true)
@@ -265,7 +267,7 @@ export const WithLinkItems: Story = {
type SortOrder = 'newest' | 'oldest' | 'name' type SortOrder = 'newest' | 'oldest' | 'name'
const ComplexDemo = () => { function ComplexDemo() {
const [sortOrder, setSortOrder] = React.useState<SortOrder>('newest') const [sortOrder, setSortOrder] = React.useState<SortOrder>('newest')
const [showArchived, setShowArchived] = React.useState(false) const [showArchived, setShowArchived] = React.useState(false)
@@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { createPortal } from 'react-dom' import * as ReactDOM from 'react-dom'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { Infotip, InfotipContent, InfotipTrigger } from '..' import { Infotip, InfotipContent, InfotipTrigger } from '..'
@@ -11,7 +11,7 @@ describe('Infotip composition', () => {
const screen = await render( const screen = await render(
<button type="button" onClick={parentClick}> <button type="button" onClick={parentClick}>
Parent action Parent action
{createPortal( {ReactDOM.createPortal(
<Infotip> <Infotip>
<InfotipTrigger aria-label="Details" onClick={triggerClick} ref={triggerRef} /> <InfotipTrigger aria-label="Details" onClick={triggerClick} ref={triggerRef} />
<InfotipContent aria-label="Details">Explanation</InfotipContent> <InfotipContent aria-label="Details">Explanation</InfotipContent>
@@ -1,5 +1,5 @@
import type * as React from 'react'
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
import * as React from 'react'
export function infotipTypeContracts( export function infotipTypeContracts(
triggerRef: React.Ref<HTMLButtonElement>, triggerRef: React.Ref<HTMLButtonElement>,
@@ -8,7 +8,7 @@ export function infotipTypeContracts(
const composition = ( const composition = (
<Infotip<{ title: string }>> <Infotip<{ title: string }>>
{({ payload }) => ( {({ payload }) => (
<> <React.Fragment>
<InfotipTrigger <InfotipTrigger
aria-label="Details" aria-label="Details"
ref={triggerRef} ref={triggerRef}
@@ -28,7 +28,7 @@ export function infotipTypeContracts(
> >
{payload?.title} {payload?.title}
</InfotipContent> </InfotipContent>
</> </React.Fragment>
)} )}
</Infotip> </Infotip>
) )
@@ -1,3 +1,4 @@
import * as React from 'react'
import { userEvent } from 'vite-plus/test/browser' import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { Button } from '../../button' import { Button } from '../../button'
@@ -157,7 +158,7 @@ describe('Invalid focus colors', () => {
document.documentElement.dataset.theme = theme document.documentElement.dataset.theme = theme
try { try {
const screen = await render( const screen = await render(
<> <React.Fragment>
<Button>Before</Button> <Button>Before</Button>
<Field invalid> <Field invalid>
<FieldLabel>Invalid value</FieldLabel> <FieldLabel>Invalid value</FieldLabel>
@@ -166,7 +167,7 @@ describe('Invalid focus colors', () => {
<InputGroupAddon>suffix</InputGroupAddon> <InputGroupAddon>suffix</InputGroupAddon>
</InputGroup> </InputGroup>
</Field> </Field>
</>, </React.Fragment>,
) )
const input = screen.getByRole('textbox', { name: 'Invalid value' }) const input = screen.getByRole('textbox', { name: 'Invalid value' })
const surface = screen.getByTestId('invalid-surface').element() const surface = screen.getByTestId('invalid-surface').element()
+13 -11
View File
@@ -43,7 +43,7 @@ const meta = {
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
const HotkeyKbdGroup = ({ function HotkeyKbdGroup({
hotkey, hotkey,
color = 'gray', color = 'gray',
platform = 'mac', platform = 'mac',
@@ -51,16 +51,18 @@ const HotkeyKbdGroup = ({
hotkey: DisplayHotkey hotkey: DisplayHotkey
color?: 'gray' | 'white' color?: 'gray' | 'white'
platform?: FormatDisplayOptions['platform'] platform?: FormatDisplayOptions['platform']
}) => ( }) {
<KbdGroup> return (
{formatForDisplay(hotkey, { platform, parts: true }).map((key, index) => ( <KbdGroup>
// oxlint-disable-next-line react/no-array-index-key -- Repeated display keys are static, ordered tokens with no component state. {formatForDisplay(hotkey, { platform, parts: true }).map((key, index) => (
<Kbd key={`${key}-${index}`} color={color}> // oxlint-disable-next-line react/no-array-index-key -- Repeated display keys are static, ordered tokens with no component state.
{key} <Kbd key={`${key}-${index}`} color={color}>
</Kbd> {key}
))} </Kbd>
</KbdGroup> ))}
) </KbdGroup>
)
}
export const Default: Story = { export const Default: Story = {
render: () => <HotkeyKbdGroup hotkey="Mod+K" />, render: () => <HotkeyKbdGroup hotkey="Mod+K" />,
@@ -268,7 +268,7 @@ describe('Invalid focus colors', () => {
document.documentElement.dataset.theme = theme document.documentElement.dataset.theme = theme
try { try {
const screen = await render( const screen = await render(
<> <React.Fragment>
<Button>Before</Button> <Button>Before</Button>
<Field invalid> <Field invalid>
<FieldLabel>Invalid value</FieldLabel> <FieldLabel>Invalid value</FieldLabel>
@@ -278,7 +278,7 @@ describe('Invalid focus colors', () => {
</NumberFieldGroup> </NumberFieldGroup>
</NumberField> </NumberField>
</Field> </Field>
</>, </React.Fragment>,
) )
const input = screen.getByRole('textbox', { name: 'Invalid value' }) const input = screen.getByRole('textbox', { name: 'Invalid value' })
const surface = screen.getByTestId('invalid-surface').element() const surface = screen.getByTestId('invalid-surface').element()
@@ -208,7 +208,7 @@ const PLACEMENTS: PopoverPlacement[] = [
'left-end', 'left-end',
] ]
const PlacementsDemo = () => { function PlacementsDemo() {
const [placement, setPlacement] = React.useState<PopoverPlacement>('bottom') const [placement, setPlacement] = React.useState<PopoverPlacement>('bottom')
return ( return (
@@ -254,7 +254,7 @@ export const Placements: Story = {
render: () => <PlacementsDemo />, render: () => <PlacementsDemo />,
} }
const ControlledDemo = () => { function ControlledDemo() {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
return ( return (
@@ -126,7 +126,7 @@ const PLACEMENTS: PreviewCardPlacement[] = [
'left-end', 'left-end',
] ]
const PlacementsDemo = () => { function PlacementsDemo() {
const [placement, setPlacement] = React.useState<PreviewCardPlacement>('bottom') const [placement, setPlacement] = React.useState<PreviewCardPlacement>('bottom')
return ( return (
@@ -172,27 +172,29 @@ export const Placements: Story = {
render: () => <PlacementsDemo />, render: () => <PlacementsDemo />,
} }
const CustomDelayDemo = () => ( function CustomDelayDemo() {
<PreviewCard> return (
<PreviewCardTrigger <PreviewCard>
delay={100} <PreviewCardTrigger
closeDelay={100} delay={100}
href="#preview-card-delay" closeDelay={100}
className={triggerButtonClassName} href="#preview-card-delay"
> className={triggerButtonClassName}
Snappy trigger >
</PreviewCardTrigger> Snappy trigger
<PreviewCardContent className="w-64 p-3"> </PreviewCardTrigger>
<div className="flex flex-col gap-1"> <PreviewCardContent className="w-64 p-3">
<div className="text-sm font-semibold text-text-primary">Fast hover</div> <div className="flex flex-col gap-1">
<div className="text-xs text-text-secondary"> <div className="text-sm font-semibold text-text-primary">Fast hover</div>
Base UI defaults (600ms / 300ms) are tuned for link previews. Override per trigger for <div className="text-xs text-text-secondary">
denser UIs. Base UI defaults (600ms / 300ms) are tuned for link previews. Override per trigger for
denser UIs.
</div>
</div> </div>
</div> </PreviewCardContent>
</PreviewCardContent> </PreviewCard>
</PreviewCard> )
) }
export const CustomDelays: Story = { export const CustomDelays: Story = {
render: () => <CustomDelayDemo />, render: () => <CustomDelayDemo />,
@@ -1,4 +1,4 @@
import type * as React from 'react' import * as React from 'react'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { import {
ScrollArea, ScrollArea,
@@ -105,7 +105,7 @@ describe('scroll area', () => {
it('should keep accessible region semantics on the viewport', async () => { it('should keep accessible region semantics on the viewport', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
<p id="installed-apps-label">Installed apps</p> <p id="installed-apps-label">Installed apps</p>
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root"> <ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
<ScrollAreaViewport <ScrollAreaViewport
@@ -121,7 +121,7 @@ describe('scroll area', () => {
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollArea>
</>, </React.Fragment>,
) )
const viewport = screen.getByRole('region', { name: 'Installed apps' }) const viewport = screen.getByRole('region', { name: 'Installed apps' })
@@ -1,11 +1,11 @@
import type * as React from 'react' import * as React from 'react'
import { userEvent } from 'vite-plus/test/browser' import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '../index' import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '../index'
function SegmentedControlTypeExamples() { function SegmentedControlTypeExamples() {
return ( return (
<> <React.Fragment>
<SegmentedControl<number> value={10} onValueChange={() => {}} aria-label="Page size"> <SegmentedControl<number> value={10} onValueChange={() => {}} aria-label="Page size">
<SegmentedControlItem<number> value={10}>10</SegmentedControlItem> <SegmentedControlItem<number> value={10}>10</SegmentedControlItem>
<SegmentedControlItem<number> value={20}>20</SegmentedControlItem> <SegmentedControlItem<number> value={20}>20</SegmentedControlItem>
@@ -14,7 +14,7 @@ function SegmentedControlTypeExamples() {
<SegmentedControl aria-label="Missing value"> <SegmentedControl aria-label="Missing value">
<SegmentedControlItem value="one">One</SegmentedControlItem> <SegmentedControlItem value="one">One</SegmentedControlItem>
</SegmentedControl> </SegmentedControl>
</> </React.Fragment>
) )
} }
@@ -30,14 +30,18 @@ type SegmentedControlProps = {
noPadding?: boolean noPadding?: boolean
} }
const Icon = () => <i className="i-ri-information-line size-4 shrink-0" aria-hidden="true" /> function Icon() {
return <i className="i-ri-information-line size-4 shrink-0" aria-hidden="true" />
}
const Item = () => ( function Item() {
<React.Fragment> return (
<Icon /> <React.Fragment>
<span className="px-0.5">Item</span> <Icon />
</React.Fragment> <span className="px-0.5">Item</span>
) </React.Fragment>
)
}
function SegmentedControlExample({ function SegmentedControlExample({
defaultValue, defaultValue,
@@ -298,7 +298,7 @@ export const ReadOnly: Story = {
), ),
} }
const ControlledDemo = () => { function ControlledDemo() {
const [value, setValue] = React.useState<string | null>('balanced') const [value, setValue] = React.useState<string | null>('balanced')
return ( return (
@@ -336,7 +336,7 @@ export const Controlled: Story = {
render: () => <ControlledDemo />, render: () => <ControlledDemo />,
} }
const MultipleControlledDemo = () => { function MultipleControlledDemo() {
const [value, setValue] = React.useState<DeploymentRegion[]>(['us-east', 'eu-west']) const [value, setValue] = React.useState<DeploymentRegion[]>(['us-east', 'eu-west'])
return ( return (
@@ -1,13 +1,14 @@
import * as React from 'react'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { StatusDot, StatusDotSkeleton } from '../index' import { StatusDot, StatusDotSkeleton } from '../index'
describe('StatusDot', () => { describe('StatusDot', () => {
it('hides the visual indicator and its loading placeholder from assistive technology', async () => { it('hides the visual indicator and its loading placeholder from assistive technology', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
<StatusDot data-testid="dot" /> <StatusDot data-testid="dot" />
<StatusDotSkeleton data-testid="skeleton" /> <StatusDotSkeleton data-testid="skeleton" />
</>, </React.Fragment>,
) )
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true') await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
@@ -16,14 +17,14 @@ describe('StatusDot', () => {
it('does not allow consumers to override the decorative contract', async () => { it('does not allow consumers to override the decorative contract', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
{/* @ts-expect-error StatusDot is always decorative */} {/* @ts-expect-error StatusDot is always decorative */}
<StatusDot aria-hidden={false} data-testid="dot" /> <StatusDot aria-hidden={false} data-testid="dot" />
{/* @ts-expect-error Status semantics belong to the surrounding component */} {/* @ts-expect-error Status semantics belong to the surrounding component */}
<StatusDot aria-label="Active" data-testid="labelled-dot" /> <StatusDot aria-label="Active" data-testid="labelled-dot" />
{/* @ts-expect-error StatusDotSkeleton is always decorative */} {/* @ts-expect-error StatusDotSkeleton is always decorative */}
<StatusDotSkeleton aria-hidden={false} data-testid="skeleton" /> <StatusDotSkeleton aria-hidden={false} data-testid="skeleton" />
</>, </React.Fragment>,
) )
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true') await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
@@ -1,5 +1,6 @@
import type { SwitchProps } from '@langgenius/dify-ui/switch' import type { SwitchProps } from '@langgenius/dify-ui/switch'
import { Switch } from '@langgenius/dify-ui/switch' import { Switch } from '@langgenius/dify-ui/switch'
import * as React from 'react'
function SwitchTypeExamples() { function SwitchTypeExamples() {
const onCheckedChange: NonNullable<SwitchProps['onCheckedChange']> = (checked, details) => { const onCheckedChange: NonNullable<SwitchProps['onCheckedChange']> = (checked, details) => {
@@ -7,12 +8,12 @@ function SwitchTypeExamples() {
} }
return ( return (
<> <React.Fragment>
<Switch defaultChecked onCheckedChange={onCheckedChange} /> <Switch defaultChecked onCheckedChange={onCheckedChange} />
<Switch checked={false} onCheckedChange={(checked) => void checked} /> <Switch checked={false} onCheckedChange={(checked) => void checked} />
{/* @ts-expect-error the composite owns its thumb and loading indicator */} {/* @ts-expect-error the composite owns its thumb and loading indicator */}
<Switch>Custom thumb</Switch> <Switch>Custom thumb</Switch>
</> </React.Fragment>
) )
} }
+26 -24
View File
@@ -50,7 +50,7 @@ type SwitchDemoProps = Partial<
checked?: boolean checked?: boolean
} }
const SwitchDemo = (args: SwitchDemoProps) => { function SwitchDemo(args: SwitchDemoProps) {
const [enabled, setEnabled] = React.useState(args.checked ?? false) const [enabled, setEnabled] = React.useState(args.checked ?? false)
return ( return (
@@ -113,7 +113,7 @@ export const DisabledOn: Story = {
}, },
} }
const AllStatesDemo = () => { function AllStatesDemo() {
const sizes = ['xs', 'sm', 'md', 'lg'] as const const sizes = ['xs', 'sm', 'md', 'lg'] as const
return ( return (
@@ -202,7 +202,7 @@ export const AllStates: Story = {
}, },
} }
const SizeComparisonDemo = () => { function SizeComparisonDemo() {
const [states, setStates] = React.useState({ const [states, setStates] = React.useState({
xs: false, xs: false,
sm: false, sm: false,
@@ -260,7 +260,7 @@ export const SizeComparison: Story = {
render: () => <SizeComparisonDemo />, render: () => <SizeComparisonDemo />,
} }
const LoadingDemo = () => { function LoadingDemo() {
const [loading, setLoading] = React.useState(true) const [loading, setLoading] = React.useState(true)
return ( return (
@@ -362,7 +362,7 @@ function useMockUpdateAutoRetrySettingMutation({
} }
} }
const MutationLoadingDemo = () => { function MutationLoadingDemo() {
const autoRetrySetting = useMockAutoRetrySettingQuery() const autoRetrySetting = useMockAutoRetrySettingQuery()
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({ const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
onSuccess: autoRetrySetting.setData, onSuccess: autoRetrySetting.setData,
@@ -406,26 +406,28 @@ export const MutationLoadingGuard: Story = {
}, },
} }
const SkeletonDemo = () => ( function SkeletonDemo() {
<div className="flex flex-col items-center space-y-4"> return (
<div className="flex items-center gap-3"> <div className="flex flex-col items-center space-y-4">
<SwitchSkeleton size="xs" aria-hidden="true" /> <div className="flex items-center gap-3">
<span className="text-sm text-gray-700">Extra Small skeleton</span> <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" aria-hidden="true" />
<span className="text-sm text-gray-700">Small skeleton</span>
</div>
<div className="flex items-center gap-3">
<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" aria-hidden="true" />
<span className="text-sm text-gray-700">Large skeleton</span>
</div>
</div> </div>
<div className="flex items-center gap-3"> )
<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" aria-hidden="true" />
<span className="text-sm text-gray-700">Regular skeleton</span>
</div>
<div className="flex items-center gap-3">
<SwitchSkeleton size="lg" aria-hidden="true" />
<span className="text-sm text-gray-700">Large skeleton</span>
</div>
</div>
)
export const Skeleton: Story = { export const Skeleton: Story = {
render: () => <SkeletonDemo />, render: () => <SkeletonDemo />,
@@ -110,7 +110,7 @@ export const States: Story = {
), ),
} }
const FormDemo = () => { function FormDemo() {
const [savedDescription, setSavedDescription] = React.useState<string | null>(null) const [savedDescription, setSavedDescription] = React.useState<string | null>(null)
return ( return (
@@ -153,7 +153,7 @@ export const WithField: Story = {
render: () => <FormDemo />, render: () => <FormDemo />,
} }
const ControlledDemo = () => { function ControlledDemo() {
const [value, setValue] = React.useState( const [value, setValue] = React.useState(
'Summarize customer feedback into actionable product themes.', 'Summarize customer feedback into actionable product themes.',
) )
@@ -180,7 +180,7 @@ export const Controlled: Story = {
), ),
} }
const CharacterCounterDemo = () => { function CharacterCounterDemo() {
const maxLength = 120 const maxLength = 120
const [value, setValue] = React.useState( const [value, setValue] = React.useState(
'Summarize customer feedback into actionable product themes.', 'Summarize customer feedback into actionable product themes.',
@@ -1,4 +1,5 @@
import type { ToastManager, ToastViewportProps } from '../index' import type { ToastManager, ToastViewportProps } from '../index'
import * as React from 'react'
import { userEvent } from 'vite-plus/test/browser' import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react' import { render } from 'vitest-browser-react'
import { import {
@@ -88,7 +89,7 @@ describe('@langgenius/dify-ui/toast', () => {
it('should not intercept pointer events below a collapsed top-anchored stack', async () => { it('should not intercept pointer events below a collapsed top-anchored stack', async () => {
const screen = await render( const screen = await render(
<> <React.Fragment>
<style>{'[role="dialog"] { transition: none !important; }'}</style> <style>{'[role="dialog"] { transition: none !important; }'}</style>
<button <button
type="button" type="button"
@@ -103,7 +104,7 @@ describe('@langgenius/dify-ui/toast', () => {
Underlying action Underlying action
</button> </button>
<ExampleToastHost manager={manager} timeout={0} /> <ExampleToastHost manager={manager} timeout={0} />
</>, </React.Fragment>,
) )
toast('Older notification') toast('Older notification')
@@ -134,7 +135,7 @@ describe('@langgenius/dify-ui/toast', () => {
try { try {
const screen = await render( const screen = await render(
<> <React.Fragment>
<style> <style>
{` {`
[role="dialog"]:not([data-ending-style]) { [role="dialog"]:not([data-ending-style]) {
@@ -170,7 +171,7 @@ describe('@langgenius/dify-ui/toast', () => {
}} }}
/> />
<ExampleToastHost manager={manager} timeout={0} offset={{ top: 120 }} /> <ExampleToastHost manager={manager} timeout={0} offset={{ top: 120 }} />
</>, </React.Fragment>,
) )
toast('Directional notification') toast('Directional notification')
@@ -209,7 +210,7 @@ describe('@langgenius/dify-ui/toast', () => {
try { try {
const screen = await render( const screen = await render(
<> <React.Fragment>
<style> <style>
{` {`
[role="dialog"][data-ending-style] { [role="dialog"][data-ending-style] {
@@ -230,7 +231,7 @@ describe('@langgenius/dify-ui/toast', () => {
}} }}
/> />
<ExampleToastHost manager={manager} timeout={0} /> <ExampleToastHost manager={manager} timeout={0} />
</>, </React.Fragment>,
) )
toast('Background notification') toast('Background notification')
@@ -269,10 +270,10 @@ describe('@langgenius/dify-ui/toast', () => {
const localManager = createToastManager() const localManager = createToastManager()
const localToast = createToast(localManager) const localToast = createToast(localManager)
const screen = await render( const screen = await render(
<> <React.Fragment>
<ExampleToastHost manager={manager} /> <ExampleToastHost manager={manager} />
<ExampleToastHost manager={localManager} /> <ExampleToastHost manager={localManager} />
</>, </React.Fragment>,
) )
localToast.error('Local error') localToast.error('Local error')
@@ -361,7 +362,7 @@ describe('@langgenius/dify-ui/toast', () => {
try { try {
const screen = await render( const screen = await render(
<> <React.Fragment>
<style> <style>
{` {`
[role="dialog"][data-ending-style] { [role="dialog"][data-ending-style] {
@@ -391,7 +392,7 @@ describe('@langgenius/dify-ui/toast', () => {
Underlying action Underlying action
</button> </button>
<ExampleToastHost manager={manager} /> <ExampleToastHost manager={manager} />
</>, </React.Fragment>,
) )
toast('Dismiss me', { toast('Dismiss me', {
+9 -9
View File
@@ -53,7 +53,7 @@ const buttonClassName =
const cardClassName = const cardClassName =
'flex min-h-[220px] flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6 shadow-sm shadow-shadow-shadow-3' 'flex min-h-[220px] flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6 shadow-sm shadow-shadow-shadow-3'
const ExampleCard = ({ function ExampleCard({
eyebrow, eyebrow,
title, title,
description, description,
@@ -63,7 +63,7 @@ const ExampleCard = ({
title: string title: string
description: string description: string
children: React.ReactNode children: React.ReactNode
}) => { }) {
return ( return (
<section className={cardClassName}> <section className={cardClassName}>
<div className="space-y-2"> <div className="space-y-2">
@@ -76,7 +76,7 @@ const ExampleCard = ({
) )
} }
const VariantExamples = () => { function VariantExamples() {
const createVariantToast = (type: 'success' | 'error' | 'warning' | 'info') => { const createVariantToast = (type: 'success' | 'error' | 'warning' | 'info') => {
const copy = { const copy = {
success: { success: {
@@ -132,7 +132,7 @@ const VariantExamples = () => {
) )
} }
const StackExamples = () => { function StackExamples() {
const createStack = () => { const createStack = () => {
;[ ;[
{ {
@@ -194,7 +194,7 @@ const StackExamples = () => {
) )
} }
const PromiseExamples = () => { function PromiseExamples() {
const [pendingExample, setPendingExample] = React.useState<'success' | 'error' | null>(null) const [pendingExample, setPendingExample] = React.useState<'success' | 'error' | null>(null)
const exportDsl = async (outcome: 'success' | 'error') => { const exportDsl = async (outcome: 'success' | 'error') => {
@@ -255,7 +255,7 @@ const PromiseExamples = () => {
) )
} }
const ActionExamples = () => { function ActionExamples() {
const createActionToast = () => { const createActionToast = () => {
let archivedToastId = '' let archivedToastId = ''
archivedToastId = toast.warning('Project archived', { archivedToastId = toast.warning('Project archived', {
@@ -301,7 +301,7 @@ const ActionExamples = () => {
) )
} }
const DeduplicateExamples = () => { function DeduplicateExamples() {
const saveCountRef = React.useRef(0) const saveCountRef = React.useRef(0)
const saveDraft = () => { const saveDraft = () => {
@@ -328,7 +328,7 @@ const DeduplicateExamples = () => {
) )
} }
const UpdateExamples = () => { function UpdateExamples() {
const createUpdatableToast = () => { const createUpdatableToast = () => {
const toastId = toast.info('Import started', { const toastId = toast.info('Import started', {
description: 'Preparing assets and metadata for processing.', description: 'Preparing assets and metadata for processing.',
@@ -365,7 +365,7 @@ const UpdateExamples = () => {
) )
} }
const ToastDocsDemo = () => { function ToastDocsDemo() {
return ( return (
<React.Fragment> <React.Fragment>
<ExampleToastHost manager={manager} /> <ExampleToastHost manager={manager} />
+21 -19
View File
@@ -106,7 +106,7 @@ const PLACEMENTS: TooltipPlacement[] = [
'left-end', 'left-end',
] ]
const PlacementsDemo = () => { function PlacementsDemo() {
const [placement, setPlacement] = React.useState<TooltipPlacement>('top') const [placement, setPlacement] = React.useState<TooltipPlacement>('top')
return ( return (
@@ -158,24 +158,26 @@ const DELAY_PRESETS: Array<{ label: string; delay: number }> = [
{ label: 'Default', delay: 600 }, { label: 'Default', delay: 600 },
] ]
const DelayDemo = () => ( function DelayDemo() {
<div className="flex items-center gap-3"> return (
{DELAY_PRESETS.map(({ label, delay }) => ( <div className="flex items-center gap-3">
<TooltipProvider key={delay} delay={delay}> {DELAY_PRESETS.map(({ label, delay }) => (
<Tooltip> <TooltipProvider key={delay} delay={delay}>
<TooltipTrigger <Tooltip>
render={ <TooltipTrigger
<DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary"> render={
<span aria-hidden className="i-ri-timer-line size-4" /> <DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary">
</DifyIconButton> <span aria-hidden className="i-ri-timer-line size-4" />
} </DifyIconButton>
/> }
<TooltipContent>{`${label} (${delay}ms)`}</TooltipContent> />
</Tooltip> <TooltipContent>{`${label} (${delay}ms)`}</TooltipContent>
</TooltipProvider> </Tooltip>
))} </TooltipProvider>
</div> ))}
) </div>
)
}
export const WithDelay: Story = { export const WithDelay: Story = {
parameters: { parameters: {
-10
View File
@@ -15,16 +15,6 @@ export default defineConfig({
resolve: { resolve: {
tsconfigPaths: true, tsconfigPaths: true,
}, },
optimizeDeps: {
include: [
'vite-plus/test/browser',
'@daypicker/react/locale/en-US',
'@daypicker/react/locale/ar-TN',
'@daypicker/react/locale/fa-IR',
'@base-ui/utils/useStableCallback',
'@base-ui/utils/useTimeout',
],
},
test: { test: {
browser: { browser: {
expect: { toMatchScreenshot: { screenshotDirectory: './.vitest-browser/screenshots' } }, expect: { toMatchScreenshot: { screenshotDirectory: './.vitest-browser/screenshots' } },