mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Stabilize desktop nav accessible names
This commit is contained in:
@@ -66,6 +66,11 @@ runtime cost control, and shared AI transport surfaces.
|
||||
cache for assistant context and suggestions, but it must not reopen
|
||||
`useResources()` or trigger a second unfiltered `all-resources` REST fetch
|
||||
just because the drawer component is present in the app shell.
|
||||
The same app-shell boundary keeps Patrol/Assistant utility navigation
|
||||
accessible-name safe: labelled icon SVGs may remain meaningful when rendered
|
||||
standalone, but `frontend-modern/src/AppLayout.tsx` must treat them as
|
||||
decorative inside tabs so the announced tab name comes from product chrome
|
||||
and meaningful badge text.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
||||
@@ -278,6 +278,10 @@ Community limit enforcement.
|
||||
invitation flow must therefore refresh org bootstrap through the shared
|
||||
`organizations_changed` app-shell path instead of forking a second hosted
|
||||
org bootstrap or pricing-aware shell reload.
|
||||
App-shell navigation rendered by `frontend-modern/src/AppLayout.tsx` must
|
||||
also keep decorative icon titles out of tab accessible names, so hosted and
|
||||
self-hosted chrome announce the canonical tab label plus meaningful badge
|
||||
counts rather than duplicating branded SVG titles.
|
||||
22. Keep the hosted account portal shell task-first and compact. Section
|
||||
headers, billing action rows, and the maintained portal bundle under
|
||||
`internal/cloudcp/portal/` may surface the facts an operator needs, but the
|
||||
|
||||
@@ -48,6 +48,16 @@ const NAV_TAB_ICON_CLASS = 'w-4 h-4 shrink-0';
|
||||
const AI_CHAT_LAUNCHER_BUTTON_CLASS =
|
||||
'fixed right-4 bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] z-40 flex h-11 w-11 items-center justify-center rounded-full border border-border bg-surface text-blue-600 shadow-lg transition-colors duration-200 hover:bg-surface-hover hover:text-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 dark:text-blue-400 dark:hover:text-blue-300 lg:right-0 lg:top-1/2 lg:bottom-auto lg:h-auto lg:w-auto lg:min-h-9 lg:min-w-10 lg:-translate-y-1/2 lg:rounded-l-lg lg:rounded-r-none lg:border-r-0 lg:px-2.5 lg:py-2.5 lg:shadow-none';
|
||||
|
||||
function getDesktopUtilityTabAriaLabel(tab: UtilityTab): string {
|
||||
if (tab.id === 'alerts') {
|
||||
const count = tab.count ?? 0;
|
||||
if (count > 0) {
|
||||
return `${count} ${tab.label}`;
|
||||
}
|
||||
}
|
||||
return tab.label;
|
||||
}
|
||||
|
||||
export interface AppLayoutProps {
|
||||
connectionStatus: () => AppConnectionStatus;
|
||||
lastUpdateText: () => string;
|
||||
@@ -604,12 +614,15 @@ export function AppLayout(props: AppLayoutProps) {
|
||||
<div
|
||||
class={className()}
|
||||
role="tab"
|
||||
aria-label={platform.label}
|
||||
aria-disabled={disabled()}
|
||||
onMouseEnter={() => warmNavigationTarget(getPlatformTargetRoute(platform))}
|
||||
onClick={() => handlePlatformClick(platform)}
|
||||
title={title()}
|
||||
>
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
<span aria-hidden="true" class="inline-flex items-center justify-center">
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
</span>
|
||||
<span class="hidden xs:inline-flex items-center gap-1">
|
||||
<span>{platform.label}</span>
|
||||
<Show when={platform.badge}>
|
||||
@@ -644,12 +657,15 @@ export function AppLayout(props: AppLayoutProps) {
|
||||
<div
|
||||
class={className()}
|
||||
role="tab"
|
||||
aria-label={getDesktopUtilityTabAriaLabel(tab)}
|
||||
aria-disabled={false}
|
||||
onMouseEnter={() => warmNavigationTarget(tab.route)}
|
||||
onClick={() => handleUtilityClick(tab)}
|
||||
title={tab.tooltip}
|
||||
>
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
<span aria-hidden="true" class="inline-flex items-center justify-center">
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="hidden xs:inline">{tab.label}</span>
|
||||
<span class="xs:hidden">{tab.label.charAt(0)}</span>
|
||||
|
||||
@@ -130,9 +130,14 @@ describe('App architecture', () => {
|
||||
"type MobileNavBarPlatformTab as PlatformTab,\n type MobileNavBarUtilityTab as UtilityTab,",
|
||||
);
|
||||
expect(appLayoutSource).toContain("const NAV_TAB_ICON_CLASS = 'w-4 h-4 shrink-0';");
|
||||
expect(appLayoutSource).toContain('function getDesktopUtilityTabAriaLabel(tab: UtilityTab)');
|
||||
expect(appLayoutSource).toContain('return `${count} ${tab.label}`;');
|
||||
expect(appLayoutSource).toContain('const platformTabs = createMemo<PlatformTab[]>(() =>');
|
||||
expect(appLayoutSource).toContain('const Icon = platform.icon;');
|
||||
expect(appLayoutSource).toContain('const Icon = tab.icon;');
|
||||
expect(appLayoutSource).toContain('aria-label={platform.label}');
|
||||
expect(appLayoutSource).toContain('aria-label={getDesktopUtilityTabAriaLabel(tab)}');
|
||||
expect(appLayoutSource).toContain('<span aria-hidden="true" class="inline-flex items-center justify-center">');
|
||||
expect(appLayoutSource).toContain('<Icon class={NAV_TAB_ICON_CLASS} />');
|
||||
expect(appLayoutSource).not.toContain('type PlatformTab = {');
|
||||
expect(appLayoutSource).not.toContain('type UtilityTab = {');
|
||||
|
||||
@@ -77,6 +77,15 @@ describe('AppLayout navigation icons', () => {
|
||||
desktopTabs.forEach((tab) => {
|
||||
expect(tab.querySelector('svg')).toBeTruthy();
|
||||
});
|
||||
const desktopPatrolTab = within(systemGroup as HTMLElement).getByRole('tab', {
|
||||
name: 'Patrol',
|
||||
});
|
||||
expect(desktopPatrolTab.querySelector('svg')).toBeTruthy();
|
||||
expect(within(systemGroup as HTMLElement).getByRole('tab', { name: '1 Alerts' })).toBeTruthy();
|
||||
expect(
|
||||
within(systemGroup as HTMLElement).queryByRole('tab', { name: 'Pulse Patrol Patrol' }),
|
||||
).toBeNull();
|
||||
expect(within(systemGroup as HTMLElement).queryByRole('tab', { name: 'Patrol P' })).toBeNull();
|
||||
|
||||
const mobileTablist = screen.getByRole('tablist', { name: 'Mobile navigation' });
|
||||
['alerts', 'ai', 'settings'].forEach((tabId) => {
|
||||
@@ -84,6 +93,8 @@ describe('AppLayout navigation icons', () => {
|
||||
expect(button).toBeTruthy();
|
||||
expect(button?.querySelector('svg')).toBeTruthy();
|
||||
});
|
||||
const mobilePatrolTab = within(mobileTablist).getByRole('button', { name: 'Patrol' });
|
||||
expect(mobilePatrolTab.querySelector('svg')).toBeTruthy();
|
||||
|
||||
expect(container).toHaveTextContent('Dashboard body');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user