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 的点击区域。
|
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
|
||||||
|
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
|
||||||
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
|
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
|
||||||
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
|
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
|
||||||
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
|
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
|
||||||
|
|||||||
@@ -354,6 +354,23 @@ describe('ProjectSwitcher project activity integration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('ProjectSwitcher runtime fields', () => {
|
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 () => {
|
it('creates an ordinary project with DeepSeek Harness', async () => {
|
||||||
const { onCreate } = renderSwitcher()
|
const { onCreate } = renderSwitcher()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { LucideIcon } from 'lucide-react'
|
import type { LucideIcon } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type {
|
import type {
|
||||||
AssistantProject,
|
AssistantProject,
|
||||||
@@ -1413,7 +1414,7 @@ export function ProjectSwitcher({
|
|||||||
<Settings size={15} />
|
<Settings size={15} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{dialogMode && (
|
{dialogMode && createPortal(
|
||||||
<div
|
<div
|
||||||
className="project-create-backdrop"
|
className="project-create-backdrop"
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
@@ -2139,7 +2140,8 @@ export function ProjectSwitcher({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user