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"