Skip to content

Commit

Permalink
Update browser bar (#1071)
Browse files Browse the repository at this point in the history
  • Loading branch information
azangru authored Jan 10, 2024
1 parent c47918d commit c25ef15
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 193 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
.browserBar {
display: flex;
display: grid;
grid-template-columns: [browser-reset] auto [feature-summary] 1fr [empty] 20px [location-indicator] auto; /* "empty" column plus two gaps around it should produce 60px of white space */
column-gap: 20px;
width: 100%;
align-items: center;
position: relative;
padding-left: 18px;
}

.browserResetWrapper {
margin-right: 20px;
.featureSummaryStrip {
justify-self: start;
max-width: 100%;
}

.browserLocationIndicatorWrapper {
margin-left: auto;
.browserLocationIndicator {
grid-column: location-indicator;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import React from 'react';
import React, { useRef } from 'react';

import { useAppSelector } from 'src/store';

Expand All @@ -31,26 +31,31 @@ export const BrowserBar = () => {
const focusObject = useAppSelector(getBrowserActiveFocusObject);
const isDrawerOpened = useAppSelector(getIsDrawerOpened);

const browserBarRef = useRef<HTMLDivElement>(null);
const featureSummaryRef = useRef<HTMLDivElement>(null);

// return empty div instead of null, so that the dedicated slot in the CSS grid of StandardAppLayout
// always contains a child DOM element
if (!focusObject) {
return <div />;
}

return (
<div className={styles.browserBar}>
<div className={styles.browserResetWrapper}>
<BrowserReset />
</div>
<div className={styles.browserBar} ref={browserBarRef}>
<BrowserReset />
{focusObject && (
<FeatureSummaryStrip
focusObject={focusObject}
isGhosted={isDrawerOpened}
ref={featureSummaryRef}
className={styles.featureSummaryStrip}
/>
)}
<div className={styles.browserLocationIndicatorWrapper}>
<BrowserLocationIndicator />
</div>
<BrowserLocationIndicator
className={styles.browserLocationIndicator}
containerRef={browserBarRef}
nonOverlapElementRef={featureSummaryRef}
/>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
align-items: center;
white-space: nowrap;
flex-wrap: nowrap;
line-height: 1;
font-size: 14px;
}

Expand All @@ -22,14 +21,14 @@
align-items: center;
}

.chrCode {
background: var(--color-dark-grey);
color: var(--color-white);
.regionNameContainer {
display: inline-block;
font-weight: var(--font-weight-bold);
margin: 0 0.5em;
padding: 5px;
text-align: center;
position: relative;
margin-right: 0.5em;
}

.regionName {
color: var(--color-medium-dark-grey);
}

.chrSeparator {
Expand All @@ -40,3 +39,18 @@
color: var(--color-dark-grey);
display: inline-block;
}

.probeAnchor {
position: absolute;
top: 0;
right: 0;
height: calc(14px * 1.5); /* font size of the element times the line height */
}

.probe {
position: absolute;
top: 0;
right: 0;
color: red;
visibility: hidden;
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ describe('BrowserLocationIndicator', () => {
it('displays chromosome name', async () => {
const { container } = renderBrowserLocationIndicator();
await waitFor(() => {
const renderedName = container.querySelector('.chrCode');
const renderedName = container.querySelector('.regionName');
expect(renderedName?.textContent).toBe(humanChromosomeName);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

import React from 'react';
import React, { useState, useEffect, useRef, type RefObject } from 'react';
import classNames from 'classnames';

import { formatNumber } from 'src/shared/helpers/formatters/numberFormatter';

Expand All @@ -26,29 +27,61 @@ import { getActualChrLocation } from 'src/content/app/genome-browser/state/brows

import styles from './BrowserLocationIndicator.module.css';

export const BrowserLocationIndicator = () => {
type Props = {
className?: string;
containerRef?: RefObject<HTMLElement>;
nonOverlapElementRef?: RefObject<HTMLElement>;
};

export const BrowserLocationIndicator = (props: Props) => {
const shouldCheckProximity = props.containerRef && props.nonOverlapElementRef;
const [shouldShowRegionName, setShouldShowRegionName] = useState(
!shouldCheckProximity
); // start with false if component needs to figure out how close it is to its neightbor on the left
const actualChrLocation = useAppSelector(getActualChrLocation);
const activeGenomeId = useAppSelector(getBrowserActiveGenomeId) as string;

const { data: genomeKaryotype } = useGenomeKaryotypeQuery(activeGenomeId);

const [chrCode, chrStart, chrEnd] = actualChrLocation || [];
const [regionName, chrStart, chrEnd] = actualChrLocation || [];

if (!chrCode || !chrStart || !chrEnd || !activeGenomeId) {
if (!regionName || !chrStart || !chrEnd || !activeGenomeId) {
return null;
}

const activeChromosome = genomeKaryotype?.find((karyotype) => {
return karyotype.name === chrCode;
return karyotype.name === regionName;
});

const componentClasses = classNames(
styles.browserLocationIndicator,
props.className
);

const onRegionNameVisibilityChange = (shouldShowRegionName: boolean) => {
setShouldShowRegionName(shouldShowRegionName);
};

return (
<div className={styles.browserLocationIndicator}>
<div className={componentClasses}>
<div className={styles.chrLocationView}>
{activeChromosome?.is_circular ? (
<CircularChromosomeIndicator />
) : (
<div className={styles.chrCode}>{chrCode}</div>
<div className={styles.regionNameContainer}>
{shouldShowRegionName && (
<span className={styles.regionName}>{regionName}</span>
)}
{props.nonOverlapElementRef && props.containerRef && (
<ProximitySensor
regionName={regionName}
containerRef={props.containerRef}
nonOverlapElementRef={props.nonOverlapElementRef}
isRegionNameVisible={shouldShowRegionName}
onRegionNameVisibilityChange={onRegionNameVisibilityChange}
/>
)}
</div>
)}
<div className={styles.chrRegion}>
<span>{formatNumber(chrStart as number)}</span>
Expand All @@ -60,6 +93,72 @@ export const BrowserLocationIndicator = () => {
);
};

const ProximitySensor = ({
regionName,
containerRef,
nonOverlapElementRef,
isRegionNameVisible,
onRegionNameVisibilityChange
}: {
regionName: string;
containerRef: RefObject<HTMLElement>;
nonOverlapElementRef: RefObject<HTMLElement>;
isRegionNameVisible: boolean;
onRegionNameVisibilityChange: (x: boolean) => void;
}) => {
const probeRef = useRef<HTMLSpanElement>(null);
const isRegionNameVisibleRef = useRef(isRegionNameVisible);
const minDistanceToLeft = 60;

useEffect(() => {
if (!probeRef.current) {
return;
}

const resizeObserver = new ResizeObserver(onContainerResize);
resizeObserver.observe(containerRef.current as HTMLElement);

return () => {
resizeObserver.disconnect();
};
}, [probeRef.current, nonOverlapElementRef.current]);

useEffect(() => {
isRegionNameVisibleRef.current = isRegionNameVisible;
}, [isRegionNameVisible]);

const onContainerResize = () => {
const isTooClose = isTooCloseToLeft();
const isRegionNameVisible = isRegionNameVisibleRef.current;
if (isTooClose && isRegionNameVisible) {
onRegionNameVisibilityChange(false);
} else if (!isTooClose && !isRegionNameVisible) {
onRegionNameVisibilityChange(true);
}
};

const isTooCloseToLeft = () => {
const featureSummaryStrip = nonOverlapElementRef.current as HTMLElement;
const probe = probeRef.current as HTMLElement;
const featureSummaryStripRightCoord =
featureSummaryStrip.getBoundingClientRect().right;
const isfeatureSummaryStripOverflowing =
featureSummaryStrip.scrollWidth > featureSummaryStrip.clientWidth;
const probeLeftCoord = probe.getBoundingClientRect().left;
const distance = probeLeftCoord - featureSummaryStripRightCoord;

return distance < minDistanceToLeft || isfeatureSummaryStripOverflowing;
};

return (
<div className={styles.probeAnchor}>
<span className={styles.probe} ref={probeRef}>
{regionName}
</span>
</div>
);
};

const CircularChromosomeIndicator = () => {
return <div className={styles.circularIndicator}></div>;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
.featureSummaryStripWrapper {
flex-grow: 1;
}

.featureSummaryStrip {
font-size: 14px;
display: flex;
align-items: center;
white-space: nowrap;
flex-wrap: nowrap;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
}

.section {
display: inline;
}

.featureSummaryStrip > div:not(:last-child) {
margin-right: 30px;
.section:not(:first-child) {
margin-left: 30px;
}

.featureSummaryStripGhosted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import {

import { createFocusObject } from 'tests/fixtures/focus-object';

jest.mock('../feature-summary-strip', () => ({
GeneSummaryStrip: () => <div>Gene Summary Strip</div>,
LocationSummaryStrip: () => <div>Location Summary Strip</div>
}));
jest.mock('./GeneSummaryStrip', () => {
const { forwardRef } = jest.requireActual('react');
return forwardRef(() => <div>Gene Summary Strip</div>);
});
jest.mock('./LocationSummaryStrip', () => {
const { forwardRef } = jest.requireActual('react');
return forwardRef(() => <div>Location Summary Strip</div>);
});

describe('<FeatureSummaryStrip />', () => {
const defaultProps = {
Expand Down
41 changes: 30 additions & 11 deletions src/shared/components/feature-summary-strip/FeatureSummaryStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,57 @@
* limitations under the License.
*/

import React from 'react';
import React, { forwardRef, type ForwardedRef } from 'react';

import {
GeneSummaryStrip,
LocationSummaryStrip,
VariantSummaryStrip
} from '../feature-summary-strip';
import GeneSummaryStrip from './GeneSummaryStrip';
import LocationSummaryStrip from './LocationSummaryStrip';
import VariantSummaryStrip from './VariantSummaryStrip';

import type { FocusObject } from 'src/shared/types/focus-object/focusObjectTypes';

export type FeatureSummaryStripProps = {
focusObject: FocusObject;
className?: string;
isGhosted?: boolean;
};

export const FeatureSummaryStrip = (props: FeatureSummaryStripProps) => {
export const FeatureSummaryStrip = (
props: FeatureSummaryStripProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const { focusObject, isGhosted } = props;

switch (focusObject.type) {
case 'gene':
return <GeneSummaryStrip gene={focusObject} isGhosted={isGhosted} />;
return (
<GeneSummaryStrip
gene={focusObject}
isGhosted={isGhosted}
ref={ref}
className={props.className}
/>
);
case 'location':
return (
<LocationSummaryStrip location={focusObject} isGhosted={isGhosted} />
<LocationSummaryStrip
location={focusObject}
isGhosted={isGhosted}
ref={ref}
className={props.className}
/>
);
case 'variant':
return (
<VariantSummaryStrip variant={focusObject} isGhosted={isGhosted} />
<VariantSummaryStrip
variant={focusObject}
isGhosted={isGhosted}
ref={ref}
className={props.className}
/>
);
default:
return null;
}
};

export default FeatureSummaryStrip;
export default forwardRef(FeatureSummaryStrip);
Loading

0 comments on commit c25ef15

Please sign in to comment.