Skip to content

feat(layout): a collapsed sidebar nav group shows its reachable child count - #26103

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25876-nav-group-child-count
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25876-nav-group-child-count

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25876

Clause-②: no
Metadata-protocol: none

What changes

A collapsed sidebar navigation group now shows how many entries it holds, as a small count badge between its label and its chevron (ui/packages/layout/src/NavigationRenderer.tsx). Before, a closed group drew a label and a chevron and nothing else, so a closed group holding one entry looked exactly like one holding ten.

  • What is counted. The entries that opening the group would reveal: every leaf that passes passesNavItemGuards (the visible expression, requiredPermissions, the requiresObject / requiresService capability gates, a doc entry's readability check), counted through nested groups. Separators and the groups themselves are not entries, and an action entry counts only when the host wires onAction.
  • One walk, not a copy. The walk that hasVisibleNavigationItems ran is now a module-local generator, visibleNavigationEntries. hasVisibleNavigationItems takes its first entry (still stopping at the first hit) and the new module-local countVisibleNavigationItems counts them all. The count therefore cannot disagree with whether the group renders, or with what it shows once opened. No export is added or changed.
  • When it shows. Only while the group is collapsed (!isOpen). An open group's entries are in view. A group holding the current route is forced open, so it never shows a count.
  • Accessibility. The badge is the already-imported Badge with aria-hidden="true", so the disclosure's accessible name stays the group label (NavigationRenderer.groupDefaultOpen-13336.test.tsx uses exact button names and passes unmodified). The disclosure already announces collapsed or expanded.
  • Icon rail. The badge carries HIDDEN_ICON_LABEL_NO_POINTER and HIDDEN_ICON_LABEL_NOT_FOCUSABLE, the same classes the group label carries. The chevron's ml-auto moves to the badge while it shows, so the badge and chevron sit together at the right edge.
  • No new i18n key. No navigation key or value changes.

The card's second bullet is void

The card also asked that "when the console replaces a report group's children with one index link, draw the group as that single entry". The seat's claim voided this bullet after checking origin/main: the console never folds a group's children. 「全部报表」 is a static sibling SidebarGroup at UnifiedSidebar.tsx:595-619. Flattening single-child groups would be a new product rule for every app, and the card does not ask for one. This PR adds only the count badge. No single-child flattening is implemented.

Tests

New: ui/packages/layout/src/__tests__/NavigationRenderer.groupChildCount-25876.test.tsx, 6 cases.

  • Red first against main's renderer (commit 3e21743, the test alone): 4 failed, 2 passed. The failures were the guarded count, the collapse/expand toggle, the nested group's own count, and the icon-mode classes. The two that passed are preservation pins: the accessible name is unchanged, and a group holding the route is forced open and shows no count.
  • The guarded count fixture holds seven nodes and four reachable entries. A visible-hidden entry, a permission-denied entry, a separator, an action with no dispatcher, a nested visible-hidden entry and the nested group itself are not counted. The two nested leaves are.
  • Ablation, one-off and not kept: node scripts/ablation-replace.mjs replaced countVisibleNavigationItems(children, guardOptions) with countVisibleNavigationItems(children, { hasActionHandler: !!onAction }), which drops the visibility and permission guards. The anchor went 1 → 0 and the blob went 71eca7a7 → c44ada02. Result: 3 failed / 3 passed. The restore was verified as blob == HEAD (71eca7a7) with git diff HEAD empty. The tests import ../NavigationRenderer from src, so there is no dist leg.

Run at head 279ce1d9 (after merging origin/main), under the verify lock:

  • pnpm --filter @object-ui/layout test: Test Files 46 passed (46), Tests 384 passed (384).
  • pnpm --filter @object-ui/layout type-check: exit 0.
  • vitest run packages/app-shell/src/layout/__tests__/UnifiedSidebar (from ui/): Test Files 16 passed (16), Tests 70 passed (70). This includes UnifiedSidebar.iconModeLabelFocus-12795. NavigationRenderer.iconModeLabelHitTest-12744 is in the layout run above.
  • node scripts/check-sdui-manifest.mjs: exit 0. The record is untouched, as expected: @object-ui/layout is outside the registry closure.

Gates, local subset, at head 279ce1d9: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands. All 34 ran, and each exited 0 (:: exit 0, captured before any pipe). dispatch-gates --ran reported: "34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED (a DERIVED zero — all 34 recorded an exit code and none of them is 3)". ESLint on the three touched files found 0 errors; its 21 warnings were already there. Every other family is left to CI.

Acceptance notes

  • File-surface breach, declared. One existing test outside the dispatched surface needed a one-line lookup change: ui/packages/layout/src/__tests__/AppSchemaRenderer.mobileTabOrder-11395.test.tsx, case "expanded: false … a collapsed group's children stay tabs". It found the sidebar group trigger by el.textContent === 'Sales'. A closed trigger now also contains the aria-hidden count, so its text is Sales2. The lookup now matches a closed trigger (aria-expanded="false") whose first child, the label text node, reads Sales. The assertion still checks the same things: the trigger exists outside the bar, and the bar's tabs are Deals, Leads. Only the test changed. AppSchemaRenderer.tsx itself is untouched. The change is in its own commit (c809cd2, together with the changeset) and is easy to drop if the seat prefers another route.
  • The count badge uses variant="secondary", so it reads as information, not as the default (primary) colour that an authored leaf badge takes by default.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui seat 2 · session_01SMps7wq7rBTk53e8KTSEup · 2026-10-26T12:13Z · head 279ce1d9

  1. 形态: draft → main, assignee os-steve. The body leads with Fixes #25876, and it states the card's second bullet (single-child flattening) as void per claim 6414962192. Clause-②: no / Metadata-protocol: none lead their lines in the body and the changeset. no is correct: hasVisibleNavigationItems keeps its signature and behaviour, and the new walk and counter are module-local.
  2. 前提实测: the seat read the diff at this head. The guard walk becomes the module-local generator visibleNavigationEntries. hasVisibleNavigationItems takes its first entry, and countVisibleNavigationItems counts all of them, so the count is exactly what opening the group reveals: separators and groups don't count, nested leaves do, and an action counts only with a handler. The group's CollapsibleTrigger draws a secondary Badge only while !isOpen. It is aria-hidden and carries the label's icon-rail hiding classes. Red first: 3e21743, 4 of 6 failing. The two preservation pins (accessible name, forced-open group) stay green either way. The ablation of the guard options went red 3/6.
  3. 门禁 check-runs: CI was in progress at the time of reading. Landing waits on Lint & Repo Gates and TypeScript Type Check success, with every check green.
  4. 接受的偏差: one file outside the surface, accepted: ui/packages/layout/src/__tests__/AppSchemaRenderer.mobileTabOrder-11395.test.tsx, in its own commit c809cd2. That test found the closed group's trigger by textContent === 'Sales', which the visible badge digit now extends. The lookup moves to aria-expanded="false" plus the label text node, and the test's assertions are unchanged. AppSchemaRenderer.tsx is untouched. Otherwise the diff matches the claim: NavigationRenderer.tsx, the new groupChildCount-25876 test, and the root .changeset/ patch. scripts/sdui-manifest.record.json is untouched (@object-ui/layout is outside the registry closure).
  5. 范围外去向: the void second bullet is recorded in the claim and the PR body. Flattening single-child groups would be a new nav product rule, and the seat files nothing for it unless the maintainer asks.
  6. 落地条款: no governed path, +251/−6. This PR does not move the sdui record, so it lands outside the ui lane's single-token train. Before enqueueing, the seat confirms it is green against the latest main. It lands via relay pr_ready + automerge_enable. If the only change is a merge of main, this ACCEPT carries over.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants