diff --git a/apps/modernization-ui/src/apps/search/layout/SearchLayout.tsx b/apps/modernization-ui/src/apps/search/layout/SearchLayout.tsx index b9f72c9e34..f190c5c379 100644 --- a/apps/modernization-ui/src/apps/search/layout/SearchLayout.tsx +++ b/apps/modernization-ui/src/apps/search/layout/SearchLayout.tsx @@ -3,11 +3,13 @@ import { Button } from 'components/button'; import { Loading } from 'components/Spinner'; import { CollapsiblePanel } from 'design-system/collapsible-panel'; import { SearchNavigation } from './navigation/SearchNavigation'; +import { PatientSearchHeader } from './patientSearchHeader/PatientSearchHeader'; import { useSearchInteraction, useSearchResultDisplay } from 'apps/search'; import { SearchLanding } from './landing'; import { SearchResults } from './result'; import { NoResults } from './result/none'; import { NoInput } from './result/NoInput'; +import { FeatureToggle } from 'feature'; import styles from './search-layout.module.scss'; @@ -58,7 +60,12 @@ const SearchLayout = ({ return (
- + features?.search?.events?.enabled} + fallback={}> + + +
ReactNode; + +type Props = { + className?: string; + actions?: ActionsRenderer; +}; + +const PatientSearchHeader = ({ className, actions }: Props) => { + const infoIconRef = useRef(null); + + return ( + + ); +}; + +export { PatientSearchHeader }; diff --git a/apps/modernization-ui/src/apps/search/layout/patientSearchHeader/patient-search-header.module.scss b/apps/modernization-ui/src/apps/search/layout/patientSearchHeader/patient-search-header.module.scss new file mode 100644 index 0000000000..3234812e40 --- /dev/null +++ b/apps/modernization-ui/src/apps/search/layout/patientSearchHeader/patient-search-header.module.scss @@ -0,0 +1,28 @@ +.navigation { + display: flex; + gap: 1rem; + flex-shrink: 0; + z-index: 100; + + h1 { + margin: 0; + } + + .linkContainer { + display: flex; + align-items: center; + min-height: 2.75rem; + } + + .links { + flex-grow: 1; + } +} + +.infoIconContainer { + display: flex; + justify-content: center; + align-items: center; + height: 24px; + width: 24px; +} \ No newline at end of file diff --git a/apps/modernization-ui/src/configuration/configuration.ts b/apps/modernization-ui/src/configuration/configuration.ts index dad8477044..8d0b7dc8f9 100644 --- a/apps/modernization-ui/src/configuration/configuration.ts +++ b/apps/modernization-ui/src/configuration/configuration.ts @@ -19,6 +19,9 @@ type SearchView = Toggle & { }; type Search = { + events: Toggle; + investigations: Toggle; + laboratoryReports: Toggle; view: SearchView; }; diff --git a/apps/modernization-ui/src/configuration/defaults.ts b/apps/modernization-ui/src/configuration/defaults.ts index 525a452b80..d21a9062e4 100644 --- a/apps/modernization-ui/src/configuration/defaults.ts +++ b/apps/modernization-ui/src/configuration/defaults.ts @@ -1,6 +1,15 @@ import { Features, Properties, Configuration, Settings, Search } from './configuration'; const search: Search = { + events: { + enabled: true + }, + investigations: { + enabled: true + }, + laboratoryReports: { + enabled: true + }, view: { table: { enabled: false diff --git a/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.spec.tsx b/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.spec.tsx new file mode 100644 index 0000000000..cb83afec82 --- /dev/null +++ b/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.spec.tsx @@ -0,0 +1,18 @@ +import { render } from '@testing-library/react'; +import RichTooltip from './RichTooltip'; + +describe('when a rich tooltip is displayed', () => { + it('should render with no errors.', async () => { + const mockRichTooltipAnchorRef: React.RefObject = { + current: document.createElement('div', { is: 'mockRichTooltipAnchorRef' }) + }; + const { container } = render( +
+ Contents +
+ ); + + expect(container).toBeTruthy(); + expect(mockRichTooltipAnchorRef.current).toBeTruthy(); + }); +}); diff --git a/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.tsx b/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.tsx new file mode 100644 index 0000000000..396aaba852 --- /dev/null +++ b/apps/modernization-ui/src/design-system/richTooltip/RichTooltip.tsx @@ -0,0 +1,65 @@ +import React, { RefObject, useEffect, useRef } from 'react'; +import { useTooltip } from './useRichTooltip'; +import styles from './rich-tooltip.module.scss'; + +type RichTooltipProps = { + anchorRef: RefObject; + children: React.ReactNode; + marginTop?: number; + marginLeft?: number; +}; + +/** + * Rich Tooltip component that provides a full React Node outlet for the tooltip that you wish to display. + * The children of the tooltip component is the content of the tooltip itself that is shown or hidden on mouseover. + * The anchorRef is the ref to the anchor element that is permanently displayed to the user (i.e an icon or button). + * You can override the default placement of the tooltip using the optional marginTop and marginLeft parameters. + * + * @param {React.ReactNode} param0.children + * @param {RefObject} param0.anchorRef + * @param {number} [param0.marginTop] + * @param {number} [param0.marginLeft] + * @return {JSX.Element} + */ +const RichTooltip = ({ children, anchorRef, marginTop, marginLeft }: RichTooltipProps) => { + const richTooltipRef = useRef(null); + + const { position, isVisible, onMouseEnter, onMouseLeave } = useTooltip({ + anchorRef, + richTooltipRef, + marginTop, + marginLeft + }); + + useEffect(() => { + const element = anchorRef?.current; + if (element) { + element.addEventListener('mouseenter', onMouseEnter); + element.addEventListener('mouseleave', onMouseLeave); + } + return () => { + if (element) { + element.removeEventListener('mouseenter', onMouseEnter); + element.removeEventListener('mouseleave', onMouseLeave); + } + }; + }, [anchorRef, onMouseEnter, onMouseLeave]); + + if (!isVisible) { + return null; + } + + return ( +
+ {children} +
+ ); +}; + +export default RichTooltip; diff --git a/apps/modernization-ui/src/design-system/richTooltip/rich-tooltip.module.scss b/apps/modernization-ui/src/design-system/richTooltip/rich-tooltip.module.scss new file mode 100644 index 0000000000..a74df0a800 --- /dev/null +++ b/apps/modernization-ui/src/design-system/richTooltip/rich-tooltip.module.scss @@ -0,0 +1,16 @@ +@use 'styles/colors.scss'; + +.richtooltipcontainer { + position: absolute; + margin: 0; + padding: 16px; + font-size: 14px; + line-height: 20px; + background-color: colors.$base-white; + color: colors.$base-darkest; + outline-color: colors.$base-lighter; + outline-width: 1px; + outline-style: solid; + filter: drop-shadow(0px 4px 4px colors.$base-light); + width: 300px; +} \ No newline at end of file diff --git a/apps/modernization-ui/src/design-system/richTooltip/useRichTooltip.ts b/apps/modernization-ui/src/design-system/richTooltip/useRichTooltip.ts new file mode 100644 index 0000000000..7683ecf6cb --- /dev/null +++ b/apps/modernization-ui/src/design-system/richTooltip/useRichTooltip.ts @@ -0,0 +1,60 @@ +import { RefObject, useCallback, useMemo, useState } from 'react'; + +type UseTooltipProps = { + anchorRef: RefObject; + richTooltipRef: RefObject; + marginTop?: number; + marginLeft?: number; +}; + +type Position = { + top?: number; + left?: number; + width?: number; +}; + +export function useTooltip({ anchorRef, richTooltipRef, marginTop, marginLeft }: UseTooltipProps) { + const [isVisible, setIsVisible] = useState(false); + const [position, setPosition] = useState({}); + + const calcBottomLeftPosition = (top: number, left: number, width: number) => { + // The left is calculated from the center of the element which the tooltip parent element is attached to. + // The offset is calculated then from subtracting half of that parent element's width to align with the start of the parent element. + const leftOffset = left - width / 2; + return { top: top, left: leftOffset, width: width }; + }; + + useMemo(() => { + if (!anchorRef.current) { + return; + } + + if (isVisible) { + const { top, left, width } = anchorRef.current.getBoundingClientRect(); + setPosition({ ...calcBottomLeftPosition(marginTop || top, marginLeft || left, width) }); + } + + if (!isVisible) { + setPosition({}); + } + }, [isVisible, anchorRef, richTooltipRef]); + + const onMouseEnter = useCallback(() => { + setIsVisible(true); + }, []); + + const onMouseLeave = useCallback(() => { + setIsVisible(false); + }, []); + + return { + position: { + top: position.top ?? 0, + left: position.left ?? 0, + width: position.width ?? 0 + }, + isVisible, + onMouseEnter, + onMouseLeave + }; +}