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