Skip to content

Commit

Permalink
[Chrome service] Expose handler to toggle the sidenav (elastic#193192)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored and markov00 committed Sep 18, 2024
1 parent fa0d681 commit f7bcbbe
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { registerAnalyticsContextProviderMock } from './chrome_service.test.mock
import { shallow, mount } from 'enzyme';
import React from 'react';
import * as Rx from 'rxjs';
import { toArray } from 'rxjs';
import { toArray, firstValueFrom } from 'rxjs';
import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks';
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
Expand Down Expand Up @@ -556,6 +556,39 @@ describe('start', () => {
`);
});
});

describe('side nav', () => {
describe('isCollapsed$', () => {
it('should return false by default', async () => {
const { chrome, service } = await start();
const isCollapsed = await firstValueFrom(chrome.sideNav.getIsCollapsed$());
service.stop();
expect(isCollapsed).toBe(false);
});

it('should read the localStorage value', async () => {
store.set('core.chrome.isSideNavCollapsed', 'true');
const { chrome, service } = await start();
const isCollapsed = await firstValueFrom(chrome.sideNav.getIsCollapsed$());
service.stop();
expect(isCollapsed).toBe(true);
});
});

describe('setIsCollapsed', () => {
it('should update the isCollapsed$ observable', async () => {
const { chrome, service } = await start();
const isCollapsed$ = chrome.sideNav.getIsCollapsed$();
const isCollapsed = await firstValueFrom(isCollapsed$);

chrome.sideNav.setIsCollapsed(!isCollapsed);

const updatedIsCollapsed = await firstValueFrom(isCollapsed$);
service.stop();
expect(updatedIsCollapsed).toBe(!isCollapsed);
});
});
});
});

describe('stop', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type { InternalChromeStart } from './types';
import { HeaderTopBanner } from './ui/header/header_top_banner';

const IS_LOCKED_KEY = 'core.chrome.isLocked';
const IS_SIDENAV_COLLAPSED_KEY = 'core.chrome.isSideNavCollapsed';
const SNAPSHOT_REGEX = /-snapshot/i;

interface ConstructorParams {
Expand Down Expand Up @@ -86,7 +87,9 @@ export class ChromeService {
private readonly docTitle = new DocTitleService();
private readonly projectNavigation: ProjectNavigationService;
private mutationObserver: MutationObserver | undefined;
private readonly isSideNavCollapsed$ = new BehaviorSubject<boolean>(true);
private readonly isSideNavCollapsed$ = new BehaviorSubject(
localStorage.getItem(IS_SIDENAV_COLLAPSED_KEY) === 'true'
);
private logger: Logger;
private isServerless = false;

Expand Down Expand Up @@ -360,6 +363,11 @@ export class ChromeService {
projectNavigation.setProjectName(projectName);
};

const setIsSideNavCollapsed = (isCollapsed: boolean) => {
localStorage.setItem(IS_SIDENAV_COLLAPSED_KEY, JSON.stringify(isCollapsed));
this.isSideNavCollapsed$.next(isCollapsed);
};

if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
notifications.toasts.addWarning({
title: mountReactNode(
Expand Down Expand Up @@ -431,9 +439,8 @@ export class ChromeService {
docLinks={docLinks}
kibanaVersion={injectedMetadata.getKibanaVersion()}
prependBasePath={http.basePath.prepend}
toggleSideNav={(isCollapsed) => {
this.isSideNavCollapsed$.next(isCollapsed);
}}
isSideNavCollapsed$={this.isSideNavCollapsed$}
toggleSideNav={setIsSideNavCollapsed}
>
<SideNavComponent activeNodes={activeNodes} />
</ProjectHeader>
Expand Down Expand Up @@ -556,7 +563,10 @@ export class ChromeService {
getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)),
setChromeStyle,
getChromeStyle$: () => chromeStyle$,
getIsSideNavCollapsed$: () => this.isSideNavCollapsed$.asObservable(),
sideNav: {
getIsCollapsed$: () => this.isSideNavCollapsed$.asObservable(),
setIsCollapsed: setIsSideNavCollapsed,
},
getActiveSolutionNavId$: () => projectNavigation.getActiveSolutionNavId$(),
project: {
setHome: setProjectHome,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('Header', () => {
navControlsCenter$: Rx.of([]),
navControlsRight$: Rx.of([]),
customBranding$: Rx.of({}),
isSideNavCollapsed$: Rx.of(false),
prependBasePath: (str) => `hello/world/${str}`,
toggleSideNav: jest.fn(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface Props {
navControlsCenter$: Observable<ChromeNavControl[]>;
navControlsRight$: Observable<ChromeNavControl[]>;
prependBasePath: (url: string) => string;
isSideNavCollapsed$: Observable<boolean>;
toggleSideNav: (isCollapsed: boolean) => void;
}

Expand Down Expand Up @@ -248,7 +249,12 @@ export const ProjectHeader = ({
<EuiHeader position="fixed" className="header__firstBar">
<EuiHeaderSection grow={false} css={headerCss.leftHeaderSection}>
<Router history={application.history}>
<ProjectNavigation toggleSideNav={toggleSideNav}>{children}</ProjectNavigation>
<ProjectNavigation
isSideNavCollapsed$={observables.isSideNavCollapsed$}
toggleSideNav={toggleSideNav}
>
{children}
</ProjectNavigation>
</Router>

<EuiHeaderSectionItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,28 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useEffect, useRef, FC, PropsWithChildren } from 'react';
import React, { FC, PropsWithChildren } from 'react';
import { EuiCollapsibleNavBeta } from '@elastic/eui';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';

const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const;
interface Props {
toggleSideNav: (isVisible: boolean) => void;
isSideNavCollapsed$: Observable<boolean>;
}

export const ProjectNavigation: FC<
PropsWithChildren<{ toggleSideNav: (isVisible: boolean) => void }>
> = ({ children, toggleSideNav }) => {
const isMounted = useRef(false);
const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false);
const onCollapseToggle = (nextIsCollapsed: boolean) => {
setIsCollapsed(nextIsCollapsed);
toggleSideNav(nextIsCollapsed);
};

useEffect(() => {
if (!isMounted.current && isCollapsed !== undefined) {
toggleSideNav(isCollapsed);
}
isMounted.current = true;
}, [isCollapsed, toggleSideNav]);
export const ProjectNavigation: FC<PropsWithChildren<Props>> = ({
children,
isSideNavCollapsed$,
toggleSideNav,
}) => {
const isCollapsed = useObservable(isSideNavCollapsed$, false);

return (
<EuiCollapsibleNavBeta
data-test-subj="projectLayoutSideNav"
initialIsCollapsed={isCollapsed}
onCollapseToggle={onCollapseToggle}
isCollapsed={isCollapsed}
onCollapseToggle={toggleSideNav}
css={
isCollapsed
? undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ const createStartContractMock = () => {
setBadge: jest.fn(),
getBreadcrumbs$: jest.fn(),
setBreadcrumbs: jest.fn(),
getIsSideNavCollapsed$: jest.fn(),
sideNav: {
getIsCollapsed$: jest.fn(),
setIsCollapsed: jest.fn(),
},
getBreadcrumbsAppendExtension$: jest.fn(),
setBreadcrumbsAppendExtension: jest.fn(),
getGlobalHelpExtensionMenuLinks$: jest.fn(),
Expand Down Expand Up @@ -94,7 +97,7 @@ const createStartContractMock = () => {
startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false));
startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([]));
startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false));
startContract.getIsSideNavCollapsed$.mockReturnValue(new BehaviorSubject(false));
startContract.sideNav.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
return startContract;
};

Expand Down
16 changes: 12 additions & 4 deletions packages/core/chrome/core-chrome-browser/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,18 @@ export interface ChromeStart {
*/
getChromeStyle$(): Observable<ChromeStyle>;

/**
* Get an observable of the current collapsed state of the side nav.
*/
getIsSideNavCollapsed$(): Observable<boolean>;
sideNav: {
/**
* Get an observable of the current collapsed state of the side nav.
*/
getIsCollapsed$(): Observable<boolean>;

/**
* Set the collapsed state of the side nav.
* @param isCollapsed The collapsed state of the side nav.
*/
setIsCollapsed(isCollapsed: boolean): void;
};

/**
* Get the id of the currently active project navigation or `null` otherwise.
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-ux/chrome/navigation/src/services.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const NavigationKibanaProvider: FC<PropsWithChildren<NavigationKibanaDepe
const { chrome, http, analytics } = core;
const { basePath } = http;
const { navigateToUrl } = core.application;
const isSideNavCollapsed = useObservable(chrome.getIsSideNavCollapsed$(), true);
const isSideNavCollapsed = useObservable(chrome.sideNav.getIsCollapsed$(), true);

const value: NavigationServices = useMemo(
() => ({
Expand Down
4 changes: 3 additions & 1 deletion packages/shared-ux/chrome/navigation/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export interface NavigationKibanaDependencies {
navLinks: {
getNavLinks$: () => Observable<Readonly<ChromeNavLink[]>>;
};
getIsSideNavCollapsed$: () => Observable<boolean>;
sideNav: {
getIsCollapsed$: () => Observable<boolean>;
};
};
http: {
basePath: BasePathService;
Expand Down

0 comments on commit f7bcbbe

Please sign in to comment.