feat(ui): add Classic, Smart, and Compact desktop navigation styles (#1642)

* feat(ui): add Classic, Smart, and Compact desktop navigation styles

Introduce a shared app-nav registry and reachable model so TopBar, the
command palette, and mobile menus share one destination source. Smart bar
is the default; Appearance gains a Navigation subsection with quick links.

* fix(e2e): stop clearing top-nav prefs on every reload

The desktop-navigation suite used addInitScript to wipe mode storage,
which re-ran on reload and undid the classic/compact values under test.

* fix(ui): harden nav PR docs and Compact quick-link coverage

Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload.

* fix(ui): polish Compact quick links and menu mastheads

Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove.

* test(e2e): exact-match Compact Networking pin locator

Avoid Playwright strict-mode clash with Actions for Networking.

* fix(ui): simplify Compact quick link removal and fix + button trailing

Remove the (...) dropdown per quick link in Compact mode. Right-click
context menu remains as the sole on-bar removal affordance. Fix the +
add-button to trail quick links rather than pinning to the far right
by removing flex-1 from the quick-link rail.
This commit is contained in:
Anso
2026-07-16 21:48:03 -04:00
committed by GitHub
parent d8e4ede94f
commit 25586fc8ab
24 changed files with 1871 additions and 216 deletions
@@ -7,6 +7,7 @@
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Home, Radar } from 'lucide-react';
import { TopBar, type TopBarNavItem } from '../TopBar';
@@ -87,3 +88,215 @@ describe('TopBar showLabels', () => {
expect(screen.getByRole('navigation', { name: 'Primary' }).previousElementSibling).toBeNull();
});
});
describe('TopBar smart and compact modes', () => {
const overflowGroups = [
{
group: 'operations' as const,
label: 'Operations',
items: [{ value: 'global-observability' as const, label: 'Logs', icon: Home }],
},
];
const launcherGroups = [
{
group: 'overview' as const,
label: 'Overview',
items: [{ value: 'dashboard' as const, label: 'Home', icon: Home }],
},
{
group: 'settings' as const,
label: 'Settings',
items: [{ value: 'settings' as const, label: 'Settings', icon: Radar }],
},
];
const emptyModel = {
allPageItems: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
primaryItems: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
overflowGroups: [] as typeof overflowGroups,
launcherGroups: [] as typeof launcherGroups,
quickLinkCandidates: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
};
it('marks More with aria-current when the active page is in overflow', () => {
renderTopBar({
navMode: 'smart',
activeView: 'global-observability',
navModel: {
...emptyModel,
overflowGroups,
},
});
expect(screen.getByRole('button', { name: 'More navigation' })).toHaveAttribute(
'aria-current',
'page',
);
expect(screen.getByRole('button', { name: 'More navigation' })).toHaveTextContent('More');
});
it('opens the More menu with masthead chrome and keeps overflow labels', async () => {
const onNavigate = vi.fn();
renderTopBar({
navMode: 'smart',
onNavigate,
navModel: {
...emptyModel,
overflowGroups,
},
});
const more = screen.getByRole('button', { name: 'More navigation' });
more.focus();
fireEvent.keyDown(more, { key: 'Enter' });
expect(await screen.findByText('More', { selector: '.font-heading' })).toBeInTheDocument();
expect(await screen.findByRole('menuitem', { name: /Logs/i })).toBeInTheDocument();
fireEvent.click(screen.getByRole('menuitem', { name: /Logs/i }));
expect(onNavigate).toHaveBeenCalledWith('global-observability');
});
it('renders Compact pins with always-inline labels and a trailing Add control', async () => {
const user = userEvent.setup();
const onNavigate = vi.fn();
const onAddQuickLink = vi.fn();
const onRemoveQuickLink = vi.fn();
const onOpenSettings = vi.fn();
renderTopBar({
navMode: 'compact',
activeView: 'dashboard',
onNavigate,
onAddQuickLink,
onRemoveQuickLink,
onOpenSettings,
persistedQuickLinkIds: ['dashboard'],
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
navModel: {
...emptyModel,
launcherGroups,
quickLinkCandidates: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
},
});
const home = screen.getByRole('button', { name: 'Home' });
expect(home.querySelector('span.inline')).toBeTruthy();
expect(home.querySelector('span.hidden')).toBeNull();
await user.click(screen.getByRole('button', { name: 'Add quick link' }));
await user.click(await screen.findByRole('menuitem', { name: /Fleet/i }));
expect(onAddQuickLink).toHaveBeenCalledWith('fleet');
await user.click(screen.getByRole('button', { name: 'Home' }));
expect(onNavigate).toHaveBeenCalledWith('dashboard');
await user.pointer({ keys: '[MouseRight]', target: screen.getByRole('button', { name: 'Home' }) });
await user.click(await screen.findByRole('menuitem', { name: /^Remove$/i }));
expect(onRemoveQuickLink).toHaveBeenCalledWith('dashboard');
expect(onNavigate).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
expect(await screen.findByText('Navigate', { selector: '.font-heading' })).toBeInTheDocument();
await user.click(await screen.findByRole('menuitem', { name: /Settings/i }));
expect(onOpenSettings).toHaveBeenCalled();
});
it('disables Add when persisted capacity is full even if fewer pins are visible', () => {
renderTopBar({
navMode: 'compact',
persistedQuickLinkIds: ['dashboard', 'fleet', 'resources', 'security', 'networking'],
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
navModel: {
...emptyModel,
launcherGroups,
quickLinkCandidates: emptyModel.quickLinkCandidates,
},
});
expect(screen.getByRole('button', { name: 'Add quick link' })).toBeDisabled();
});
it('offers Compact launcher context Add for unpinned destinations', async () => {
const user = userEvent.setup();
const onAddQuickLink = vi.fn();
const compactLauncher = [
{
group: 'overview' as const,
label: 'Overview',
items: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
},
];
renderTopBar({
navMode: 'compact',
onAddQuickLink,
persistedQuickLinkIds: ['dashboard'],
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
navModel: {
...emptyModel,
launcherGroups: compactLauncher,
quickLinkCandidates: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
},
});
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Fleet/i }));
await user.click(await screen.findByRole('menuitem', { name: /Add to quick links/i }));
expect(onAddQuickLink).toHaveBeenCalledWith('fleet');
});
it('hides Compact launcher context Add for already-pinned destinations', async () => {
const user = userEvent.setup();
const compactLauncher = [
{
group: 'overview' as const,
label: 'Overview',
items: [
{ value: 'dashboard' as const, label: 'Home', icon: Home },
{ value: 'fleet' as const, label: 'Fleet', icon: Radar },
],
},
];
renderTopBar({
navMode: 'compact',
onAddQuickLink: vi.fn(),
persistedQuickLinkIds: ['dashboard'],
quickLinks: [{ value: 'dashboard', label: 'Home', icon: Home }],
navModel: {
...emptyModel,
launcherGroups: compactLauncher,
},
});
await user.click(screen.getByRole('button', { name: 'Open navigation launcher' }));
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Home/i }));
expect(screen.queryByRole('menuitem', { name: /Add to quick links/i })).toBeNull();
});
it('does not offer Add to quick links on Smart More', async () => {
const user = userEvent.setup();
renderTopBar({
navMode: 'smart',
onAddQuickLink: vi.fn(),
navModel: {
...emptyModel,
primaryItems: [{ value: 'dashboard' as const, label: 'Home', icon: Home }],
overflowGroups,
},
});
await user.click(screen.getByRole('button', { name: 'More navigation' }));
fireEvent.contextMenu(await screen.findByRole('menuitem', { name: /Logs/i }));
expect(screen.queryByRole('menuitem', { name: /Add to quick links/i })).toBeNull();
});
});