fix(web): выделение текста, отпущенное за краем окна, не закрывает карточку (I-2); спека: перезагрузка сохраняет страницу под окном (I-1) (#37)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-15 15:17:33 +04:00
parent 901a2c1cb7
commit 16835abbf8
4 changed files with 27 additions and 4 deletions

View File

@ -80,7 +80,8 @@
13. **Элемент `<dialog>`** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем
сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации
пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по
самому `<dialog>` вне области содержимого.
самому `<dialog>` вне области содержимого, причём нажатие (`pointerdown`) тоже было на фоне (I-2: выделение текста,
отпущенное за краем окна, иначе закрывало окно).
Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна
прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю
ширину.

View File

@ -80,8 +80,10 @@
### Requirement: Карточка задачи
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
загруженные строки, прокрутку и раскладку. Открытый напрямую адрес (новая вкладка, закладка, перезагрузка) SHALL
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону и
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую
адрес (новая вкладка, закладка) SHALL
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста,
отпущенное за краем окна, окно не закрывает) и
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не

View File

@ -75,6 +75,18 @@ describe("карточка окном", () => {
expect(router.state.location.pathname).toBe("/app/issues");
});
it("I-2: выделение текста внутри окна, отпущенное над фоном, окно не закрывает", async () => {
const router = at(["/app/issues"]);
render(<App router={router} />);
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 — на <dialog>
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(router.state.location.pathname).toBe("/app/issues/AURORA-9001");
});
it("с доски: окно поверх доски, вкладка «Доска» активна, после закрытия доска та же", async () => {
const router = at(["/app/board"]);
render(<App router={router} />);

View File

@ -8,6 +8,9 @@ import { IssueCard } from "./IssueCard.tsx";
// пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок).
export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) {
const ref = useRef<HTMLDialogElement>(null);
// I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна,
// даёт click на самом <dialog>, но нажатие было внутри содержимого
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: (
<dialog ref={ref} className="issue-modal" aria-labelledby="issue-modal-title" aria-label={issueKey}
onCancel={(e) => { 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();
}}>
<ErrorBoundary key={issueKey}><IssueCard issueKey={issueKey} onClose={onClose} /></ErrorBoundary>
</dialog>
);