diff --git a/package.json b/package.json
index 7f5d220b6..1c5addfb4 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/main/webapp/app/components/geneticTypeTabs/GeneticTypeTabs.tsx b/src/main/webapp/app/components/geneticTypeTabs/GeneticTypeTabs.tsx
index ee02a87d2..eb778eb23 100644
--- a/src/main/webapp/app/components/geneticTypeTabs/GeneticTypeTabs.tsx
+++ b/src/main/webapp/app/components/geneticTypeTabs/GeneticTypeTabs.tsx
@@ -26,8 +26,9 @@ const GeneticTypeTabs: FunctionComponent<{
return (
- {[GENETIC_TYPE.SOMATIC, GENETIC_TYPE.GERMLINE].map(geneOrigin => (
+ {[GENETIC_TYPE.SOMATIC, GENETIC_TYPE.GERMLINE].map((geneOrigin, idx) => (
= props => {
{props.title}
- {props.categories.map(category => (
-
+ {props.categories.map((category, idx) => (
+
))}
diff --git a/src/main/webapp/app/pages/annotationPage/AlterationView.tsx b/src/main/webapp/app/pages/annotationPage/AlterationView.tsx
index 46b6cefce..faf152514 100644
--- a/src/main/webapp/app/pages/annotationPage/AlterationView.tsx
+++ b/src/main/webapp/app/pages/annotationPage/AlterationView.tsx
@@ -179,6 +179,7 @@ export default class AlterationView extends React.Component<
+
{!this.hasContent && (
@@ -840,9 +853,9 @@ export default class SomaticGermlineGenePage extends React.Component<
{this.hasClinicalImplications && (
<>
-
+
Clinical Implications
-
+
0 && (
<>
-
+
Annotated{' '}
{this.isGermline
? 'Variants'
: 'Alterations'}
-
+
+ {children}
+
+ );
+}
diff --git a/src/main/webapp/app/shared/nav/StickyMiniNavBar.tsx b/src/main/webapp/app/shared/nav/StickyMiniNavBar.tsx
new file mode 100644
index 000000000..0b96c3f2b
--- /dev/null
+++ b/src/main/webapp/app/shared/nav/StickyMiniNavBar.tsx
@@ -0,0 +1,218 @@
+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>(
+ {}
+ );
+ const stickyDivRef = useRef(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 (
+
+
+
+
+
+ );
+}
diff --git a/tsconfig.json b/tsconfig.json
index 86191528b..4e3b3f574 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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": "./",
diff --git a/yarn.lock b/yarn.lock
index 1fc13de7c..93b43948e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
@@ -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"