web: fold all keeps the top-level row open
COLLAPSE ALL folded the top-level row as well, and that row is already its own
fold: one click on its name shuts it and takes the whole subtree with it. Folding
it from the menu hid every row of the tree — the item repeated a click the reader
already had, while the one thing a click there cannot reach, the folders nested
below, was the part that got lost with it.
So the item folds the folders nested below the top level: every row with a
subfolder under it that is not itself a folder picked at the top level. The
top-level row stays drawn and stays open, its own subfolders are drawn folded,
and no top-level row ever leaves the column. `nested` is computed off `parents`
once, and the item is disabled on a folder with nothing nested under it.
Verified:
library-check.mjs — 35 steps, all passed. The fold step now reads the rows the
click leaves behind instead of a row count: the fully open roll (4 rows)
becomes exactly CheckRoll aria-expanded=true, CheckRoll/2026
aria-expanded=false, CheckRoll/Empty (a leaf, so no aria-expanded) —
CheckRoll/2026/04 is away, the top-level row is not — with the strip's 2
tiles unmoved and the row menu still leading with lib-menu-collapse.
frontend tsc --noEmit clean. Live 8090 on index-DAIpyDNV.js matching dist/:
/, /library and /app 200 with 0 console errors.
ponytail: with several folders picked, one COLLAPSE ALL folds the nested rows of
all of them, not only the row that was clicked — no top-level row is hidden
either way; scope it to the clicked folder's prefix when a column routinely
holds many picked folders.
This commit is contained in:
@@ -374,11 +374,12 @@ await page.keyboard.press('Escape');
|
||||
step('escape closes the folder menu too', (await page.$('[data-key="lib-menu"]')) === null);
|
||||
|
||||
// The head of the tree is the whole tree's control: a right click on the column's
|
||||
// own name folds every row that has something under it, so a roll read to the
|
||||
// bottom of its dates comes back to its own line. The frames in the strip are the
|
||||
// strip's business and do not move with it. The subfolder opened above carries a
|
||||
// fold of its own, so it is opened first: the claim is what ONE click does to
|
||||
// every row at once, not to the one that was already shut.
|
||||
// own name, or on the row of a folder picked at the top level, folds the folders
|
||||
// nested below the top level. The top-level row itself stays open — one click on
|
||||
// its name already shuts it, so the menu item would only repeat that click — and
|
||||
// the frames in the strip are the strip's business and do not move with the rows.
|
||||
// The subfolder opened above carries a fold of its own, so it is opened first:
|
||||
// the claim is what ONE click does to every row at once, not to one already shut.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 4, null, {
|
||||
timeout: 10_000,
|
||||
@@ -407,29 +408,21 @@ step(
|
||||
await page.getAttribute('[data-key="lib-root-name"]', 'title')
|
||||
);
|
||||
await page.click('[data-key="lib-menu-collapse"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 1, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const flatTiles = await page.$$eval(thumbs, (els) => els.length);
|
||||
const shutRoot = await page.getAttribute('[data-key="lib-node-CheckRoll"]', 'aria-expanded');
|
||||
// Opening the root back up is the ruler for the rest: only the rows the fold shut
|
||||
// are still away, and the subfolder two deep is one of them.
|
||||
await page.click('[data-key="lib-node-CheckRoll"]');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node-"]').length === 3, null, {
|
||||
timeout: 10_000,
|
||||
polling: 50,
|
||||
});
|
||||
const revealed = await page.$$eval('[data-key^="lib-node-"]', (els) => els.map((e) => e.dataset.key));
|
||||
step(
|
||||
'and one click folds every row that has something under it',
|
||||
deepRows === 4 && shutRoot === 'false' && flatTiles === deepTiles && revealed.join(',') === 'lib-node-CheckRoll,lib-node-CheckRoll/2026,lib-node-CheckRoll/Empty',
|
||||
`${deepRows} rows → 1, ${revealed.length} back with the root opened, ${flatTiles} tiles kept`,
|
||||
const flatTiles = await page.$$eval(thumbs, (els) => els.length);
|
||||
const ended = await page.$$eval('[data-key^="lib-node-"]', (els) =>
|
||||
els.map((e) => `${e.dataset.key}:${e.getAttribute('aria-expanded') ?? '-'}`)
|
||||
);
|
||||
step(
|
||||
'and one click folds the subfolders, leaving the top-level row open',
|
||||
deepRows === 4 &&
|
||||
flatTiles === deepTiles &&
|
||||
ended.join(' ') === 'lib-node-CheckRoll:true lib-node-CheckRoll/2026:false lib-node-CheckRoll/Empty:-',
|
||||
`${deepRows} rows → ${ended.length}, ${ended.join(' ')}, ${flatTiles} tiles kept`,
|
||||
);
|
||||
// The open folder is the visit's, not the fold's, so it is put back where the
|
||||
// sections below left it.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 });
|
||||
|
||||
// The divider drags the column, and what the visit leaves behind — the width the
|
||||
// column was left at and the folder that was open — is what the next visit opens
|
||||
|
||||
@@ -109,7 +109,7 @@ export function Library() {
|
||||
// right click landed on the empty part of the column, which is the way in for
|
||||
// one more folder; `root` means it landed on the head of a tree — the column's
|
||||
// own name, or the row of a folder picked at the top level — which carries
|
||||
// folding every row that has something under it.
|
||||
// folding the folders nested below the top level.
|
||||
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; x: number; y: number } | null>(null);
|
||||
// Where the drag of the column divider started, and how wide the column was
|
||||
// then — the pointer only ever reports a position, not a distance moved.
|
||||
@@ -355,6 +355,11 @@ export function Library() {
|
||||
[nodes]
|
||||
);
|
||||
|
||||
// The folders nested below a top-level row — what COLLAPSE ALL has to fold. A
|
||||
// top-level row is not one of them: it is its own fold, since one click on its
|
||||
// name shuts it, so the menu item would only repeat the row's own click.
|
||||
const nested = useMemo(() => new Set([...parents].filter((key) => key.includes('/'))), [parents]);
|
||||
|
||||
// What the column draws: a folded row takes its whole subtree with it.
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
@@ -517,7 +522,7 @@ export function Library() {
|
||||
{/* The column is titled with the folder the tree belongs to, so
|
||||
a renamed roll is named where its frames are named. The name
|
||||
is the whole tree's handle as well: one right click here
|
||||
folds every row that has something under it. */}
|
||||
folds the folders nested below the top level. */}
|
||||
<b
|
||||
className="lib-col-title"
|
||||
data-key="lib-root-name"
|
||||
@@ -566,9 +571,9 @@ export function Library() {
|
||||
// act on the folder that was picked, so a right click on a
|
||||
// subfolder row brings up its roll's menu. A row at the top
|
||||
// level is a whole tree's head, so it also carries folding
|
||||
// the tree — the item the column's own name has, where a
|
||||
// reader actually right-clicks. Shift+F10 raises the same
|
||||
// event, which is the keyboard's way in.
|
||||
// what sits under it — the item the column's own name has,
|
||||
// where a reader actually right-clicks. Shift+F10 raises
|
||||
// the same event, which is the keyboard's way in.
|
||||
onContextMenu={(e) => {
|
||||
if (!folder) return;
|
||||
e.preventDefault();
|
||||
@@ -725,18 +730,18 @@ export function Library() {
|
||||
{menu ? (
|
||||
<div className="lib-menu" data-key="lib-menu" style={{ left: menu.x, top: menu.y }} onContextMenu={(e) => e.preventDefault()}>
|
||||
{/* Fold-the-tree leads the menu of a top-level row, and stands alone on
|
||||
the column's own name: every row that has something under it goes,
|
||||
so a long roll comes back to one line per folder and the click that
|
||||
folds a row still opens it again one at a time. */}
|
||||
the column's own name: the folders nested below the top level go,
|
||||
one line per folder, and no top-level row ever leaves the column —
|
||||
the row itself is one click away and its click already shuts it. */}
|
||||
{menu.root ? (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key="lib-menu-collapse"
|
||||
disabled={parents.size === 0}
|
||||
disabled={nested.size === 0}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
setFolded(new Set(parents));
|
||||
setFolded(new Set(nested));
|
||||
}}
|
||||
>
|
||||
{t('lib.collapseAll')}
|
||||
|
||||
Reference in New Issue
Block a user