fix: widen project picker beyond the sidebar

Float the project picker at 380px with viewport bounds and preserve menu focus and dismissal. Align local package metadata and packaged release notes with Desktop 0.13.13.

Release note: 项目下拉列表可浮出侧栏,宽度调整为 380px,窄窗口自动收窄。
This commit is contained in:
mesalogo
2026-09-24 00:34:08 +08:00
parent d5f6dc6990
commit 0ed1be1773
7 changed files with 97 additions and 11 deletions
+27
View File
@@ -354,6 +354,33 @@ describe('ProjectSwitcher project activity integration', () => {
})
describe('ProjectSwitcher runtime fields', () => {
it('floats the project menu outside the sidebar and keeps menu interactions open', async () => {
renderSwitcher()
const trigger = screen.getByRole('button', { name: '当前项目' })
fireEvent.click(trigger)
const menu = screen.getByRole('menu')
expect(trigger.closest('.project-switcher')).not.toContainElement(menu)
expect(menu.closest('.floating-portal')?.parentElement).toBe(document.body)
expect(menu.style.width).toBe('380px')
const selected = within(menu).getByRole('menuitemradio', { checked: true })
await waitFor(() => expect(selected).toHaveFocus())
fireEvent.pointerDown(selected)
expect(menu).toBeInTheDocument()
const width = vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(360)
fireEvent(window, new Event('resize'))
expect(menu.style.width).toBe('328px')
expect(menu.style.left).toBe('16px')
width.mockRestore()
fireEvent.keyDown(selected, { key: 'Escape' })
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
expect(trigger).toHaveFocus()
fireEvent.click(trigger)
fireEvent.pointerDown(document.body)
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
})
it.each(['新建项目', '项目设置'])('keeps %s outside the sidebar and restores focus after closing', (name) => {
renderSwitcher()
const trigger = screen.getByRole('button', {
+31 -2
View File
@@ -16,7 +16,7 @@ import {
X
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import type {
@@ -57,6 +57,7 @@ import { ProjectWorkModeFields } from './ProjectWorkModeFields'
import { ChannelIcon } from './ChannelIcon'
import { SegmentedControl } from './WorkspacePrimitives'
import { InlineHelp } from './InlineHelp'
import { FloatingPortal } from './FloatingPortal'
import { displayErrorMessage } from './error-message'
type ProjectSwitcherProps = {
@@ -739,6 +740,31 @@ export function ProjectSwitcher({
}
}
useLayoutEffect(() => {
if (!projectMenuOpen) return
const anchor = projectPickerButtonRef.current
const menu = projectPickerMenuRef.current
if (!anchor || !menu) return
const position = (): void => {
const rect = anchor.getBoundingClientRect()
const width = Math.min(380, window.innerWidth - 32)
menu.style.width = `${width}px`
menu.style.left = `${Math.max(16, Math.min(rect.left, window.innerWidth - width - 16))}px`
menu.style.top = `${Math.max(16, Math.min(rect.bottom + 8, window.innerHeight - menu.offsetHeight - 16))}px`
}
position()
const observer = new ResizeObserver(position)
observer.observe(anchor)
observer.observe(menu)
window.addEventListener('resize', position)
window.addEventListener('scroll', position, true)
return () => {
observer.disconnect()
window.removeEventListener('resize', position)
window.removeEventListener('scroll', position, true)
}
}, [projectMenuOpen])
useEffect(() => {
if (!projectMenuOpen) {
return
@@ -757,7 +783,8 @@ export function ProjectSwitcher({
const closeOutside = (event: Event): void => {
if (
event.target instanceof Node &&
!projectPickerRef.current?.contains(event.target)
!projectPickerRef.current?.contains(event.target) &&
!projectPickerMenuRef.current?.contains(event.target)
) {
setProjectMenuOpen(false)
}
@@ -1202,6 +1229,7 @@ export function ProjectSwitcher({
<ChevronDown aria-hidden="true" size={14} />
</button>
{projectMenuOpen && (
<FloatingPortal anchorRef={projectPickerButtonRef}>
<div
aria-label={t('projectSwitcher.selector.ariaLabel')}
className="project-switcher__menu"
@@ -1337,6 +1365,7 @@ export function ProjectSwitcher({
</div>
)}
</div>
</FloatingPortal>
)}
</div>
<button
+7 -6
View File
@@ -2724,18 +2724,19 @@ textarea:focus-visible {
}
.project-switcher__menu {
position: absolute;
z-index: 30;
top: calc(var(--control-height) + var(--space-2));
right: 0;
left: 0;
position: fixed;
-webkit-app-region: no-drag;
z-index: var(--z-dialog);
display: grid;
max-height: min(420px, calc(100vh - 160px));
max-height: min(420px, calc(100dvh - 32px));
max-width: calc(100vw - 32px);
padding: var(--space-2);
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
overflow-y: auto;
background: var(--surface-raised);
color: var(--text-primary);
font-size: var(--font-body);
box-shadow: var(--shadow-dialog);
gap: var(--space-2);
}