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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/ui/src/components/sidebar-header/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { useConnector } from '@/data/core';
import { SidebarHeader } from './index';

vi.mock( '@tanstack/react-router', () => ( {
useNavigate: () => vi.fn(),
} ) );

vi.mock( '@/data/core', () => ( {
useConnector: vi.fn(),
} ) );

vi.mock( '@/hooks/use-traffic-light-space', () => ( {
useTrafficLightSpace: () => false,
} ) );

const useConnectorMock = vi.mocked( useConnector, { partial: true } );

describe( 'SidebarHeader', () => {
it( 'shows the app menu button when the host has no native menu bar', () => {
useConnectorMock.mockReturnValue( { showsAppMenuButton: true, popupAppMenu: vi.fn() } );

render( <SidebarHeader onToggleSidebar={ () => {} } /> );

expect( screen.getByRole( 'button', { name: 'Menu' } ) ).toBeInTheDocument();
} );

it( 'hides the app menu button when the host has a native menu bar', () => {
useConnectorMock.mockReturnValue( { showsAppMenuButton: false } );

render( <SidebarHeader onToggleSidebar={ () => {} } /> );

expect( screen.queryByRole( 'button', { name: 'Menu' } ) ).not.toBeInTheDocument();
} );
} );
20 changes: 11 additions & 9 deletions apps/ui/src/components/sidebar-header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ export function SidebarHeader( { onToggleSidebar }: Props ) {
};
return (
<div className={ `${ styles.root } ${ reserveTrafficLightSpace ? '' : styles.flush }` }>
<IconButton
variant="minimal"
tone="neutral"
size="small"
className={ styles.menuButton }
icon={ menu }
label={ __( 'Menu' ) }
onClick={ handleOpenAppMenu }
/>
{ connector.showsAppMenuButton && (
<IconButton
variant="minimal"
tone="neutral"
size="small"
className={ styles.menuButton }
icon={ menu }
label={ __( 'Menu' ) }
onClick={ handleOpenAppMenu }
/>
) }
<div className={ styles.actions }>
<Menu.Root modal={ false }>
<Menu.Trigger
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/data/core/connectors/hosted/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ export function createHostedConnector( { apiBaseUrl }: HostedConnectorOptions ):
window.open( url, '_blank', 'noopener,noreferrer' );
},
async popupAppMenu() {},
showsAppMenuButton: false,
async copyText( text ) {
await navigator.clipboard.writeText( text );
},
Expand Down
15 changes: 11 additions & 4 deletions apps/ui/src/data/core/connectors/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export function createIpcConnector(): Connector {
);
}

// The IPC connector only runs in Electron, so `navigator` reflects the
// desktop OS.
const isMacOS = /mac/i.test( navigator.platform || navigator.userAgent );

// Preview CLI commands are path-based, not id-based. Look up the matching
// site once per call so UI code can keep working with the stable site id.
async function resolveSiteFolder( siteId: string ): Promise< string > {
Expand Down Expand Up @@ -684,6 +688,10 @@ export function createIpcConnector(): Connector {
ipcApi.popupAppMenu( position );
},

// Windows/Linux have no native menu bar, so the UI provides the entry
// point; macOS keeps the native application menu.
showsAppMenuButton: ! isMacOS,

async copyText( text: string ): Promise< void > {
await ipcApi.copyText( text );
},
Expand All @@ -693,10 +701,9 @@ export function createIpcConnector(): Connector {
},

// Window state
// The IPC connector only runs in Electron, so `navigator` reflects the
// desktop OS. macOS overlays the traffic lights on the content (so we
// reserve space for them); Windows and Linux don't.
reservesTrafficLightSpace: /mac/i.test( navigator.platform || navigator.userAgent ),
// macOS overlays the traffic lights on the content (so we reserve
// space for them); Windows and Linux don't.
reservesTrafficLightSpace: isMacOS,

async isFullscreen(): Promise< boolean > {
return ipcApi.isFullscreen();
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/data/core/connectors/local/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ export function createLocalConnector( { apiBaseUrl }: LocalConnectorOptions ): C
window.open( url, '_blank', 'noopener,noreferrer' );
},
async popupAppMenu() {},
showsAppMenuButton: false,
async openSiteUrl( siteId, relativeUrl = '' ) {
const sites = lastSites ?? ( await api< SiteDetails[] >( '/sites' ) );
const target = new URL( relativeUrl || '/', findSiteUrl( sites, siteId ) ).toString();
Expand Down
6 changes: 6 additions & 0 deletions apps/ui/src/data/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ export interface Connector {

popupAppMenu( position: { x: number; y: number } ): Promise< void >;

// Whether the UI should render a button that opens the app menu via
// `popupAppMenu`. True only in the Windows/Linux desktop app, which has no
// native menu bar; macOS has the native application menu and the browser
// (`studio ui` / hosted) has no app menu at all.
showsAppMenuButton: boolean;

// Clipboard — routed to the host so it works where the renderer's
// `navigator.clipboard` is unavailable (e.g. Electron permission denial).
copyText( text: string ): Promise< void >;
Expand Down