fix: use bottom-anchored positioning for above-trigger dropdown in ModelSelector

When the dropdown opens above the trigger button, use CSS bottom instead
of top to prevent a gap between the dropdown and the button when content
is shorter than maxHeight.

Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
This commit is contained in:
派大星 2026-05-24 00:53:28 +08:00
parent aa1321a02b
commit 12ccd2b611
2 changed files with 18 additions and 8 deletions

View File

@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
@ -34,6 +34,12 @@ afterEach(() => {
useHahaOpenAIOAuthStore.setState(useHahaOpenAIOAuthStore.getInitialState(), true)
})
// Prevent real API calls from fetchStatus on mount
beforeEach(() => {
useHahaOAuthStore.setState({ fetchStatus: async () => {} })
useHahaOpenAIOAuthStore.setState({ fetchStatus: async () => {} })
})
describe('ModelSelector', () => {
it('uses controlled model selection without mutating settings directly', async () => {
const onChange = vi.fn()
@ -139,7 +145,10 @@ describe('ModelSelector', () => {
},
]
const setSessionRuntime = vi.fn()
useHahaOpenAIOAuthStore.setState({ status: { loggedIn: true, expiresAt: null, email: null, accountId: null } })
useHahaOpenAIOAuthStore.setState({
status: { loggedIn: true, expiresAt: null, email: null, accountId: null },
fetchStatus: async () => {},
})
useSettingsStore.setState({
locale: 'en',
availableModels: openAIModels,
@ -175,8 +184,8 @@ describe('ModelSelector', () => {
})
it('hides official provider sections when OAuth is not logged in', async () => {
useHahaOAuthStore.setState({ status: { loggedIn: false } })
useHahaOpenAIOAuthStore.setState({ status: { loggedIn: false } })
useHahaOAuthStore.setState({ status: { loggedIn: false }, fetchStatus: async () => {} })
useHahaOpenAIOAuthStore.setState({ status: { loggedIn: false }, fetchStatus: async () => {} })
useSettingsStore.setState({
locale: 'en',
availableModels: MODELS,

View File

@ -38,7 +38,8 @@ type Props = {
}
type DropdownPosition = {
top: number
top: number | undefined
bottom: number | undefined
left: number
width: number
maxHeight: number
@ -267,9 +268,8 @@ export function ModelSelector({
const maxHeight = Math.min(DROPDOWN_MAX_HEIGHT, availableHeight)
setDropdownPosition({
top: placeBelow
? rect.bottom + DROPDOWN_GAP
: Math.max(VIEWPORT_MARGIN, rect.top - DROPDOWN_GAP - maxHeight),
top: placeBelow ? rect.bottom + DROPDOWN_GAP : undefined,
bottom: placeBelow ? undefined : (viewportHeight - rect.top + DROPDOWN_GAP),
left,
width,
maxHeight,
@ -535,6 +535,7 @@ export function ModelSelector({
className="fixed z-[80] rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] shadow-[var(--shadow-dropdown)]"
style={{
top: dropdownPosition.top,
bottom: dropdownPosition.bottom,
left: dropdownPosition.left,
width: dropdownPosition.width,
}}