diff --git a/frontend/src/components/Requirements.jsx b/frontend/src/components/Requirements.jsx index 0600b8bc..129b505c 100644 --- a/frontend/src/components/Requirements.jsx +++ b/frontend/src/components/Requirements.jsx @@ -5,8 +5,13 @@ import 'react-treeview/react-treeview.css'; import TreeView from 'react-treeview/lib/react-treeview.js'; const REQUIREMENTS_POPOVER_CLEANUP_KEY = '__tigerpathReqPopoverCleanup'; +const HEADER_POPOVER_CLEANUP_KEY = '__tigerpathHeaderPopoverCleanup'; const TREE_ITEM_CLICK_HANDLER_KEY = '__tigerpathTreeItemClickHandler'; +function escapeHref(url) { + return String(url).replace(/&/g, '&').replace(/"/g, '"'); +} + export default function Requirements({ onChange, requirements, schedule }) { const [loading, setLoading] = useState(false); const containerRef = useRef(null); @@ -52,7 +57,9 @@ export default function Requirements({ onChange, requirements, schedule }) { const Popover = window.bootstrap?.Popover; if (!Popover) return; - const reqLabels = containerRef.current.querySelectorAll('.reqLabel'); + const reqLabels = containerRef.current.querySelectorAll( + '.reqLabel:not(.reqLabel-main)' + ); reqLabels.forEach((reqLabel) => { const existingCleanup = reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY]; if (typeof existingCleanup === 'function') { @@ -100,6 +107,48 @@ export default function Requirements({ onChange, requirements, schedule }) { }); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Main-req info icon: manual trigger + shared hover bridge so the popover stays + // open while moving the pointer onto links (Bootstrap hover trigger closes in the gap). + const addHeaderPopovers = useCallback(() => { + if (!containerRef.current) return; + const Popover = window.bootstrap?.Popover; + if (!Popover) return; + + const icons = containerRef.current.querySelectorAll('.info-icon'); + + icons.forEach((icon) => { + const existingCleanup = icon[HEADER_POPOVER_CLEANUP_KEY]; + if (typeof existingCleanup === 'function') { + existingCleanup(); + } + + const existing = Popover.getInstance(icon); + if (existing) existing.dispose(); + + const popoverInstance = new Popover(icon, { + trigger: 'manual', + html: true, + animation: true, + placement: 'left', + fallbackPlacements: ['left', 'top', 'bottom'], + boundary: 'viewport', + template: + '
' + mainReq.explanation.split('\n').join('
') + '
' + mainReq.description.split('\n').join('
') + '
' +
- contact.type +
- ':
' +
- contact.name +
- '
' +
- contact.email +
- '
' + - url + + linkLabels[i] + '
'; }); + } else { + popoverContent += + 'No official program link is listed for this requirement.
'; } popoverContent += '