Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 0 additions & 61 deletions apps/web/src/components/EntryNavRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import { GITHUB_STARS_FALLBACK_LABEL, formatStars, useGithubStars } from './useG
import { PlanWordmark, planBadgeTierForWorkspace } from './PlanWordmark';
import { RemixIcon } from './RemixIcon';
import { InviteDialog } from './InviteDialog';
import { MessageCenter } from './MessageCenter';
import type { EntrySettingsSection } from './EntrySettingsMenu';
import { useI18n } from '../i18n';
import { useDismissOnOutsideInteraction } from '../hooks/useDismissOnOutsideInteraction';
Expand Down Expand Up @@ -624,17 +623,6 @@ export function EntryNavRail({
});

const [accountOpen, setAccountOpen] = useState(false);
// Message-center panel (opened from the account menu's 消息中心 row) and its
// unread count, which drives the red dot on the account avatar.
const [messageCenterOpen, setMessageCenterOpen] = useState(false);
const [messageUnreadCount, setMessageUnreadCount] = useState(0);
// Where the message-center panel returns keyboard focus on close. The
// signed-in 消息中心 row cannot be it: the account menu unmounts the row before
// the panel opens, so the account trigger it hangs off is the stable control.
// Signed-out has no menu — the rail item itself stays mounted.
const accountTriggerRef = useRef<HTMLButtonElement | null>(null);
const messageCenterRailRef = useRef<HTMLButtonElement | null>(null);
const messageCenterReturnFocusRef = context ? accountTriggerRef : messageCenterRailRef;
// Sign-out confirm gate (recvqgMWpJZqhL): the menu item only ARMS the
// confirmation dialog; the real logout chain runs on explicit confirm.
const [confirmSignOut, setConfirmSignOut] = useState(false);
Expand Down Expand Up @@ -937,7 +925,6 @@ export function EntryNavRail({
{updaterSlot}
</div>
<button
ref={accountTriggerRef}
type="button"
className="entry-nav-rail__account-trigger"
onClick={() => setAccountOpen((v) => !v)}
Expand All @@ -947,9 +934,6 @@ export function EntryNavRail({
>
<span className="entry-nav-rail__account-avatar" aria-hidden>
{accountInitial}
{messageUnreadCount > 0 ? (
<span className="entry-nav-rail__account-avatar-dot" data-testid="account-avatar-unread-dot" />
) : null}
</span>
<span className="entry-nav-rail__account-name">{accountName}</span>
{/* #5517: the plan badge replaces the chevron when a tier is
Expand Down Expand Up @@ -1029,23 +1013,6 @@ export function EntryNavRail({
>
<Icon name="settings" size={15} /> {t('entry.accountSettings')}
</button>
<button
type="button"
className="entry-nav-rail__menu-item"
role="menuitem"
aria-haspopup="dialog"
aria-expanded={messageCenterOpen}
data-testid="account-menu-message-center"
onClick={() => {
setAccountOpen(false);
setMessageCenterOpen(true);
}}
>
<Icon name="bell" size={15} /> {t('messageCenter.title')}
{messageUnreadCount > 0 ? (
<span className="entry-nav-rail__menu-item-dot" aria-hidden />
) : null}
</button>
{/* #5517's account menu goes 设置 → GitHub 帮助 → 功能建议 → 社交行,
with no theme row, no language submenu, and no divider in
between. Both controls still have a home in 设置·通用 (theme
Expand Down Expand Up @@ -1398,23 +1365,6 @@ export function EntryNavRail({
>
<Icon name="settings" size={16} />
</NavButton>
{/* Signed-out has no account menu (where the 消息中心 row lives when
signed in), which left the message panel with no opener at all.
It rides here as the rail item under 设置. */}
<NavButton
ariaLabel={t('messageCenter.title')}
label={t('messageCenter.title')}
onClick={() => setMessageCenterOpen(true)}
testId="entry-nav-message-center"
buttonRef={messageCenterRailRef}
ariaHasPopup="dialog"
ariaExpanded={messageCenterOpen}
>
<Icon name="bell" size={16} />
{messageUnreadCount > 0 ? (
<span className="entry-nav-rail__btn-dot" aria-hidden />
) : null}
</NavButton>
</>
)}
</div>
Expand All @@ -1433,17 +1383,6 @@ export function EntryNavRail({
) : null}
</div>

{/* Panel + unread polling live here (outside the hover menu, which
unmounts when closed); the 消息中心 menu row above just opens it. */}
<MessageCenter
hideTrigger
returnFocusRef={messageCenterReturnFocusRef}
open={messageCenterOpen}
onOpenChange={setMessageCenterOpen}
onUnreadCountChange={setMessageUnreadCount}
onOpenNotificationSettings={onOpenSettings ? () => onOpenSettings('notifications') : undefined}
/>

<InviteDialog
open={inviteOpen}
onClose={() => setInviteOpen(false)}
Expand Down
38 changes: 1 addition & 37 deletions apps/web/src/styles/home/entry-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -772,26 +772,13 @@
transition: background-color 120ms var(--ease-out), color 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.entry-nav-rail__btn-icon {
/* Anchor for the unread dot (message-center rail item). */
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 18px;
/* Tracks the unselected label tone above. */
color: color-mix(in srgb, var(--text) 68%, transparent);
}
/* Unread marker on a rail item's icon — same 6px red dot as the account
menu's `__menu-item-dot`, pinned to the glyph's top-right corner. */
.entry-nav-rail__btn-dot {
position: absolute;
top: -2px;
right: -2px;
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--red);
}
.entry-nav-rail__btn-icon > svg {
width: 18px;
height: 18px;
Expand Down Expand Up @@ -5465,27 +5452,6 @@
.entry-nav-rail__account-updater:empty {
display: none;
}
/* Unread-messages red dot riding the account avatar's top-right corner. */
.entry-nav-rail__account-avatar-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--red);
/* Panel-colored ring separates the dot from the avatar artwork. */
box-shadow: 0 0 0 2px var(--bg-panel);
}
/* Matching dot on the account menu's 消息中心 row, pushed to the far edge. */
.entry-nav-rail__menu-item-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--red);
margin-left: auto;
flex: 0 0 auto;
}
.entry-nav-rail__account-trigger {
appearance: none;
border: 0;
Expand All @@ -5512,11 +5478,9 @@
background: color-mix(in srgb, var(--text) 5%, transparent);
}
.entry-nav-rail__account-avatar {
position: relative;
width: 25px;
height: 25px;
/* Rounded square (8px), replacing the squircle clip-path — a clip-path
would also swallow the unread dot that overflows the top-right corner. */
/* Rounded square (8px), replacing the old squircle clip-path. */
border-radius: 8px;
background: linear-gradient(135deg, #87ea5c, #0d5400);
color: #fff;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
// @vitest-environment jsdom
//
// The message-center panel moved behind two external openers on the rail (the
// signed-in account menu's 消息中心 row and the signed-out rail item), with
// `MessageCenter hideTrigger`. `hideTrigger` leaves the component's internal
// `triggerRef` unattached, so `closePanel()` had nothing to restore focus to:
// opening the panel focuses the portaled dialog, and closing it unmounted the
// focused node and dropped keyboard focus to the document. Both openers also
// have to advertise the dialog they own (`aria-haspopup="dialog"` plus the
// `aria-expanded` state) — the built-in bell already did.
//
// Focus must return to a control that is still mounted after the close: the
// signed-in row lives in a hover menu that unmounts before the panel opens, so
// the account trigger is the stable host control there.
// Product intentionally does not expose the message center in the web shell.
// Keep this at the rail boundary so both identity branches stay protected:
// signed-in users must not get an account-menu entry, and signed-out users
// must not get a standalone rail entry. The underlying notification settings
// and message-center client remain available for future product work.

import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import type { WorkspaceCollabContext } from '@open-design/contracts';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

Expand Down Expand Up @@ -52,24 +45,17 @@ function renderRail(context: WorkspaceCollabContext | null) {
);
}

function stubFetch() {
beforeEach(() => {
localStorage.clear();
resetWorkspaceDirectoryCache();
vi.stubGlobal(
'fetch',
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/messages?')) {
return Response.json({ messages: [], nextCursor: null, unreadCount: 0 });
}
if (url.includes('/status')) return Response.json({ loggedIn: false });
return Response.json({ items: [] });
}),
);
}

beforeEach(() => {
localStorage.clear();
resetWorkspaceDirectoryCache();
stubFetch();
});

afterEach(() => {
Expand All @@ -79,62 +65,21 @@ afterEach(() => {
vi.unstubAllGlobals();
});

describe('EntryNavRail message-center openers', () => {
it('returns focus to the account trigger when the panel closes with Escape', async () => {
describe('EntryNavRail message-center visibility', () => {
it('does not expose a message-center entry for signed-in users', () => {
renderRail(teamContext());
const accountTrigger = screen.getByTestId('entry-nav-account');

fireEvent.click(accountTrigger);
fireEvent.click(screen.getByTestId('account-menu-message-center'));
await waitFor(() => expect(screen.getByTestId('message-center-dialog')).toBeTruthy());

fireEvent.keyDown(document, { key: 'Escape' });
fireEvent.click(screen.getByTestId('entry-nav-account'));

expect(screen.queryByTestId('account-menu-message-center')).toBeNull();
expect(screen.queryByTestId('message-center-trigger')).toBeNull();
expect(screen.queryByTestId('message-center-dialog')).toBeNull();
expect(document.activeElement).toBe(accountTrigger);
});

it('returns focus to the account trigger when the panel closes via the backdrop', async () => {
renderRail(teamContext());
const accountTrigger = screen.getByTestId('entry-nav-account');

fireEvent.click(accountTrigger);
fireEvent.click(screen.getByTestId('account-menu-message-center'));
const dialog = await waitFor(() => screen.getByTestId('message-center-dialog'));

fireEvent.mouseDown(screen.getByTestId('message-center-backdrop'));

expect(dialog.isConnected).toBe(false);
expect(document.activeElement).toBe(accountTrigger);
});

it('returns focus to the signed-out rail opener when the panel closes', async () => {
it('does not expose a message-center entry for signed-out users', () => {
renderRail(null);
const railOpener = screen.getByTestId('entry-nav-message-center');

fireEvent.click(railOpener);
await waitFor(() => expect(screen.getByTestId('message-center-dialog')).toBeTruthy());

fireEvent.keyDown(document, { key: 'Escape' });

expect(screen.queryByTestId('entry-nav-message-center')).toBeNull();
expect(screen.queryByTestId('message-center-trigger')).toBeNull();
expect(screen.queryByTestId('message-center-dialog')).toBeNull();
expect(document.activeElement).toBe(railOpener);
});

it('advertises the dialog on both external openers', () => {
const signedOut = renderRail(null);
const railOpener = screen.getByTestId('entry-nav-message-center');
expect(railOpener.getAttribute('aria-haspopup')).toBe('dialog');
expect(railOpener.getAttribute('aria-expanded')).toBe('false');

fireEvent.click(railOpener);
expect(screen.getByTestId('entry-nav-message-center').getAttribute('aria-expanded')).toBe('true');
signedOut.unmount();

renderRail(teamContext());
fireEvent.click(screen.getByTestId('entry-nav-account'));
const menuRow = screen.getByTestId('account-menu-message-center');
expect(menuRow.getAttribute('aria-haspopup')).toBe('dialog');
expect(menuRow.getAttribute('aria-expanded')).toBe('false');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('updater rocket placement above the rail account row', () => {
expect(trigger.contains(rocket)).toBe(false);

fireEvent.click(trigger);
await waitFor(() => expect(screen.getByTestId('account-menu-message-center')).toBeTruthy());
await waitFor(() => expect(screen.getByRole('menu')).toBeTruthy());
expect(trigger.getAttribute('aria-expanded')).toBe('true');
});

Expand Down
Loading