refactor: use segmented control

This commit is contained in:
yyh
2026-05-27 16:38:20 +08:00
parent ef00f850e4
commit 7a8a92082b
2 changed files with 39 additions and 22 deletions

View File

@ -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')
})
})
})

View File

@ -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