diff --git a/web/app/components/base/select/locale-signin.tsx b/web/app/components/base/select/locale-signin.tsx
deleted file mode 100644
index 046a76a5d4..0000000000
--- a/web/app/components/base/select/locale-signin.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-'use client'
-import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
-import { GlobeAltIcon } from '@heroicons/react/24/outline'
-import { Fragment } from 'react'
-
-type ISelectProps = {
- items: Array<{ value: string, name: string }>
- value?: string
- className?: string
- onChange?: (value: string) => void
-}
-
-export default function LocaleSigninSelect({
- items,
- value,
- onChange,
-}: ISelectProps) {
- const item = items.filter(item => item.value === value)[0]
-
- return (
-
-
-
- )
-}
diff --git a/web/app/components/base/select/__tests__/locale-signin.spec.tsx b/web/app/signin/__tests__/_locale-menu.spec.tsx
similarity index 69%
rename from web/app/components/base/select/__tests__/locale-signin.spec.tsx
rename to web/app/signin/__tests__/_locale-menu.spec.tsx
index 1eca2b1aed..e43587033f 100644
--- a/web/app/components/base/select/__tests__/locale-signin.spec.tsx
+++ b/web/app/signin/__tests__/_locale-menu.spec.tsx
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import LocaleSigninSelect from '../locale-signin'
+import LocaleMenu from '../_locale-menu'
const localeItems = [
{ value: 'en-US', name: 'English (US)' },
@@ -8,16 +8,15 @@ const localeItems = [
{ value: 'ja-JP', name: '日本語' },
]
-describe('LocaleSigninSelect', () => {
+describe('LocaleMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
})
- // Rendering behavior for selected value and fallback state.
describe('Rendering', () => {
it('should render selected locale name when value matches an item', () => {
render(
- {
it('should render trigger without selected label when value is not found', () => {
render(
- {
})
})
- // Menu interactions and callback behavior.
describe('User Interactions', () => {
it('should call onChange with selected locale value when clicking an option', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
render(
- {
)
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
- await user.click(screen.getByRole('menuitem', { name: '日本語' }))
+ await user.click(screen.getByRole('menuitemradio', { name: '日本語' }))
- expect(onChange).toHaveBeenCalledWith('ja-JP')
+ expect(onChange).toHaveBeenCalledWith('ja-JP', expect.anything())
})
it('should render all locale options when menu is opened', async () => {
const user = userEvent.setup()
render(
- {
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
- expect(screen.getByRole('menuitem', { name: 'English (US)' })).toBeInTheDocument()
- expect(screen.getByRole('menuitem', { name: '简体中文' })).toBeInTheDocument()
- expect(screen.getByRole('menuitem', { name: '日本語' })).toBeInTheDocument()
+ expect(screen.getByRole('menuitemradio', { name: 'English (US)' })).toBeInTheDocument()
+ expect(screen.getByRole('menuitemradio', { name: '简体中文' })).toBeInTheDocument()
+ expect(screen.getByRole('menuitemradio', { name: '日本語' })).toBeInTheDocument()
})
})
- // Edge behavior for missing callback and empty data.
describe('Edge Cases', () => {
it('should not throw when onChange is undefined and option is selected', async () => {
const user = userEvent.setup()
render(
- ,
)
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
- await user.click(screen.getByRole('menuitem', { name: '简体中文' }))
- // No assertion needed — test verifies no exception is thrown during selection without onChange.
+ await user.click(screen.getByRole('menuitemradio', { name: '简体中文' }))
+
+ expect(screen.getByRole('menuitemradio', { name: '简体中文' })).toBeInTheDocument()
})
it('should render no options when items are empty', async () => {
const user = userEvent.setup()
render(
- {
)
await user.click(screen.getByRole('button'))
- expect(screen.queryAllByRole('menuitem')).toHaveLength(0)
+
+ expect(screen.queryAllByRole('menuitemradio')).toHaveLength(0)
})
})
})
diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx
index 0b424d45c2..5b638b4208 100644
--- a/web/app/signin/_header.tsx
+++ b/web/app/signin/_header.tsx
@@ -2,12 +2,12 @@
import type { Locale } from '@/i18n-config'
import { useSuspenseQuery } from '@tanstack/react-query'
import Divider from '@/app/components/base/divider'
-import LocaleSigninSelect from '@/app/components/base/select/locale-signin'
import { useLocale } from '@/context/i18n'
import { setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
import dynamic from '@/next/dynamic'
import { systemFeaturesQueryOptions } from '@/service/system-features'
+import LocaleMenu from './_locale-menu'
// Avoid rendering the logo and theme selector on the server
const DifyLogo = dynamic(() => import('@/app/components/base/logo/dify-logo'), {
@@ -35,7 +35,7 @@ const Header = () => {
)
: }
-
item.supported)}
onChange={(value) => {
diff --git a/web/app/signin/_locale-menu.tsx b/web/app/signin/_locale-menu.tsx
new file mode 100644
index 0000000000..be63286b37
--- /dev/null
+++ b/web/app/signin/_locale-menu.tsx
@@ -0,0 +1,64 @@
+'use client'
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuRadioItemIndicator,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+
+type LocaleMenuProps = {
+ items: Array<{ value: string, name: string }>
+ value?: string
+ onChange?: (value: string) => void
+}
+
+export default function LocaleMenu({
+ items,
+ value,
+ onChange,
+}: LocaleMenuProps) {
+ const selectedItem = items.find(item => item.value === value)
+
+ return (
+
+
+
+
+
+ )}
+ >
+
+ {selectedItem?.name}
+
+
+
+
+
+ {items.map(item => (
+
+ {item.name}
+
+
+ ))}
+
+
+
+
+ )
+}