diff --git a/openspec/changes/issue-card-modal/design.md b/openspec/changes/issue-card-modal/design.md index 1c481f8..a98e619 100644 --- a/openspec/changes/issue-card-modal/design.md +++ b/openspec/changes/issue-card-modal/design.md @@ -80,7 +80,8 @@ 13. **Элемент ``** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по - самому `` вне области содержимого. + самому `` вне области содержимого, причём нажатие (`pointerdown`) тоже было на фоне (I-2: выделение текста, + отпущенное за краем окна, иначе закрывало окно). Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю ширину. diff --git a/openspec/changes/issue-card-modal/specs/web-app/spec.md b/openspec/changes/issue-card-modal/specs/web-app/spec.md index f10e298..a5b8a05 100644 --- a/openspec/changes/issue-card-modal/specs/web-app/spec.md +++ b/openspec/changes/issue-card-modal/specs/web-app/spec.md @@ -80,8 +80,10 @@ ### Requirement: Карточка задачи Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры, -загруженные строки, прокрутку и раскладку. Открытый напрямую адрес (новая вкладка, закладка, перезагрузка) SHALL -показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону и +загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую +адрес (новая вкладка, закладка) SHALL +показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста, +отпущенное за краем окна, окно не закрывает) и возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад» браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не diff --git a/web/src/issues/IssueModal.test.tsx b/web/src/issues/IssueModal.test.tsx index 936a6be..4a07f01 100644 --- a/web/src/issues/IssueModal.test.tsx +++ b/web/src/issues/IssueModal.test.tsx @@ -75,6 +75,18 @@ describe("карточка окном", () => { expect(router.state.location.pathname).toBe("/app/issues"); }); + it("I-2: выделение текста внутри окна, отпущенное над фоном, окно не закрывает", async () => { + const router = at(["/app/issues"]); + render(); + await userEvent.click(await screen.findByRole("link", { name: "AURORA-9001" })); + const title = await within(dialog()).findByRole("heading", { name: /AURORA-9001/ }); + fireEvent.pointerDown(title); // нажали на тексте карточки… + fireEvent.pointerUp(dialog()); + fireEvent.click(dialog()); // …отпустили над фоном: click — на + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(router.state.location.pathname).toBe("/app/issues/AURORA-9001"); + }); + it("с доски: окно поверх доски, вкладка «Доска» активна, после закрытия доска та же", async () => { const router = at(["/app/board"]); render(); diff --git a/web/src/issues/IssueModal.tsx b/web/src/issues/IssueModal.tsx index 25b8b3d..a4f7134 100644 --- a/web/src/issues/IssueModal.tsx +++ b/web/src/issues/IssueModal.tsx @@ -8,6 +8,9 @@ import { IssueCard } from "./IssueCard.tsx"; // пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок). export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) { const ref = useRef(null); + // I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна, + // даёт click на самом , но нажатие было внутри содержимого + const downOnBackdrop = useRef(false); useEffect(() => { const d = ref.current; if (d && !d.open) d.showModal(); @@ -19,7 +22,12 @@ export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: ( { if (e.cancelable) { e.preventDefault(); onClose(); } }} onClose={() => { const d = ref.current; if (d?.isConnected) { d.showModal(); onClose(); } }} - onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}> + onPointerDown={(e) => { downOnBackdrop.current = e.target === e.currentTarget; }} + onClick={(e) => { + const fromBackdrop = downOnBackdrop.current; + downOnBackdrop.current = false; + if (e.target === e.currentTarget && fromBackdrop) onClose(); + }}> );