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:
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user