Skip to content

Commit

Permalink
Make app responsive and toggle sidebar (#533)
Browse files Browse the repository at this point in the history
* Create a new top bar
* Move Authentication buttons to that top bar
* Move offline indicator to the TopBar
* Add a new button to toggle the sidebar
* Fix tests (I'll move the topbar tests to its own file)
* Both states have size memory
  • Loading branch information
sejas committed Sep 13, 2024
1 parent ea25dcf commit 5542c97
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 184 deletions.
28 changes: 25 additions & 3 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import {
__experimentalVStack as VStack,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { useState } from 'react';
import { useLocalizationSupport } from '../hooks/use-localization-support';
import { useOnboarding } from '../hooks/use-onboarding';
import { isWindows } from '../lib/app-globals';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import MainSidebar from './main-sidebar';
import Onboarding from './onboarding';
import { SiteContentTabs } from './site-content-tabs';
import TopBar from './top-bar';
import UserSettings from './user-settings';
import WindowsTitlebar from './windows-titlebar';

export default function App() {
useLocalizationSupport();
const { needsOnboarding } = useOnboarding();
const [ isSidebarVisible, setIsSidebarVisible ] = useState( true );
const toggleSidebar = () => {
getIpcApi().toggleMinWindowWidth( isSidebarVisible );
setIsSidebarVisible( ! isSidebarVisible );
};

return (
<>
Expand All @@ -35,12 +43,26 @@ export default function App() {
) }
spacing="0"
>
{ isWindows() && <WindowsTitlebar className="h-titlebar-win flex-shrink-0" /> }
{ isWindows() ? (
<WindowsTitlebar className="h-titlebar-win flex-shrink-0">
<TopBar onToggleSidebar={ toggleSidebar } />
</WindowsTitlebar>
) : (
<div className="pl-20 flex-shrink-0">
<TopBar onToggleSidebar={ toggleSidebar } />
</div>
) }

<HStack spacing="0" alignment="left" className="flex-grow">
<MainSidebar className="basis-52 flex-shrink-0 h-full" />
<MainSidebar
className={ cx(
'h-full transition-all duration-500',
isSidebarVisible ? 'basis-52 flex-shrink-0' : 'basis-0 !min-w-[10px]'
) }
/>
<main
data-testid="site-content"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden z-10"
>
<SiteContentTabs />
</main>
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function ButtonsSection( { buttonsArray, title, className = '' }: Buttons
return (
<div className="w-full">
<h2 className="a8c-subtitle-small mb-3">{ title }</h2>
<div className={ cx( 'gap-3', className || 'grid sd:grid-cols-2 lg:grid-cols-3' ) }>
<div className={ cx( 'gap-3', className || 'grid grid-cols-2 lg:grid-cols-3' ) }>
{ buttonsArray.map( ( button, index ) => (
<Button
className={ button.className }
Expand Down
2 changes: 1 addition & 1 deletion src/components/content-tab-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const ImportSite = ( props: { selectedSite: SiteDetails } ) => {
>
<div
className={ cx(
'h-48 w-full rounded-sm border border-zinc-300 flex-col justify-center items-center inline-flex',
'h-36 w-full rounded-sm border border-zinc-300 flex-col justify-center items-center inline-flex',
isDraggingOver && ! isImporting && 'border-a8c-blueberry bg-a8c-gray-0'
) }
>
Expand Down
12 changes: 5 additions & 7 deletions src/components/gravatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,26 @@ 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();
const { user } = useAuth();
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 = () => (
<Icon
icon={ detailedDefaultImage ? profileIconDetailed : commentAuthorAvatar }
viewBox={ detailedDefaultImage ? '0 0 32 33' : '4 4 16 16' }
size={ isLarge ? 32 : 16 }
size={ size }
className={ childClassName }
/>
);
Expand All @@ -47,6 +44,7 @@ export function Gravatar( {
alt={ __( 'User avatar' ) }
className={ childClassName }
onError={ () => setImageError( true ) }
style={ { width: `${ size }px`, height: `${ size }px` } }
/>
);
}
113 changes: 2 additions & 111 deletions src/components/main-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav aria-label={ __( 'Global' ) }>
<ul className="flex items-start self-stretch w-full">
<li>
<Button
onClick={ () => getIpcApi().showUserSettings() }
aria-label={ __( 'Account' ) }
variant="icon"
>
<Gravatar className="m-1" />
</Button>
</li>
<li className="ml-1.5">
<Button onClick={ openDocs } aria-label={ __( 'Help' ) } variant="icon">
<Icon size={ 22 } className="m-px text-white" icon={ help } />
</Button>
</li>
</ul>
</nav>
);
}

return (
<Tooltip
disabled={ ! isOffline }
icon={ offlineIcon }
text={ offlineMessage }
placement="right"
className="flex"
>
<Button
aria-description={ isOffline ? offlineMessage : '' }
aria-disabled={ isOffline }
className="flex gap-x-2 justify-between w-full text-white rounded !px-0 py-1 h-auto active:!text-white hover:!text-white hover:underline items-end"
onClick={ () => {
if ( isOffline ) {
return;
}
authenticate();
} }
>
<WordPressLogo />

<div className="text-xs text-right">{ __( 'Log in' ) }</div>
</Button>
</Tooltip>
);
}

function SidebarToolbar() {
const isOffline = useOffline();
const offlineMessage = [
__( 'You’re currently offline.' ),
__( 'Some features will be unavailable.' ),
];
return (
<div
className={ cx(
'absolute right-4 app-no-drag-region',
isMac() && 'top-1',
! isMac() && 'top-0'
) }
>
{ isOffline && (
<Tooltip
text={
<span>
{ offlineMessage[ 0 ] }
<br />
{ offlineMessage[ 1 ] }
</span>
}
>
<Button
aria-label={ __( 'Offline indicator' ) }
aria-description={ offlineMessage.join( ' ' ) }
className="cursor-default"
variant="icon"
>
<Icon className="m-1 text-white" size={ 16 } icon={ offlineIcon } />
</Button>
</Tooltip>
) }
</div>
);
}

export default function MainSidebar( { className }: MainSidebarProps ) {
return (
<div
data-testid="main-sidebar"
className={ cx(
'text-chrome-inverted relative',
isMac() && 'pt-[50px]',
isMac() && 'pt-[10px]',
! isMac() && 'pt-[38px]',
className
) }
>
<SidebarToolbar />
<div className="flex flex-col h-full">
<div
className={ cx(
Expand All @@ -138,10 +32,7 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
<div className="flex flex-col gap-4 pt-5 border-white border-t border-opacity-10 app-no-drag-region">
<RunningSites />
<div className={ cx( isMac() ? 'mx-5' : 'mx-4' ) }>
<AddSite className="w-full mb-4" />
<div className="mb-[6px]">
<SidebarAuthFooter />
</div>
<AddSite className="min-w-[168px] w-full mb-4" />
</div>
</div>
</div>
Expand Down
59 changes: 1 addition & 58 deletions src/components/tests/main-sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -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' );
Expand Down Expand Up @@ -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( <MainSidebar /> ) );
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( <MainSidebar /> ) );
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( <MainSidebar className={ 'test-class' } /> )
Expand All @@ -94,30 +76,6 @@ describe( 'MainSidebar Footer', () => {
await act( async () => render( <MainSidebar /> ) );
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( <MainSidebar /> ) );
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( <MainSidebar /> ) );
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', () => {
Expand Down Expand Up @@ -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( <MainSidebar /> );

const helpIconButton = screen.getByRole( 'button', { name: 'Help' } );
await user.click( helpIconButton );
await waitFor( () =>
expect( mockOpenURL ).toHaveBeenCalledWith(
`https://developer.wordpress.com/docs/developer-tools/studio/`
)
);
} );
} );
Loading

0 comments on commit 5542c97

Please sign in to comment.