Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
425d09eb45
commit
f116be4f7f
@ -37,8 +37,9 @@
|
||||
по модулям и подсказками под полем; узкий экран с меткой проекта и сводной точкой состояния, модальная выезжающая
|
||||
панель; «Задачи» без горизонтальной прокрутки страницы, просрочка текстом; «Вики» открывает корень и показывает
|
||||
понятную шапку; мелкие тексты («Команды хаба», подсказка выбора проекта).
|
||||
- **Закрываемые follow-ups:** #94 (веб: T4.1, T4.2, T4.4, T4.5, T10.1–T10.7, T9.1), #90 (T7.5, T7.6), #73 (M-1…M-7),
|
||||
#68 (N-1, N-2, M-2, M-12, M-14, M-17, M-18, T5.3), #35 (M-9, O-16; T3.1/M-10 — отметить сделанным).
|
||||
- **Закрываемые follow-ups:** #94 (веб: T4.1, T4.2, T4.4, T4.5, T10.1–T10.7), #90 (T7.5, T7.6), #73 (M-1…M-7),
|
||||
#68 (N-1, N-2, M-2, M-12, M-14, M-17, T5.3), #35 (M-9, O-16; T3.1/M-10 — отметить сделанным). Остаются открытыми: #68 M-18 (заголовки разделов 16 против 24 px) и #94 T9.1 (лишний `GET /projects`) —
|
||||
в коде не закрыты.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ describe("оболочка", () => {
|
||||
|
||||
it("«Сменить токен» показывает экран входа", async () => {
|
||||
render(<App router={at("/app/jira/board")} />);
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Ещё" }));
|
||||
await userEvent.click(within(await screen.findByRole("complementary", { name: "Панель приложения" })).getByRole("button", { name: "Ещё" })); // #68 M-2: на доске есть ещё и «Ещё — доска»
|
||||
await userEvent.click(screen.getByRole("button", { name: "Сменить токен" }));
|
||||
expect(getAuth().token).toBe("");
|
||||
expect(screen.getByRole("heading", { name: "Вход в хаб" })).toBeInTheDocument();
|
||||
|
||||
@ -103,7 +103,7 @@ describe("страница доски", () => {
|
||||
hub();
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё — доска" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Экспорт JSON" }));
|
||||
const text = (screen.getByRole("textbox") as HTMLTextAreaElement).value;
|
||||
expect(JSON.parse(text)).toMatchObject({ format: "arm-ka-links/1", source: "Хаб" });
|
||||
@ -115,7 +115,7 @@ describe("страница доски", () => {
|
||||
localStorage.setItem("graph-pos:board", '{"AURORA-9001":{"x":1,"ry":40}}');
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё — доска" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Сбросить раскладку" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Да, сбросить" }));
|
||||
expect(localStorage.getItem("graph-pos:board")).toBeNull();
|
||||
@ -139,7 +139,7 @@ describe("страница доски", () => {
|
||||
await screen.findByText(/задач из 9/);
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const click = vi.spyOn(input, "click");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё — доска" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Загрузить файл…" }));
|
||||
expect(click).toHaveBeenCalled();
|
||||
});
|
||||
@ -170,8 +170,8 @@ describe("страница доски", () => {
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
expect(screen.getByRole("button", { name: "Обновить из Jira" })).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
|
||||
expect(screen.getByRole("button", { name: "Ещё" })).toHaveAttribute("aria-expanded", "true");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Ещё — доска" }));
|
||||
expect(screen.getByRole("button", { name: "Ещё — доска" })).toHaveAttribute("aria-expanded", "true");
|
||||
expect(screen.getByRole("button", { name: "Перечитать" })).toBeInTheDocument(); // меню действительно открыто
|
||||
expect(screen.queryByRole("button", { name: /^Тема/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -35,7 +35,7 @@ export function BoardHeader(p: BoardHeaderProps) {
|
||||
title="Сроки блокеров, задачи без исполнителя, передачи между людьми">
|
||||
Проблемы <Badge tone={p.problemCount ? "error" : "neutral"}>{p.problemCount}</Badge>
|
||||
</Button>
|
||||
<Menu label="Ещё">
|
||||
<Menu label="Ещё — доска">
|
||||
{(close) => (
|
||||
<>
|
||||
<MenuItem title="Загрузить снимок зеркала хаба заново, без обращения к Jira" onSelect={() => { close(); p.onReload(); }}>Перечитать</MenuItem>
|
||||
|
||||
@ -76,4 +76,12 @@ describe("Tree", () => {
|
||||
expect(item("D")).toHaveAttribute("aria-posinset", "2");
|
||||
expect(item("C")).toHaveAttribute("aria-setsize", "1");
|
||||
});
|
||||
|
||||
it("T10.6: узел выбранной страницы пришёл раньше родителя — предки раскрываются, когда цепочка до корня стала полной", () => {
|
||||
const { rerender } = render(<Tree nodes={[N("A", 0, null), N("C", 2, "B")]} selected="C" onOpen={() => {}} />);
|
||||
expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull();
|
||||
rerender(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
|
||||
expect(item("C")).toBeInTheDocument();
|
||||
expect(item("B")).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,11 +19,20 @@ export function Tree({ nodes, selected, onOpen }: { nodes: WikiNode[]; selected:
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
const refs = useRef(new Map<string, HTMLDivElement>());
|
||||
|
||||
// предки выбранной страницы раскрываются при смене selected и когда её узел впервые появился в nodes (дерево из кэша
|
||||
// дополнилось свежим набором). Новый массив nodes, добавление других узлов и ручное сворачивание родителя этого не
|
||||
// повторяют: selectedKnown меняется только с false на true. Актуальные узлы читаются через byCodeRef; его эффект
|
||||
// объявлен ВЫШЕ эффекта раскрытия, поэтому в одном проходе ref уже свежий
|
||||
const selectedKnown = !!selected && byCode.has(selected);
|
||||
// предки выбранной страницы раскрываются при смене selected и когда её цепочка предков до корня стала полной в nodes
|
||||
// (дерево из кэша дополнилось свежим набором). Новый массив nodes с теми же узлами, добавление посторонних узлов и ручное
|
||||
// сворачивание родителя раскрытия не повторяют: зависимость эффекта — булево selectedKnown, оно сменяется только при
|
||||
// появлении или пропаже цепочки. Актуальные узлы читаются через byCodeRef; его эффект объявлен ВЫШЕ эффекта раскрытия,
|
||||
// поэтому в одном проходе ref уже свежий
|
||||
const selectedKnown = (() => {
|
||||
if (!selected) return false;
|
||||
let n = byCode.get(selected);
|
||||
for (let guard = 0; n && guard <= nodes.length; guard++) {
|
||||
if (!n.parent_code) return true; // дошли до корня: цепочка полная
|
||||
n = byCode.get(n.parent_code);
|
||||
}
|
||||
return false; // узла или одного из предков в nodes ещё нет
|
||||
})();
|
||||
const byCodeRef = useRef(byCode);
|
||||
useEffect(() => { byCodeRef.current = byCode; });
|
||||
useEffect(() => {
|
||||
|
||||
@ -303,4 +303,17 @@ describe("боковая панель на узком экране", () => {
|
||||
await waitFor(() => expect(menu).toHaveFocus());
|
||||
expect(screen.getByRole("main")).not.toHaveAttribute("inert");
|
||||
});
|
||||
|
||||
it("#73 M-2: панель — модальное окно «Меню» (role=dialog, aria-modal), прокрутка страницы под ней заблокирована до закрытия", async () => {
|
||||
hub();
|
||||
open("/app/jira/board?project=AWPCA");
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Меню" }));
|
||||
const dlg = screen.getByRole("dialog", { name: "Меню" });
|
||||
expect(dlg).toHaveAttribute("aria-modal", "true");
|
||||
expect(within(dlg).getByRole("complementary", { name: "Панель приложения" })).toBeInTheDocument();
|
||||
expect(document.documentElement.style.overflow).toBe("hidden");
|
||||
await userEvent.keyboard("{Escape}");
|
||||
expect(screen.queryByRole("dialog", { name: "Меню" })).toBeNull();
|
||||
expect(document.documentElement.style.overflow).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@ -62,10 +62,11 @@ function NarrowShell(p: Props) {
|
||||
<SummaryDot />
|
||||
</header>
|
||||
<ModuleEventStrip />
|
||||
{open && <>
|
||||
<div className="sidebar-backdrop" data-testid="sidebar-backdrop" onClick={drawer.close} />
|
||||
<Panel {...p} drawer={drawer} />
|
||||
</>}
|
||||
{open && (
|
||||
<div className="sidebar-modal" role="dialog" aria-modal="true" aria-label="Меню">
|
||||
<div className="sidebar-backdrop" data-testid="sidebar-backdrop" onClick={drawer.close} />
|
||||
<Panel {...p} drawer={drawer} />
|
||||
</div>)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -85,9 +86,15 @@ function Panel({ section, project, drawer }: Props & { drawer?: { close(): void;
|
||||
const esc = (e: KeyboardEvent) => { if (e.key === "Escape" && !e.defaultPrevented) drawer.close(); };
|
||||
document.addEventListener("keydown", esc);
|
||||
// выезжающая панель модальна: страница и строка приложения недоступны с клавиатуры и для чтения с экрана
|
||||
const behind = [...document.querySelectorAll(".shell > :not(.sidebar):not(.sidebar-backdrop)")];
|
||||
const behind = [...document.querySelectorAll(".shell > :not(.sidebar-modal)")];
|
||||
behind.forEach((el) => el.setAttribute("inert", ""));
|
||||
return () => { document.removeEventListener("keydown", esc); behind.forEach((el) => el.removeAttribute("inert")); };
|
||||
// и страница под подложкой не прокручивается (#73 M-2)
|
||||
const root = document.documentElement, overflow = root.style.overflow;
|
||||
root.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", esc); behind.forEach((el) => el.removeAttribute("inert"));
|
||||
root.style.overflow = overflow;
|
||||
};
|
||||
}, [drawer]);
|
||||
const link = (s: NavItem, ref?: Ref<HTMLAnchorElement>) => (
|
||||
<Link key={s.to} ref={ref} to={href(s.to)} className="sidebar__link"
|
||||
|
||||
@ -82,3 +82,4 @@
|
||||
padding:var(--space-2) var(--space-4);border-bottom:1px solid var(--color-border-default);
|
||||
background:var(--color-bg-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;overflow-wrap:anywhere;
|
||||
}
|
||||
.sidebar-modal{display:contents} /* обёртка модального окна «Меню»: раскладку не меняет */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user