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

View File

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