From 12ccd2b611b0a5de8aae6123e8326f4ed3bdffd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B4=BE=E5=A4=A7=E6=98=9F?= Date: Sun, 24 May 2026 00:53:28 +0800 Subject: [PATCH] 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 --- .../components/controls/ModelSelector.test.tsx | 17 +++++++++++++---- .../src/components/controls/ModelSelector.tsx | 9 +++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/desktop/src/components/controls/ModelSelector.test.tsx b/desktop/src/components/controls/ModelSelector.test.tsx index 62f65dab..78277703 100644 --- a/desktop/src/components/controls/ModelSelector.test.tsx +++ b/desktop/src/components/controls/ModelSelector.test.tsx @@ -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, diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index 0b38c88a..45e651d2 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -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, }}