diff --git a/src/components/gravatar.tsx b/src/components/gravatar.tsx
index 8cee4ec6d..28e858e96 100644
--- a/src/components/gravatar.tsx
+++ b/src/components/gravatar.tsx
@@ -10,12 +10,12 @@ import profileIconDetailed from './profile-icon-detailed';
export function Gravatar( {
className,
isBlack = false,
- isLarge = false,
+ size = 16,
detailedDefaultImage = false,
}: {
className?: string;
isBlack?: boolean;
- isLarge?: boolean;
+ size?: number;
detailedDefaultImage?: boolean;
} ) {
const { __ } = useI18n();
@@ -23,16 +23,13 @@ export function Gravatar( {
const gravatarUrl = useGravatarUrl( user?.email, isBlack, detailedDefaultImage );
const [ imageError, setImageError ] = useState( false );
- const childClassName = cx(
- isLarge ? 'w-[32px] h-[32px] rounded-full' : 'w-[16px] h-[16px] rounded-full',
- className
- );
+ const childClassName = cx( 'rounded-full', className );
const renderDefaultGravatarIcon = () => (
);
@@ -47,6 +44,7 @@ export function Gravatar( {
alt={ __( 'User avatar' ) }
className={ childClassName }
onError={ () => setImageError( true ) }
+ style={ { width: `${ size }px`, height: `${ size }px` } }
/>
);
}
diff --git a/src/components/main-sidebar.tsx b/src/components/main-sidebar.tsx
index c2f6b9be7..f2dff40ea 100644
--- a/src/components/main-sidebar.tsx
+++ b/src/components/main-sidebar.tsx
@@ -1,131 +1,25 @@
import { __ } from '@wordpress/i18n';
-import { Icon, help } from '@wordpress/icons';
-import { STUDIO_DOCS_URL } from '../constants';
-import { useAuth } from '../hooks/use-auth';
-import { useOffline } from '../hooks/use-offline';
import { isMac } from '../lib/app-globals';
import { cx } from '../lib/cx';
-import { getIpcApi } from '../lib/get-ipc-api';
import AddSite from './add-site';
-import Button from './button';
-import { Gravatar } from './gravatar';
-import offlineIcon from './offline-icon';
import { RunningSites } from './running-sites';
import SiteMenu from './site-menu';
-import Tooltip from './tooltip';
-import { WordPressLogo } from './wordpress-logo';
interface MainSidebarProps {
className?: string;
}
-function SidebarAuthFooter() {
- const { isAuthenticated, authenticate } = useAuth();
- const isOffline = useOffline();
- const offlineMessage = __( 'You’re currently offline.' );
- const openDocs = async () => {
- await getIpcApi().openURL( STUDIO_DOCS_URL );
- };
- if ( isAuthenticated ) {
- return (
-
- );
-}
-
-function SidebarToolbar() {
- const isOffline = useOffline();
- const offlineMessage = [
- __( 'You’re currently offline.' ),
- __( 'Some features will be unavailable.' ),
- ];
- return (
-
- );
-}
-
export default function MainSidebar( { className }: MainSidebarProps ) {
return (
-
diff --git a/src/components/tests/main-sidebar.test.tsx b/src/components/tests/main-sidebar.test.tsx
index 52f29ab75..c235fc582 100644
--- a/src/components/tests/main-sidebar.test.tsx
+++ b/src/components/tests/main-sidebar.test.tsx
@@ -1,7 +1,6 @@
-import { fireEvent, render, act, waitFor, screen } from '@testing-library/react';
+import { render, act, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { useAuth } from '../../hooks/use-auth';
-import { useOffline } from '../../hooks/use-offline';
import MainSidebar from '../main-sidebar';
jest.mock( '../../hooks/use-auth' );
@@ -66,23 +65,6 @@ describe( 'MainSidebar Footer', () => {
expect( screen.getByRole( 'button', { name: 'Add site' } ) ).toBeVisible();
} );
- it( 'Test unauthenticated footer has the Log in button', async () => {
- const user = userEvent.setup();
- const authenticate = jest.fn();
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false, authenticate } );
- await act( async () => render(
) );
- expect( screen.getByRole( 'button', { name: 'Log in' } ) ).toBeVisible();
- await user.click( screen.getByRole( 'button', { name: 'Log in' } ) );
- expect( authenticate ).toHaveBeenCalledTimes( 1 );
- } );
-
- it( 'Test authenticated footer does not have the log in button and it has the settings and account buttons', async () => {
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
- await act( async () => render(
) );
- expect( screen.queryByRole( 'button', { name: 'Log in' } ) ).not.toBeInTheDocument();
- expect( screen.getByRole( 'button', { name: 'Account' } ) ).toBeVisible();
- } );
-
it( 'applies className prop', async () => {
const { container } = await act( async () =>
render(
)
@@ -94,30 +76,6 @@ describe( 'MainSidebar Footer', () => {
await act( async () => render(
) );
expect( screen.getByRole( 'button', { name: 'Stop all' } ) ).toBeVisible();
} );
-
- it( 'disables log in button when offline', async () => {
- ( useOffline as jest.Mock ).mockReturnValue( true );
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false } );
- await act( async () => render(
) );
- const loginButton = screen.getByRole( 'button', { name: 'Log in' } );
- expect( loginButton ).toHaveAttribute( 'aria-disabled', 'true' );
- fireEvent.mouseOver( loginButton );
- expect( screen.getByRole( 'tooltip', { name: 'You’re currently offline.' } ) ).toBeVisible();
- } );
-
- it( 'shows offline indicator', async () => {
- ( useOffline as jest.Mock ).mockReturnValue( true );
- await act( async () => render(
) );
- const offlineIndicator = screen.getByRole( 'button', {
- name: 'Offline indicator',
- } );
- fireEvent.mouseOver( offlineIndicator );
- expect(
- screen.getByRole( 'tooltip', {
- name: 'You’re currently offline. Some features will be unavailable.',
- } )
- ).toBeVisible();
- } );
} );
describe( 'MainSidebar Site Menu', () => {
@@ -155,19 +113,4 @@ describe( 'MainSidebar Site Menu', () => {
'0e9e237b-335a-43fa-b439-9b078a613333'
);
} );
-
- it( 'opens the support URL', async () => {
- const user = userEvent.setup();
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
-
- render(
);
-
- const helpIconButton = screen.getByRole( 'button', { name: 'Help' } );
- await user.click( helpIconButton );
- await waitFor( () =>
- expect( mockOpenURL ).toHaveBeenCalledWith(
- `https://developer.wordpress.com/docs/developer-tools/studio/`
- )
- );
- } );
} );
diff --git a/src/components/tests/topbar.test.tsx b/src/components/tests/topbar.test.tsx
new file mode 100644
index 000000000..c0e5f3685
--- /dev/null
+++ b/src/components/tests/topbar.test.tsx
@@ -0,0 +1,95 @@
+import { fireEvent, render, act, waitFor, screen } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { useAuth } from '../../hooks/use-auth';
+import { useOffline } from '../../hooks/use-offline';
+import TopBar from '../top-bar';
+
+jest.mock( '../../hooks/use-auth' );
+
+const mockOpenURL = jest.fn();
+const toggleMinWindowWidth = jest.fn();
+jest.mock( '../../lib/get-ipc-api', () => ( {
+ __esModule: true,
+ default: jest.fn(),
+ getIpcApi: () => ( {
+ showOpenFolderDialog: jest.fn(),
+ generateProposedSitePath: jest.fn(),
+ openURL: mockOpenURL,
+ toggleMinWindowWidth,
+ } ),
+} ) );
+
+describe( 'TopBar', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+ it( 'Test unauthenticated TopBar has the Log in button', async () => {
+ const user = userEvent.setup();
+ const authenticate = jest.fn();
+ ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false, authenticate } );
+ await act( async () => render(
) );
+ expect( screen.getByRole( 'button', { name: 'Log in' } ) ).toBeVisible();
+ await user.click( screen.getByRole( 'button', { name: 'Log in' } ) );
+ expect( authenticate ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'Test authenticated TopBar does not have the log in button and it has the settings and account buttons', async () => {
+ ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
+ await act( async () => render(
) );
+ expect( screen.queryByRole( 'button', { name: 'Log in' } ) ).not.toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Account' } ) ).toBeVisible();
+ } );
+ it( 'disables log in button when offline', async () => {
+ ( useOffline as jest.Mock ).mockReturnValue( true );
+ ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false } );
+ await act( async () => render(
) );
+ const loginButton = screen.getByRole( 'button', { name: 'Log in' } );
+ expect( loginButton ).toHaveAttribute( 'aria-disabled', 'true' );
+ fireEvent.mouseOver( loginButton );
+ expect( screen.getByRole( 'tooltip', { name: 'You’re currently offline.' } ) ).toBeVisible();
+ } );
+
+ it( 'shows offline indicator', async () => {
+ ( useOffline as jest.Mock ).mockReturnValue( true );
+ await act( async () => render(
) );
+ const offlineIndicator = screen.getByRole( 'button', {
+ name: 'Offline indicator',
+ } );
+ fireEvent.mouseOver( offlineIndicator );
+ expect(
+ screen.getByRole( 'tooltip', {
+ name: 'You’re currently offline. Some features will be unavailable.',
+ } )
+ ).toBeVisible();
+ } );
+
+ it( 'opens the support URL', async () => {
+ const user = userEvent.setup();
+ ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
+
+ render(
);
+
+ const helpIconButton = screen.getByRole( 'button', { name: 'Help' } );
+ await user.click( helpIconButton );
+ await waitFor( () =>
+ expect( mockOpenURL ).toHaveBeenCalledWith(
+ `https://developer.wordpress.com/docs/developer-tools/studio/`
+ )
+ );
+ } );
+
+ it( 'calls toggleMinWindowWidth when sidebar toggle button is clicked', async () => {
+ const user = userEvent.setup();
+ const onToggleSidebar = jest.fn().mockImplementation( () => {
+ toggleMinWindowWidth( true );
+ } );
+
+ render(
);
+
+ const toggleButton = screen.getByRole( 'button', { name: 'Toggle Sidebar' } );
+ await user.click( toggleButton );
+
+ expect( onToggleSidebar ).toHaveBeenCalledTimes( 1 );
+ expect( toggleMinWindowWidth ).toHaveBeenCalledTimes( 1 );
+ } );
+} );
diff --git a/src/components/top-bar.tsx b/src/components/top-bar.tsx
new file mode 100644
index 000000000..cd156dbde
--- /dev/null
+++ b/src/components/top-bar.tsx
@@ -0,0 +1,123 @@
+import { __, sprintf } from '@wordpress/i18n';
+import { Icon, help, drawerLeft } from '@wordpress/icons';
+import { STUDIO_DOCS_URL } from '../constants';
+import { useAuth } from '../hooks/use-auth';
+import { useOffline } from '../hooks/use-offline';
+import { getIpcApi } from '../lib/get-ipc-api';
+import Button from './button';
+import { Gravatar } from './gravatar';
+import offlineIcon from './offline-icon';
+import Tooltip from './tooltip';
+import { WordPressLogo } from './wordpress-logo';
+
+interface TopBarProps {
+ onToggleSidebar: () => void;
+}
+
+function OfflineIndicator() {
+ const isOffline = useOffline();
+ const offlineMessage = [
+ __( 'You’re currently offline.' ),
+ __( 'Some features will be unavailable.' ),
+ ];
+ return (
+ isOffline && (
+
+
+ { offlineMessage[ 0 ] }
+
+ { offlineMessage[ 1 ] }
+
+ }
+ className="h-6"
+ >
+
+
+
+ )
+ );
+}
+
+function Authentication() {
+ const { isAuthenticated, authenticate, user } = useAuth();
+ const isOffline = useOffline();
+ const offlineMessage = __( 'You’re currently offline.' );
+ if ( isAuthenticated ) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default function TopBar( { onToggleSidebar }: TopBarProps ) {
+ const openDocs = async () => {
+ await getIpcApi().openURL( STUDIO_DOCS_URL );
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/user-settings.tsx b/src/components/user-settings.tsx
index ccc560d37..48e42f15f 100644
--- a/src/components/user-settings.tsx
+++ b/src/components/user-settings.tsx
@@ -37,7 +37,7 @@ const UserInfo = ( {
aria-label={ __( 'Profile link' ) }
className="py-0 px-0"
>
-
+
{ user?.displayName }
diff --git a/src/components/windows-titlebar.tsx b/src/components/windows-titlebar.tsx
index 5f936643b..42aa47d56 100644
--- a/src/components/windows-titlebar.tsx
+++ b/src/components/windows-titlebar.tsx
@@ -6,7 +6,13 @@ import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import Button from './button';
-export default function WindowsTitlebar( { className }: { className?: string } ) {
+export default function WindowsTitlebar( {
+ className,
+ children,
+}: {
+ className?: string;
+ children?: React.ReactNode;
+} ) {
return (
{ getAppGlobals().appName }
+
{ children }
);
}
diff --git a/src/constants.ts b/src/constants.ts
index 204fb1523..decacb39a 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,5 +1,6 @@
export const MAIN_MIN_WIDTH = 900;
export const MAIN_MIN_HEIGHT = 600;
+export const SIDEBAR_WIDTH = 268;
export const SCREENSHOT_WIDTH = 1040;
export const SCREENSHOT_HEIGHT = 1248;
export const LIMIT_OF_ZIP_SITES_PER_USER = 5;
diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts
index 4f95c219f..e9ce5d4f3 100644
--- a/src/ipc-handlers.ts
+++ b/src/ipc-handlers.ts
@@ -16,7 +16,7 @@ import * as Sentry from '@sentry/electron/main';
import { LocaleData, defaultI18n } from '@wordpress/i18n';
import archiver from 'archiver';
import { DEFAULT_PHP_VERSION } from '../vendor/wp-now/src/constants';
-import { SIZE_LIMIT_BYTES } from './constants';
+import { MAIN_MIN_HEIGHT, MAIN_MIN_WIDTH, SIDEBAR_WIDTH, SIZE_LIMIT_BYTES } from './constants';
import { isEmptyDir, pathExists, isWordPressDirectory, sanitizeFolderName } from './lib/fs-utils';
import { getImageData } from './lib/get-image-data';
import { exportBackup } from './lib/import-export/export/export-manager';
@@ -726,3 +726,25 @@ export function setDefaultLocaleData( _event: IpcMainInvokeEvent, locale?: Local
export function resetDefaultLocaleData( _event: IpcMainInvokeEvent ) {
defaultI18n.resetLocaleData();
}
+
+const previousWidths = {
+ sidebar: MAIN_MIN_WIDTH,
+ noSidebar: MAIN_MIN_WIDTH - SIDEBAR_WIDTH,
+};
+export function toggleMinWindowWidth( event: IpcMainInvokeEvent, isSidebarVisible: boolean ) {
+ const parentWindow = BrowserWindow.fromWebContents( event.sender );
+ if ( ! parentWindow || parentWindow.isDestroyed() || event.sender.isDestroyed() ) {
+ return;
+ }
+ const [ currentWidth, currentHeight ] = parentWindow.getSize();
+ if ( isSidebarVisible ) {
+ previousWidths.sidebar = currentWidth;
+ } else {
+ previousWidths.noSidebar = currentWidth;
+ }
+ const padding = 20;
+ const newMinWidth = isSidebarVisible ? MAIN_MIN_WIDTH - SIDEBAR_WIDTH + padding : MAIN_MIN_WIDTH;
+ const newWidth = isSidebarVisible ? previousWidths.noSidebar : previousWidths.sidebar;
+ parentWindow.setMinimumSize( newMinWidth, MAIN_MIN_HEIGHT );
+ parentWindow.setSize( newWidth, currentHeight, true );
+}
diff --git a/src/preload.ts b/src/preload.ts
index 850b2b1dd..dd1bc6ddd 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -70,6 +70,8 @@ const api: IpcApi = {
setDefaultLocaleData: ( locale?: LocaleData ) =>
ipcRenderer.invoke( 'setDefaultLocaleData', locale ),
resetDefaultLocaleData: () => ipcRenderer.invoke( 'resetDefaultLocaleData' ),
+ toggleMinWindowWidth: ( isSidebarVisible: boolean ) =>
+ ipcRenderer.invoke( 'toggleMinWindowWidth', isSidebarVisible ),
};
contextBridge.exposeInMainWorld( 'ipcApi', api );