fix(web): выделение текста, отпущенное за краем окна, не закрывает карточку (I-2); спека: перезагрузка сохраняет страницу под окном (I-1) (#37)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
901a2c1cb7
commit
16835abbf8
@ -80,7 +80,8 @@
|
|||||||
13. **Элемент `<dialog>`** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем
|
13. **Элемент `<dialog>`** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем
|
||||||
сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации
|
сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации
|
||||||
пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по
|
пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по
|
||||||
самому `<dialog>` вне области содержимого.
|
самому `<dialog>` вне области содержимого, причём нажатие (`pointerdown`) тоже было на фоне (I-2: выделение текста,
|
||||||
|
отпущенное за краем окна, иначе закрывало окно).
|
||||||
Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна
|
Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна
|
||||||
прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю
|
прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю
|
||||||
ширину.
|
ширину.
|
||||||
|
|||||||
@ -80,8 +80,10 @@
|
|||||||
### Requirement: Карточка задачи
|
### Requirement: Карточка задачи
|
||||||
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
|
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
|
||||||
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
|
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
|
||||||
загруженные строки, прокрутку и раскладку. Открытый напрямую адрес (новая вкладка, закладка, перезагрузка) SHALL
|
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую
|
||||||
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону и
|
адрес (новая вкладка, закладка) SHALL
|
||||||
|
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста,
|
||||||
|
отпущенное за краем окна, окно не закрывает) и
|
||||||
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
|
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
|
||||||
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
|
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
|
||||||
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не
|
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не
|
||||||
|
|||||||
@ -75,6 +75,18 @@ describe("карточка окном", () => {
|
|||||||
expect(router.state.location.pathname).toBe("/app/issues");
|
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 () => {
|
it("с доски: окно поверх доски, вкладка «Доска» активна, после закрытия доска та же", async () => {
|
||||||
const router = at(["/app/board"]);
|
const router = at(["/app/board"]);
|
||||||
render(<App router={router} />);
|
render(<App router={router} />);
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import { IssueCard } from "./IssueCard.tsx";
|
|||||||
// пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок).
|
// пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок).
|
||||||
export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) {
|
export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) {
|
||||||
const ref = useRef<HTMLDialogElement>(null);
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
|
// I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна,
|
||||||
|
// даёт click на самом <dialog>, но нажатие было внутри содержимого
|
||||||
|
const downOnBackdrop = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const d = ref.current;
|
const d = ref.current;
|
||||||
if (d && !d.open) d.showModal();
|
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}
|
<dialog ref={ref} className="issue-modal" aria-labelledby="issue-modal-title" aria-label={issueKey}
|
||||||
onCancel={(e) => { if (e.cancelable) { e.preventDefault(); onClose(); } }}
|
onCancel={(e) => { if (e.cancelable) { e.preventDefault(); onClose(); } }}
|
||||||
onClose={() => { const d = ref.current; if (d?.isConnected) { d.showModal(); 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>
|
<ErrorBoundary key={issueKey}><IssueCard issueKey={issueKey} onClose={onClose} /></ErrorBoundary>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user