fix: keep project dialog close controls clickable
Render project creation and settings dialogs in a body portal so the Electron title-bar drag region does not intercept their close controls. Cover dialog placement, scrolling, and focus restoration, and document the layout rule. Release note: Project creation and settings dialogs keep their close buttons clickable after scrolling and restore focus to their trigger when closed.
This commit is contained in:
@@ -709,6 +709,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
|
||||
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
|
||||
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
|
||||
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
|
||||
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
|
||||
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
|
||||
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
|
||||
|
||||
@@ -354,6 +354,23 @@ describe('ProjectSwitcher project activity integration', () => {
|
||||
})
|
||||
|
||||
describe('ProjectSwitcher runtime fields', () => {
|
||||
it.each(['新建项目', '项目设置'])('keeps %s outside the sidebar and restores focus after closing', (name) => {
|
||||
renderSwitcher()
|
||||
const trigger = screen.getByLabelText(name)
|
||||
fireEvent.click(trigger)
|
||||
const dialog = screen.getByRole('dialog', { name })
|
||||
expect(dialog.parentElement?.parentElement).toBe(document.body)
|
||||
const body = dialog.querySelector('.project-create-card__body')!
|
||||
const close = within(dialog).getByRole('button', { name: `关闭${name}` })
|
||||
expect(body).toContainElement(within(dialog).getByLabelText('名称'))
|
||||
expect(body).not.toContainElement(close)
|
||||
expect(body).not.toContainElement(within(dialog).getByRole('button', { name: '取消' }))
|
||||
fireEvent.scroll(body, { target: { scrollTop: 500 } })
|
||||
fireEvent.click(close)
|
||||
expect(screen.queryByRole('dialog', { name })).not.toBeInTheDocument()
|
||||
expect(trigger).toHaveFocus()
|
||||
})
|
||||
|
||||
it('creates an ordinary project with DeepSeek Harness', async () => {
|
||||
const { onCreate } = renderSwitcher()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
AssistantProject,
|
||||
@@ -1413,7 +1414,7 @@ export function ProjectSwitcher({
|
||||
<Settings size={15} />
|
||||
</button>
|
||||
</div>
|
||||
{dialogMode && (
|
||||
{dialogMode && createPortal(
|
||||
<div
|
||||
className="project-create-backdrop"
|
||||
onMouseDown={(event) => {
|
||||
@@ -2139,7 +2140,8 @@ export function ProjectSwitcher({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user