From 39bab0bb006042448535a7ecb7062aaef9bf78ca Mon Sep 17 00:00:00 2001 From: Yuriy <17292315+ykamendrovskiy@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:12:29 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(folders):=20align=20rail=20counts=20flu?= =?UTF-8?q?sh=20right,=20the=20=E2=8B=AF=20menu=20takes=20their=20place=20?= =?UTF-8?q?on=20hover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trailing ⋯ slot reserved a 24px column beside every count, holding the numbers ~30px off the rail's edge even with no menu showing, and a long name's ellipsis stopped at that box rather than at the digits. The slot is now exactly as wide as its count — digits sit flush at the row padding, a plain flex-gap from the name — and on hover/focus the menu takes the count's place: parked out of flow (invisible, click-through) at rest, it re-enters the flow to reveal, so the slot re-sizes to the 24px button and the name re-flows to make the room. The All Notes badge lines up as the row's last flex item, so the spacer is gone entirely. The selected row holds its ⋯ permanently, as the selected note-list item holds its actions — at the price of its count (a debatable trade, noted in a comment). Beyond that, the reveal listens to the row's own focus, this row's menu being open (a row_menu-open modifier), or keyboard focus on the ⋯ itself — Tab steps from a focused row into its menu button, as in the note list. The parked state is never display: none (a Tab target that loses its box mid-hand-over gets dropped, stranding focus on body), and the button leg uses :focus-visible, not :focus: closing the menu can strand pointer-given focus on the button (the panel divider's pointerdown preventDefault suppresses the blurring focus move), which must not pin the reveal. Co-Authored-By: Claude Fable 5 --- src/components/FolderRail.css | 67 ++++++++++++++++++++++++------ src/components/FolderRail.test.tsx | 23 ++++++++++ src/components/FolderRail.tsx | 23 ++++++---- 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/components/FolderRail.css b/src/components/FolderRail.css index 858d9fa..8578dd6 100644 --- a/src/components/FolderRail.css +++ b/src/components/FolderRail.css @@ -106,7 +106,7 @@ min-width: 0; } -/* The note-count badge: quiet, tabular, right-aligned at a steady inset. */ +/* The note-count badge: quiet, tabular, flush with the row's edge padding. */ .folder-rail__count { flex-shrink: 0; color: var(--g-color-text-hint); @@ -115,29 +115,70 @@ font-variant-numeric: tabular-nums; } -/* The ⋯ menu reveals on hover/focus in a fixed-width trailing slot (the width of the s icon button), - present on every row — incl. the empty spacer on "All Notes" — so the count keeps a single aligned - position across all rows and stays visible when the menu appears (it no longer takes the count's place). */ -.folder-rail__actions, -.folder-rail__actions-spacer { +/* The trailing slot holds the count OR the ⋯ menu, sized by whichever is showing. At rest it is + exactly as wide as the count (nothing when there is none), so the name's ellipsis boundary sits + a plain flex-gap from the digits and no gutter is reserved for the menu. On hover/focus the menu + takes the count's place — display, not opacity, so the slot re-sizes to the 24px button and the + name re-flows to make the room. The count rules are scoped to the slot, keeping the All Notes + badge (no menu to reveal) always visible. */ +.folder-rail__trailing { + position: relative; flex-shrink: 0; - width: 24px; + display: flex; + align-items: center; } -.folder-rail__actions { - display: flex; - justify-content: center; +/* Parked state: the ⋯ sits out of flow (so the slot hugs the count and the name's ellipsis stops + a plain gap from the digits), invisible and click-through — but still rendered and focusable, + so Tab can step from the row into it. Never display: none here: a Tab target that loses its box + while focus is being handed over gets dropped, stranding focus on body. The 5px pull-in is felt + only in the revealed (in-flow) state: the 24px button carries ~7px of air before its glyph, so + without it the name-to-⋯ distance reads wider than the name-to-count it replaces. */ +.folder-rail__trailing .folder-rail__actions { + position: absolute; + inset-inline-end: 0; opacity: 0; + pointer-events: none; + margin-inline-start: -5px; +} + +/* Reveal — into the flow — on the selected row, the row's own focus (keyboard nav), while this + row's menu is open, or while the ⋯ itself holds keyboard focus (Tab parity with the note + list's ⋯). The button leg is :focus-visible, NOT plain :focus: closing the menu can strand + pointer-given focus on the button (the panel divider's pointerdown calls preventDefault, + suppressing the focus move that would blur it), and a plain-focus reveal would pin the swap + after the pointer leaves. A stranded button merely stays parked: invisible, inert, and it + yields on the next focus move. + + The selected row holding its ⋯ permanently costs it its count — the one folder whose count is + never shown is the one you are in. A debatable trade, challenge welcome; without it the rail + felt inconsistent beside the note list, whose selected item holds its actions the same way. */ +.folder-rail__row_selected .folder-rail__trailing .folder-rail__actions, +.folder-rail__row:focus .folder-rail__trailing .folder-rail__actions, +.folder-rail__row_menu-open .folder-rail__trailing .folder-rail__actions, +.folder-rail__trailing .folder-rail__actions:focus-visible { + position: static; + opacity: 1; + pointer-events: auto; } -.folder-rail__row:focus-within .folder-rail__actions { - opacity: 1; +.folder-rail__row_selected .folder-rail__trailing .folder-rail__count, +.folder-rail__row:focus .folder-rail__trailing .folder-rail__count, +.folder-rail__row_menu-open .folder-rail__trailing .folder-rail__count, +.folder-rail__trailing:has(.folder-rail__actions:focus-visible) .folder-rail__count { + display: none; } /* Hover-reveal, pointer-only: on touch this revealed content is what swallowed the tap. */ @media (hover: hover) { - .folder-rail__row:hover .folder-rail__actions { + .folder-rail__row:hover .folder-rail__trailing .folder-rail__actions { + position: static; opacity: 1; + pointer-events: auto; + } + + .folder-rail__row:hover .folder-rail__trailing .folder-rail__count { + display: none; } } diff --git a/src/components/FolderRail.test.tsx b/src/components/FolderRail.test.tsx index 42aafa1..bc581be 100644 --- a/src/components/FolderRail.test.tsx +++ b/src/components/FolderRail.test.tsx @@ -180,6 +180,29 @@ describe('FolderRail — folder actions', () => { expect(props.onTogglePin).toHaveBeenCalledWith('Work'); }); + it('Tab steps from a focused row into its ⋯, then on to New Folder', async () => { + const user = userEvent.setup(); + setup({rows: [folder('Work')]}); + screen.getByRole('treeitem', {name: /Work/}).focus(); + await user.tab(); + expect(screen.getByRole('button', {name: 'Work actions'})).toHaveFocus(); + await user.tab(); + expect(screen.getByRole('button', {name: 'New folder'})).toHaveFocus(); + }); + + it('marks the row while its menu is open, and unmarks it on close', async () => { + const user = userEvent.setup(); + setup(); + const row = screen.getByRole('treeitem', {name: /Work/}); + expect(row).not.toHaveClass('folder-rail__row_menu-open'); + await user.click(screen.getByRole('button', {name: 'Work actions'})); + expect(row).toHaveClass('folder-rail__row_menu-open'); + // Acting on an item closes the menu — the mark (which keeps the ⋯ revealed while the + // menu is up, wherever the pointer is) must clear with it, not linger on button focus. + await user.click(await screen.findByRole('menuitem', {name: /Pin to top/})); + expect(row).not.toHaveClass('folder-rail__row_menu-open'); + }); + it('removes an empty folder from its menu', async () => { const user = userEvent.setup(); const {props} = setup({rows: [folder('Empty', {noteCount: 0, hasChildren: false})]}); diff --git a/src/components/FolderRail.tsx b/src/components/FolderRail.tsx index 9db76af..a11e5f0 100644 --- a/src/components/FolderRail.tsx +++ b/src/components/FolderRail.tsx @@ -423,11 +423,11 @@ export const FolderRail = forwardRef(function All Notes + {/* Last flex item, so the count ends at the row padding — where a folder row's + trailing slot puts its count — keeping the column aligned without a spacer. */} {allNotesCount > 0 ? ( {allNotesCount} ) : null} - {/* Reserve the same trailing slot as a folder row's ⋯ menu, so counts line up. */} - ); }; @@ -515,7 +515,10 @@ export const FolderRail = forwardRef(function className={ 'folder-rail__row' + (selected ? ' folder-rail__row_selected' : '') + - (dropTarget === row.path ? ' folder-rail__row_drop-target' : '') + (dropTarget === row.path ? ' folder-rail__row_drop-target' : '') + + // Keeps the ⋯ (and not the count) showing while this row's menu is open, + // however far the pointer wanders (see the reveal rules in FolderRail.css). + (contextMenu?.row.path === row.path ? ' folder-rail__row_menu-open' : '') } style={{paddingInlineStart: indentFor(row.depth)}} role="treeitem" @@ -574,14 +577,16 @@ export const FolderRail = forwardRef(function {row.name} - {row.noteCount > 0 ? ( - {row.noteCount} - ) : null} -
+ {/* The count and the ⋯ menu share one trailing slot (see FolderRail.css): the + count shows at rest, the menu takes its place on hover/focus. */} + + {row.noteCount > 0 ? ( + {row.noteCount} + ) : null} -
+
); }; From 1155575347d7932c2f91c1459cda86511f2c92b1 Mon Sep 17 00:00:00 2001 From: Yuriy <17292315+ykamendrovskiy@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:13:40 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(folders):=20position=20the=20revealed?= =?UTF-8?q?=20=E2=8B=AF=20relative,=20not=20static?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .g-button paints its hover wash, press effect and focus ring on abspos ::before/::after with inset: 0, so an un-positioned button anchors those to .folder-rail__trailing — 19px wide and shifted by the -5px pull-in — i.e. a clipped wash and a misplaced Tab ring. It renders correctly today only because uikit's own transform: scale(1) on .g-button makes it a containing block anyway; not worth depending on. Offset-less relative is identical to static in flex flow (the slot still re-sizes), and it's what .g-button sets for itself. Co-Authored-By: Claude Fable 5 --- src/components/FolderRail.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/FolderRail.css b/src/components/FolderRail.css index 8578dd6..546d101 100644 --- a/src/components/FolderRail.css +++ b/src/components/FolderRail.css @@ -152,12 +152,18 @@ The selected row holding its ⋯ permanently costs it its count — the one folder whose count is never shown is the one you are in. A debatable trade, challenge welcome; without it the rail - felt inconsistent beside the note list, whose selected item holds its actions the same way. */ + felt inconsistent beside the note list, whose selected item holds its actions the same way. + + Reveal is relative, not static: .g-button paints its hover wash, press effect and focus ring on + abspos ::before/::after with inset: 0, so an un-positioned button would anchor those to the + trailing slot (19px wide, shifted by the -5px pull-in). Today uikit's own transform on .g-button + happens to make it a containing block anyway, but that's not worth depending on; offset-less + relative is identical to static in flex flow. */ .folder-rail__row_selected .folder-rail__trailing .folder-rail__actions, .folder-rail__row:focus .folder-rail__trailing .folder-rail__actions, .folder-rail__row_menu-open .folder-rail__trailing .folder-rail__actions, .folder-rail__trailing .folder-rail__actions:focus-visible { - position: static; + position: relative; opacity: 1; pointer-events: auto; } @@ -172,7 +178,7 @@ /* Hover-reveal, pointer-only: on touch this revealed content is what swallowed the tap. */ @media (hover: hover) { .folder-rail__row:hover .folder-rail__trailing .folder-rail__actions { - position: static; + position: relative; opacity: 1; pointer-events: auto; }