From 16835abbf8090507a1be10cf96a1f89920596fa2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 15:17:33 +0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0?= =?UTF-8?q?,=20=D0=BE=D1=82=D0=BF=D1=83=D1=89=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=20=D0=BA=D1=80=D0=B0=D0=B5=D0=BC=20=D0=BE?= =?UTF-8?q?=D0=BA=D0=BD=D0=B0,=20=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D1=83=20(I-2);=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0:?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=20=D0=BE=D0=BA=D0=BD=D0=BE=D0=BC=20(I-1)=20(?= =?UTF-8?q?#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/issue-card-modal/design.md | 3 ++- .../changes/issue-card-modal/specs/web-app/spec.md | 6 ++++-- web/src/issues/IssueModal.test.tsx | 12 ++++++++++++ web/src/issues/IssueModal.tsx | 10 +++++++++- 4 files changed, 27 insertions(+), 4 deletions(-) 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(); + }}> );