mirror of
https://github.com/langgenius/dify.git
synced 2026-05-27 20:36:18 +08:00
refactor: use segmented control
This commit is contained in:
@ -57,8 +57,8 @@ describe('Category', () => {
|
||||
it('should treat unknown value as all categories selection', () => {
|
||||
renderComponent({ value: 'Unknown' })
|
||||
|
||||
const allCategoriesItem = screen.getByText('explore.apps.allCategories')
|
||||
expect(allCategoriesItem.parentElement?.className).toContain('bg-components-segmented-control-item-active-bg')
|
||||
const allCategoriesItem = screen.getByRole('button', { name: /explore\.apps\.allCategories/ })
|
||||
expect(allCategoriesItem).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
it('should render raw category name when i18n key does not exist', () => {
|
||||
@ -67,4 +67,14 @@ describe('Category', () => {
|
||||
expect(screen.getByText('CustomCategory')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should render categories as a segmented control', () => {
|
||||
renderComponent({ value: 'Writing' })
|
||||
|
||||
expect(screen.getByRole('group', { name: 'explore.tryApp.category' })).toHaveClass('bg-components-segmented-control-bg-normal')
|
||||
expect(screen.getByRole('button', { name: /explore\.apps\.allCategories/ })).toHaveAttribute('aria-pressed', 'false')
|
||||
expect(screen.getByRole('button', { name: 'explore.category.Writing' })).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AppCategory } from '@/models/explore'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import exploreI18n from '@/i18n/en-US/explore.json'
|
||||
import { ThumbsUp } from '../base/icons/src/vender/line/alertsAndFeedback'
|
||||
@ -18,28 +17,36 @@ type ICategoryProps = {
|
||||
allCategoriesEn: string
|
||||
}
|
||||
|
||||
const Category: FC<ICategoryProps> = ({
|
||||
function Category({
|
||||
className,
|
||||
list,
|
||||
value,
|
||||
onChange,
|
||||
allCategoriesEn,
|
||||
}) => {
|
||||
}: ICategoryProps) {
|
||||
const { t } = useTranslation()
|
||||
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
|
||||
|
||||
const itemClassName = (isSelected: boolean) => cn(
|
||||
'relative flex h-7 shrink-0 cursor-pointer items-center justify-center gap-0.5 overflow-hidden rounded-lg border-[0.5px] border-transparent px-2 py-1 system-sm-medium whitespace-nowrap text-text-secondary',
|
||||
isSelected && 'border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs shadow-shadow-shadow-3',
|
||||
)
|
||||
const selectedCategory = isAllCategories ? allCategoriesEn : value
|
||||
|
||||
const renderCategoryName = (name: AppCategory) => {
|
||||
const categoryKey = `category.${name}` as keyof typeof exploreI18n
|
||||
return categoryKey in exploreI18n ? t(categoryKey, { ns: 'explore' }) : name
|
||||
}
|
||||
|
||||
const handleValueChange = (nextCategories: string[]) => {
|
||||
const nextCategory = nextCategories[0]
|
||||
|
||||
if (nextCategory)
|
||||
onChange(nextCategory)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'inline-flex max-w-full items-center gap-px overflow-x-auto rounded-[10px] bg-components-segmented-control-bg-normal p-0.5 text-[13px]')}>
|
||||
<SegmentedControl
|
||||
aria-label={t('tryApp.category', { ns: 'explore' })}
|
||||
className={cn(className, 'max-w-full overflow-x-auto text-[13px]')}
|
||||
value={[selectedCategory]}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
{[
|
||||
{ name: allCategoriesEn, label: t('apps.allCategories', { ns: 'explore' }), isAll: true },
|
||||
...list.filter(name => name !== allCategoriesEn).map(name => ({
|
||||
@ -55,26 +62,26 @@ const Category: FC<ICategoryProps> = ({
|
||||
: false
|
||||
|
||||
return (
|
||||
<div key={item.isAll ? 'all' : item.name} className="relative flex items-center">
|
||||
<div
|
||||
className={itemClassName(isSelected)}
|
||||
onClick={() => onChange(item.name)}
|
||||
<span key={item.isAll ? 'all' : item.name} className="relative flex items-center">
|
||||
<SegmentedControlItem
|
||||
value={item.name}
|
||||
className="shrink-0 cursor-pointer"
|
||||
>
|
||||
{item.isAll && (
|
||||
<ThumbsUp className="mr-1 size-3.5" />
|
||||
<ThumbsUp className="mr-1 size-3.5" aria-hidden="true" />
|
||||
)}
|
||||
<span className="flex shrink-0 items-center justify-center gap-1 p-0.5">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
</SegmentedControlItem>
|
||||
{!isSelected && !isNextSelected && index < items.length - 1 && (
|
||||
<div className="absolute top-1/2 right-[-1px] h-3.5 w-px -translate-y-1/2 bg-divider-regular" />
|
||||
<SegmentedControlDivider className="absolute top-1/2 -right-px -translate-y-1/2" />
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SegmentedControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Category)
|
||||
export default Category
|
||||
|
||||
Reference in New Issue
Block a user