Unverified Commit cf69c049 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Integrate StandardAppLayout in genome browser page (#225)

Also:
- Use a dedicated component for sidebar tabs rather than generating them in
   StandardAppLayout.
- update npm script for running the style linter
- update test for ZmenuContent component (use mock redux store rather than real one)
- make browser bar content (gene info) responsive to the width of its container
   (via ResizeObserver)
- make opening and closing of the sidebar to be instantaneous rather than animated
  (better for performance)
- remove viewport metatag in the hope that the site will display in its desktop version 
   on mobile devices
parent 2d5326b6
Pipeline #53289 passed with stages
in 4 minutes and 52 seconds
......@@ -24,7 +24,7 @@
"certify": "node setup-ssl",
"lint": "npm run lint:scripts && npm run lint:styles",
"lint:scripts": "eslint 'src/**/*.{ts,tsx}'",
"lint:styles": "stylelint 'src/styles/**/*.scss'",
"lint:styles": "stylelint 'src/**/*.scss'",
"storybook": "start-storybook -p 9001 -c .storybook",
"deploy-docs": "./deploy-docs.sh",
"check-types": "tsc",
......
@import 'src/styles/common';
/*
TODO:
Add a global layout component based on CSS grid:
| HEADER |
| CONTENT |
so that the area allotted to content is calculated automatically by the browser,
and not specifically defined as currently is done below
*/
.content {
&.shorter {
height: calc(100% - 73px);
height: calc(100% - 79px);
}
&.taller {
......
@import 'src/styles/common';
.browser {
width: 100%;
height: calc(100% - 32px);
position: relative;
}
.browserInnerWrapper {
display: flex;
position: absolute;
overflow: hidden;
width: 100vw;
transition: height 0.3s linear;
&.shorter {
height: calc(100vh - 194px);
}
&.taller {
height: calc(100vh - 146px);
}
}
.browserImageWrapper {
display: grid;
height: 100%;
grid-template-rows: 80px 1fr;
}
.exampleLinks {
margin-top: 50px;
margin-left: 40px;
padding-left: 40px;
&__emptyTopBar {
height: 40px; // same as top bar height in StandardAppLayout
background: $light-grey;
box-shadow: 0 2px 3px $grey;
}
}
.exampleLink {
......@@ -46,7 +33,3 @@
.objectLabel {
color: $blue;
}
.semiOpaque {
opacity: 0.3;
}
......@@ -3,9 +3,12 @@ import { MemoryRouter } from 'react-router';
import { mount } from 'enzyme';
import faker from 'faker';
import { BreakpointWidth } from 'src/global/globalConfig';
import { Browser, BrowserProps, ExampleObjectLinks } from './Browser';
import BrowserImage from './browser-image/BrowserImage';
import TrackPanel from './track-panel/TrackPanel';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import { createChrLocationValues } from 'tests/fixtures/browser';
......@@ -16,7 +19,16 @@ jest.mock('./track-panel/TrackPanel', () => () => <div>TrackPanel</div>);
jest.mock('./browser-app-bar/BrowserAppBar', () => () => (
<div>BrowserAppBar</div>
));
jest.mock('ensembl-genome-browser', () => { return; });
jest.mock('./track-panel/track-panel-bar/TrackPanelBar', () => () => (
<div>TrackPanelBar</div>
));
jest.mock('./track-panel/track-panel-tabs/TrackPanelTabs', () => () => (
<div>TrackPanelTabs</div>
));
jest.mock('./drawer/Drawer', () => () => <div>Drawer</div>);
jest.mock('ensembl-genome-browser', () => {
return;
});
describe('<Browser />', () => {
afterEach(() => {
......@@ -38,18 +50,17 @@ describe('<Browser />', () => {
chrLocation: createChrLocationValues().tupleValue,
isDrawerOpened: false,
isTrackPanelOpened: false,
launchbarExpanded: false,
exampleEnsObjects: [],
committedSpecies: [],
changeBrowserLocation: jest.fn(),
changeFocusObject: jest.fn(),
changeDrawerView: jest.fn(),
closeDrawer: jest.fn(),
restoreBrowserTrackStates: jest.fn(),
fetchGenomeData: jest.fn(),
replace: jest.fn(),
toggleTrackPanel: jest.fn(),
toggleDrawer: jest.fn(),
setDataFromUrlAndSave: jest.fn()
setDataFromUrlAndSave: jest.fn(),
viewportWidth: BreakpointWidth.DESKTOP
};
const wrappingComponent = (props: any) => (
......@@ -90,6 +101,33 @@ describe('<Browser />', () => {
expect(wrapper.find(BrowserImage)).toHaveLength(1);
expect(wrapper.find(TrackPanel)).toHaveLength(1);
});
describe('BrowserNavBar', () => {
const props = {
...defaultProps,
browserActivated: true,
browserQueryParams: { focus: 'foo' }
};
it('is rendered when props.browserNavOpened is true', () => {
const wrapper = mountBrowserComponent(props);
expect(wrapper.find(BrowserNavBar).length).toBe(0);
wrapper.setProps({ browserNavOpened: true });
expect(wrapper.find(BrowserNavBar).length).toBe(1);
});
it('is not rendered if drawer is opened', () => {
const wrapper = mountBrowserComponent({
...props,
browserNavOpened: true
});
expect(wrapper.find(BrowserNavBar).length).toBe(1);
wrapper.setProps({ isDrawerOpened: true });
expect(wrapper.find(BrowserNavBar).length).toBe(0);
});
});
});
describe('behaviour', () => {
......@@ -103,15 +141,5 @@ describe('<Browser />', () => {
wrapper.setProps({ activeGenomeId: faker.lorem.words() });
expect(wrapper.props().fetchGenomeData).toHaveBeenCalledTimes(2);
});
test('closes drawer when clicked on genome browser area', () => {
const wrapper = mountBrowserComponent({
browserQueryParams: { focus: faker.lorem.words() },
isDrawerOpened: true,
isTrackPanelOpened: true
});
wrapper.find('.browserImageWrapper').simulate('click');
expect(wrapper.props().closeDrawer).toHaveBeenCalled();
});
});
});
......@@ -2,20 +2,17 @@ import React, { useEffect, useState, useCallback } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { replace, Replace } from 'connected-react-router';
import { useSpring, animated } from 'react-spring';
import { Link } from 'react-router-dom';
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import upperFirst from 'lodash/upperFirst';
import BrowserBar from './browser-bar/BrowserBar';
import BrowserImage from './browser-image/BrowserImage';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import TrackPanel from './track-panel/TrackPanel';
import BrowserAppBar from './browser-app-bar/BrowserAppBar';
import analyticsTracking from 'src/services/analytics-service';
import browserStorageService from './browser-storage-service';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
import { BreakpointWidth } from 'src/global/globalConfig';
import { RootState } from 'src/store';
import { ChrLocation, ChrLocations } from './browserState';
import {
changeBrowserLocation,
changeFocusObject,
......@@ -23,6 +20,10 @@ import {
ParsedUrlPayload,
restoreBrowserTrackStates
} from './browserActions';
import { fetchGenomeData } from 'src/shared/state/genome/genomeActions';
import { toggleTrackPanel } from 'src/content/app/browser/track-panel/trackPanelActions';
import { toggleDrawer } from './drawer/drawerActions';
import {
getBrowserNavOpened,
getChrLocation,
......@@ -33,31 +34,32 @@ import {
getBrowserActiveEnsObjectIds,
getAllChrLocations
} from './browserSelectors';
import { getLaunchbarExpanded } from 'src/header/headerSelectors';
import { getIsTrackPanelOpened } from './track-panel/trackPanelSelectors';
import { getChrLocationFromStr, getChrLocationStr } from './browserHelper';
import { getIsDrawerOpened } from './drawer/drawerSelectors';
import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { CommittedItem } from 'src/content/app/species-selector/types/species-search';
import { getExampleEnsObjects } from 'src/shared/state/ens-object/ensObjectSelectors';
import { EnsObject } from 'src/shared/state/ens-object/ensObjectTypes';
import analyticsTracking from 'src/services/analytics-service';
import { getBreakpointWidth } from 'src/global/globalSelectors';
import { fetchGenomeData } from 'src/shared/state/genome/genomeActions';
import {
changeDrawerView,
closeDrawer,
toggleDrawer
} from './drawer/drawerActions';
import browserStorageService from './browser-storage-service';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
import * as urlFor from 'src/shared/helpers/urlHelper';
import BrowserBar from './browser-bar/BrowserBar';
import BrowserImage from './browser-image/BrowserImage';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import TrackPanel from './track-panel/TrackPanel';
import TrackPanelBar from './track-panel/track-panel-bar/TrackPanelBar';
import TrackPanelTabs from './track-panel/track-panel-tabs/TrackPanelTabs';
import BrowserAppBar from './browser-app-bar/BrowserAppBar';
import Drawer from './drawer/Drawer';
import { StandardAppLayout } from 'src/shared/components/layout';
import styles from './Browser.scss';
import { RootState } from 'src/store';
import { ChrLocation, ChrLocations } from './browserState';
import { CommittedItem } from 'src/content/app/species-selector/types/species-search';
import { EnsObject } from 'src/shared/state/ens-object/ensObjectTypes';
import 'ensembl-genome-browser';
import styles from './Browser.scss';
export type BrowserProps = {
activeGenomeId: string | null;
activeEnsObjectId: string | null;
......@@ -69,20 +71,19 @@ export type BrowserProps = {
chrLocation: ChrLocation | null;
isDrawerOpened: boolean;
isTrackPanelOpened: boolean;
launchbarExpanded: boolean;
exampleEnsObjects: EnsObject[];
committedSpecies: CommittedItem[];
viewportWidth: BreakpointWidth;
changeBrowserLocation: (locationData: {
genomeId: string;
ensObjectId: string | null;
chrLocation: ChrLocation;
}) => void;
changeFocusObject: (objectId: string) => void;
changeDrawerView: (drawerView: string) => void;
closeDrawer: () => void;
restoreBrowserTrackStates: () => void;
fetchGenomeData: (genomeId: string) => void;
replace: Replace;
toggleTrackPanel: (isOpen: boolean) => void;
toggleDrawer: (isDrawerOpened: boolean) => void;
setDataFromUrlAndSave: (payload: ParsedUrlPayload) => void;
};
......@@ -90,7 +91,7 @@ export type BrowserProps = {
export const Browser = (props: BrowserProps) => {
const [, setTrackStatesFromStorage] = useState<BrowserTrackStates>({});
const { isDrawerOpened, closeDrawer } = props;
const { isDrawerOpened } = props;
const params: { [key: string]: string } = useParams();
const location = useLocation();
......@@ -208,41 +209,14 @@ export const Browser = (props: BrowserProps) => {
}
}, [props.browserActivated]);
const closeTrack = () => {
if (!isDrawerOpened) {
return;
}
closeDrawer();
const onSidebarToggle = () => {
props.toggleTrackPanel(!props.isTrackPanelOpened); // FIXME
};
const [trackAnimation, setTrackAnimation] = useSpring(() => ({
config: { tension: 280, friction: 45 },
height: '100%',
width: 'calc(-36px + 100vw )'
}));
const getBrowserWidth = (): string => {
if (isDrawerOpened) {
return 'calc(41px + 0vw)'; // this format must be used for the react-spring animation to function properly
}
return props.isTrackPanelOpened
? 'calc(-356px + 100vw)'
: 'calc(-36px + 100vw)';
const toggleDrawer = () => {
props.toggleDrawer(!props.isDrawerOpened);
};
useEffect(() => {
setTrackAnimation({
width: getBrowserWidth()
});
}, [isDrawerOpened, props.isTrackPanelOpened]);
const getHeightClass = (launchbarExpanded: boolean): string => {
return launchbarExpanded ? styles.shorter : styles.taller;
};
const browserBar = <BrowserBar />;
const shouldShowNavBar =
props.browserActivated && props.browserNavOpened && !isDrawerOpened;
......@@ -250,34 +224,34 @@ export const Browser = (props: BrowserProps) => {
return null;
}
return (
const mainContent = (
<>
{shouldShowNavBar && <BrowserNavBar />}
<BrowserImage />
</>
);
return (
<div className={styles.browserInnerWrapper}>
<BrowserAppBar onSpeciesSelect={changeSelectedSpecies} />
{!props.browserQueryParams.focus && (
<section className={styles.browser}>
{browserBar}
<ExampleObjectLinks {...props} />
</section>
)}
{!!props.browserQueryParams.focus && (
<section className={styles.browser}>
{browserBar}
<div
className={`${styles.browserInnerWrapper} ${getHeightClass(
props.launchbarExpanded
)}`}
>
<animated.div style={trackAnimation}>
<div className={styles.browserImageWrapper} onClick={closeTrack}>
{shouldShowNavBar && <BrowserNavBar />}
<BrowserImage />
</div>
</animated.div>
<TrackPanel />
</div>
</section>
{props.browserQueryParams.focus ? (
<StandardAppLayout
mainContent={mainContent}
sidebarContent={<TrackPanel />}
sidebarNavigation={<TrackPanelTabs />}
sidebarToolstripContent={<TrackPanelBar />}
onSidebarToggle={onSidebarToggle}
topbarContent={<BrowserBar />}
isSidebarOpen={props.isTrackPanelOpened}
isDrawerOpen={props.isDrawerOpened}
drawerContent={<Drawer />}
onDrawerClose={toggleDrawer}
viewportWidth={props.viewportWidth}
/>
) : (
<ExampleObjectLinks {...props} />
)}
</>
</div>
);
};
......@@ -306,7 +280,12 @@ export const ExampleObjectLinks = (props: BrowserProps) => {
);
});
return <div className={styles.exampleLinks}>{links}</div>;
return (
<div>
<div className={styles.exampleLinks__emptyTopBar} />
<div className={styles.exampleLinks}>{links}</div>
</div>
);
};
const mapStateToProps = (state: RootState) => ({
......@@ -320,21 +299,20 @@ const mapStateToProps = (state: RootState) => ({
chrLocation: getChrLocation(state),
isDrawerOpened: getIsDrawerOpened(state),
isTrackPanelOpened: getIsTrackPanelOpened(state),
launchbarExpanded: getLaunchbarExpanded(state),
exampleEnsObjects: getExampleEnsObjects(state),
committedSpecies: getEnabledCommittedSpecies(state)
committedSpecies: getEnabledCommittedSpecies(state),
viewportWidth: getBreakpointWidth(state)
});
const mapDispatchToProps = {
changeBrowserLocation,
changeFocusObject,
changeDrawerView,
closeDrawer,
fetchGenomeData,
replace,
toggleDrawer,
setDataFromUrlAndSave,
restoreBrowserTrackStates
restoreBrowserTrackStates,
toggleTrackPanel
};
export default connect(mapStateToProps, mapDispatchToProps)(Browser);
......@@ -2,134 +2,15 @@
.browserBar {
display: flex;
background: $light-grey;
box-shadow: 0 2px 3px $grey;
height: 32px;
padding: 6px 0 3px 0;
width: 100%;
align-items: center;
position: relative;
z-index: 200;
dd {
display: inline-block;
margin: 0 18px;
}
button {
&.selected {
background: $white;
}
}
}
.browserInfo {
display: flex;
flex-wrap: nowrap;
font-size: 12px;
margin: 0;
width: calc(100vw - 356px);
label {
color: $dark-grey;
font-weight: $light;
}
.value {
margin: 0 3px;
}
&.browserInfoExpanded {
width: calc(100vw - 36px);
@include for-big-desktop-up {
width: calc(100vw - 356px);
}
}
&.browserInfoGreyed {
color: $dark-grey;
}
dd {
margin-right: 10px;
white-space: nowrap;
}
dl {
overflow: hidden;
&.browserInfoLeft {
flex: 1 1 auto;
}
&.browserInfoRight {
flex: 0 1 auto;
text-align: right;
}
.ensObjectLabel {
margin-left: 14px;
text-transform: capitalize;
.value {
font-size: 13px;
font-weight: $bold;
}
}
}
.navigator {
margin: 0 10px 0 0;
position: relative;
top: -2px;
img {
height: 22px;
width: 22px;
}
}
}
.browserInfoRegion {
.chrLabel {
display: inline-block;
margin-right: 5px;
}
span {
cursor: pointer;
}
}
.chrLocationView {
display: inline-block;
}
.chrCode {
background: $dark-blue;
color: $white;
cursor: pointer;
display: inline-block;
font-weight: $bold;
margin: 0 5px;
padding: 1px 4px;
text-align: center;
}
.chrRegion {
color: $dark-blue;
display: inline-block;
}
.browserInfoHidden {
.chrCode {
background: $dark-grey;
}
.chrRegion {
color: $dark-grey;
}
.browserResetWrapper {
margin-right: 14px;
}
.chrSeparator {
margin: 0 2px;
.browserLocationIndicatorWrapper {
margin-left: auto;
}
import React from 'react';
import { mount } from 'enzyme';
import faker from 'faker';
import { BrowserBar, BrowserInfo, BrowserBarProps } from './BrowserBar';