Skip to content

Commit

Permalink
Merge pull request #1172 from jfkonecn/feature/mini-bar
Browse files Browse the repository at this point in the history
Added mini-navbar
  • Loading branch information
zhx828 authored Oct 17, 2024
2 parents 9d7c3c7 + 30027f9 commit 75b2bf2
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"@types/react-select": "^3.0.21",
"@types/react-spinkit": "^3.0.5",
"@types/reactstrap": "^8.0.4",
"@types/resize-observer-browser": "^0.1.11",
"@types/webpack-env": "1.15.2",
"@typescript-eslint/eslint-plugin": "2.29.0",
"@typescript-eslint/parser": "2.29.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ const GeneticTypeTabs: FunctionComponent<{

return (
<div className={styles.tabs}>
{[GENETIC_TYPE.SOMATIC, GENETIC_TYPE.GERMLINE].map(geneOrigin => (
{[GENETIC_TYPE.SOMATIC, GENETIC_TYPE.GERMLINE].map((geneOrigin, idx) => (
<div
key={idx}
style={{ width: '50%' }}
className={
selected === geneOrigin
Expand Down
4 changes: 2 additions & 2 deletions src/main/webapp/app/components/infoTile/InfoTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const InfoTile: React.FunctionComponent<InfoTile> = props => {
<div className={classnames(styles.tile, 'mr-2', props.className)}>
<div className={'h6 font-bold mb-2'}>{props.title}</div>
<div className={'d-flex'}>
{props.categories.map(category => (
<Category {...category} className={styles.category} />
{props.categories.map((category, idx) => (
<Category key={idx} {...category} className={styles.category} />
))}
</div>
</div>
Expand Down
25 changes: 19 additions & 6 deletions src/main/webapp/app/pages/genePage/SomaticGermlineGenePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ import {
UserGoogleGroupLink,
} from 'app/shared/links/SocialMediaLinks';
import styles from './GenePage.module.scss';
import StickyMiniNavBar from 'app/shared/nav/StickyMiniNavBar';
import MiniNavBarHeader from 'app/shared/nav/MiniNavBarHeader';
import { GenomicIndicatorTable } from 'app/pages/genePage/GenomicIndicatorTable';

interface MatchParams {
Expand Down Expand Up @@ -659,6 +661,17 @@ export default class SomaticGermlineGenePage extends React.Component<
geneticType={this.selectedGeneticType}
/>
<Container>
<StickyMiniNavBar
title={`${this.store.hugoSymbol} (${
this.isGermline ? 'Germline' : 'Somatic'
})`}
linkUnderlineColor={
this.isGermline ? '#D2A106' : '#0968C3'
}
stickyBackgroundColor={
this.isGermline ? '#FCF4D6' : '#F0F5FF'
}
/>
<Row className={`justify-content-center`}>
<Col md={11}>
{!this.hasContent && (
Expand Down Expand Up @@ -840,9 +853,9 @@ export default class SomaticGermlineGenePage extends React.Component<
</If>
{this.isGermline && (
<>
<h4 className={'mt-4'}>
<MiniNavBarHeader id="genomic-indicators">
Genomic Indicators
</h4>
</MiniNavBarHeader>
<GenomicIndicatorTable
data={this.store.genomicIndicators.result}
isPending={
Expand All @@ -853,9 +866,9 @@ export default class SomaticGermlineGenePage extends React.Component<
)}
{this.hasClinicalImplications && (
<>
<h4 className={'mt-4'}>
<MiniNavBarHeader id="clinical-implications">
Clinical Implications
</h4>
</MiniNavBarHeader>
<AlterationTableTabs
selectedTab={this.defaultSelectedTab}
hugoSymbol={this.store.hugoSymbol}
Expand All @@ -879,12 +892,12 @@ export default class SomaticGermlineGenePage extends React.Component<
{this.store.filteredBiologicalAlterations
.length > 0 && (
<>
<h4 className={'mt-4'}>
<MiniNavBarHeader id="annotated">
Annotated{' '}
{this.isGermline
? 'Variants'
: 'Alterations'}
</h4>
</MiniNavBarHeader>
<AnnotatedAlterations
germline={this.isGermline}
hugoSymbol={this.store.hugoSymbol}
Expand Down
13 changes: 13 additions & 0 deletions src/main/webapp/app/shared/nav/MiniNavBarHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

type IMiniNavBarHeader = {
id: string;
children: React.ReactNode;
};
export default function MiniNavBarHeader({ id, children }: IMiniNavBarHeader) {
return (
<h4 id={id} className={'mt-4'} mini-nav-bar-header="">
{children}
</h4>
);
}
217 changes: 217 additions & 0 deletions src/main/webapp/app/shared/nav/StickyMiniNavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Row, Col } from 'react-bootstrap';

function getNavBarSectionElements() {
return document.querySelectorAll('[mini-nav-bar-header]');
}

function useScrollToHash({ stickyHeight }: { stickyHeight: number }) {
const location = useLocation();

useEffect(() => {
const hash = location.hash;
if (hash) {
let element: Element | null;
try {
element = document.querySelector(hash);
} catch {
element = null;
}
if (element) {
const targetPosition =
element.getBoundingClientRect().top + window.scrollY;
window.scrollTo({
behavior: 'smooth',
top: targetPosition - stickyHeight,
});
}
}
}, [location]);
}

type IStickyMiniNavBar = {
title: string;
linkUnderlineColor: string;
stickyBackgroundColor: string;
};

export default function StickyMiniNavBar({
title,
linkUnderlineColor,
stickyBackgroundColor,
}: IStickyMiniNavBar) {
const [headerHeight, setHeaderHeight] = useState(0);
const [isSticky, setIsSticky] = useState(false);
const [sections, setSections] = useState<
{ id: string; label: string | null }[]
>([]);
const [passedElements, setPassedElements] = useState<Record<string, boolean>>(
{}
);
const stickyDivRef = useRef<HTMLDivElement | null>(null);
useScrollToHash({
stickyHeight:
headerHeight +
(stickyDivRef.current?.getBoundingClientRect().height ?? 0),
});

useEffect(() => {
const newSections: typeof sections = [];
const miniNavBarSections = getNavBarSectionElements();
miniNavBarSections.forEach(ele => {
newSections.push({
id: ele.id,
label: ele.textContent,
});
});
setSections(newSections);

const headerElement = document.querySelector('header');

const updateHeaderHeight = () => {
if (headerElement) {
setHeaderHeight(headerElement.getBoundingClientRect().height);
}
};

updateHeaderHeight();

const resizeObserver = new ResizeObserver(() => {
updateHeaderHeight();
});

if (headerElement) {
resizeObserver.observe(headerElement);
}

return () => {
if (headerElement) {
resizeObserver.unobserve(headerElement);
}
};
}, []);

useEffect(() => {
const miniNavBarSections = getNavBarSectionElements();
const intersectionObserver = new IntersectionObserver(entries => {
const newPassedElements: typeof passedElements = {};
entries.forEach(entry => {
const targetId = entry.target.getAttribute('id') ?? '';
const hasId = sections.find(x => x.id === targetId);
newPassedElements[targetId] =
hasId !== undefined &&
(entry.isIntersecting || entry.boundingClientRect.y < 0);
});
setPassedElements(x => {
return {
...x,
...newPassedElements,
};
});
});
miniNavBarSections.forEach(x => intersectionObserver.observe(x));
return () => {
miniNavBarSections.forEach(x => intersectionObserver.unobserve(x));
};
}, [sections]);

const handleScroll = () => {
if (stickyDivRef.current) {
const stickyOffset = stickyDivRef.current.getBoundingClientRect().top;
setIsSticky(stickyOffset <= headerHeight);
}
};

useEffect(() => {
window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [headerHeight]);

let currentSectionId = sections[0]?.id;
for (let i = sections.length - 1; i >= 0; i--) {
const id = sections[i].id;
if (passedElements[id]) {
currentSectionId = id;
break;
}
}

return (
<Row
className="justify-content-center"
style={{
position: 'sticky',
top: headerHeight,
zIndex: 100,
backgroundColor: isSticky ? stickyBackgroundColor : undefined,
}}
>
<Col md={11}>
<nav
ref={stickyDivRef}
className="d-flex flex-row"
style={{
gap: '40px',
}}
>
{isSticky && (
<Link
className="h6 font-weight-bold"
// # is removed from link so we have to use onclick to scroll to the top
to="#"
style={{
color: '#000000',
fontFamily: 'Gotham Bold',
padding: '7px 0px',
}}
onClick={e => {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth',
});
}}
>
{title}
</Link>
)}
<div
className="d-flex flex-row"
style={{
gap: '10px',
}}
>
{sections.map(({ id, label }) => {
const isInSection = currentSectionId === id;
return (
<Link
key={id}
to={`#${id}`}
className={`h6 ${
isInSection ? 'font-weight-bold' : 'font-weight-normal'
}`}
style={{
color: '#000000',
borderColor: isInSection
? linkUnderlineColor
: 'transparent',
borderBottomStyle: 'solid',
borderBottomWidth: '4px',
fontFamily: isInSection ? 'Gotham Bold' : 'Gotham Book',
padding: '7px 0px',
}}
>
{label}
</Link>
);
})}
</div>
</nav>
</Col>
</Row>
);
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"suppressImplicitAnyIndexErrors": true,
"outDir": "target/classes/static/app",
"lib": ["dom", "es2020"],
"types": ["jest", "webpack-env"],
"types": ["jest", "webpack-env", "resize-observer-browser"],
"allowJs": true,
"checkJs": false,
"baseUrl": "./",
Expand Down
13 changes: 9 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2726,6 +2726,11 @@
"@types/react" "*"
popper.js "^1.14.1"

"@types/resize-observer-browser@^0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz#d3c98d788489d8376b7beac23863b1eebdd3c13c"
integrity sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==

"@types/resize-observer-browser@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
Expand Down Expand Up @@ -13180,10 +13185,10 @@ oncokb-styles@~0.1.2:
resolved "https://registry.yarnpkg.com/oncokb-styles/-/oncokb-styles-0.1.2.tgz#8b26c0a0829787cdc1b595d3a021b3266607102b"
integrity sha512-tuy5s3qFxgf1ogMATQSRPNgLlAMrvOOTCAN1dm/wJ+VZoStbJ7g36/qHwc99UPfh3vrB05broLodF+k58p5tUw==

oncokb-styles@~1.6.0-alpha.0:
version "1.6.0-alpha.0"
resolved "https://registry.yarnpkg.com/oncokb-styles/-/oncokb-styles-1.6.0-alpha.0.tgz#5985b23a91583503d9133a2fefbb785d65342699"
integrity sha512-1k5glbxYOg6R8HtMXyXhAnSAmP5MfaZ9ggX1ncLVdwqsHHire19Sxu/RoVtSjdzQQ/m98QfAmd01qGOQZU87WA==
oncokb-styles@~1.4.0-alpha.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/oncokb-styles/-/oncokb-styles-1.4.2.tgz#ad601699636875abe425d80b25c050d28d47c2bc"
integrity sha512-dq/w/OZv7oTjQzyXRo54ldC3PiHHu36eVuFmS0U5PGlk3Qx8XfB9XSwELHKTgmuen5H8YKQJxc/h3cBlFBF7Xw==

oncokb-ts-api-client@^1.0.4:
version "1.0.4"
Expand Down

0 comments on commit 75b2bf2

Please sign in to comment.