From 14708ac254bbc5cca5a27419b948bf20babeb26b Mon Sep 17 00:00:00 2001 From: Frencil Date: Thu, 24 Sep 2020 10:34:27 -0600 Subject: [PATCH] IE polyfills; sticky sidebar improvements --- .env.development | 1 + .env.production | 1 + lib/components/NeonPage/NeonPage.js | 65 +++++++++++----- lib/components/SiteMap/SiteMapContext.js | 27 +++++-- lib/components/SiteMap/SiteMapLeaflet.js | 62 ++++++++-------- package-lock.json | 8 +- package.json | 5 +- src/App.jsx | 7 +- src/index.jsx | 6 ++ .../components/NeonPage/NeonPage.jsx | 57 ++++++++++---- .../components/SiteMap/SiteMapContext.jsx | 16 ++-- .../components/SiteMap/SiteMapLeaflet.jsx | 74 ++++++++++--------- 12 files changed, 206 insertions(+), 123 deletions(-) diff --git a/.env.development b/.env.development index 20868f5b..84f6819b 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,5 @@ PORT=3010 +REACT_APP_VERSION=$npm_package_version REACT_APP_NEON_API_NAME="api" REACT_APP_NEON_API_VERSION="v0" diff --git a/.env.production b/.env.production index 30299801..355f0859 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,4 @@ +REACT_APP_VERSION=$npm_package_version REACT_APP_NEON_API_NAME="api" REACT_APP_NEON_API_VERSION="v0" diff --git a/lib/components/NeonPage/NeonPage.js b/lib/components/NeonPage/NeonPage.js index bcf9ca79..da6c1a7f 100644 --- a/lib/components/NeonPage/NeonPage.js +++ b/lib/components/NeonPage/NeonPage.js @@ -296,6 +296,7 @@ var NeonPage = function NeonPage(props) { var headerRef = (0, _react.useRef)(null); var contentRef = (0, _react.useRef)(null); var sidebarRef = (0, _react.useRef)(null); + var sidebarLinksContainerRef = (0, _react.useRef)(null); var belowMd = (0, _useMediaQuery.default)(_Theme.default.breakpoints.down('sm')); /** Sidebar Setup @@ -312,9 +313,9 @@ var NeonPage = function NeonPage(props) { var sidebarHashMap = !hasSidebarLinks ? {} : Object.fromEntries(sidebarLinks.map(function (link, idx) { return [link.hash || '#', idx]; })); - var initialCurrectSidebarHash = hasSidebarLinks ? sidebarLinks[0].hash || '#' : '#'; + var initialCurrentSidebarHash = hasSidebarLinks ? sidebarLinks[0].hash || '#' : '#'; - var _useState = (0, _react.useState)(initialCurrectSidebarHash), + var _useState = (0, _react.useState)(initialCurrentSidebarHash), _useState2 = _slicedToArray(_useState, 2), currentSidebarHash = _useState2[0], setCurrentSidebarHash = _useState2[1]; @@ -345,12 +346,31 @@ var NeonPage = function NeonPage(props) { var anchor = contentRef.current.querySelector(hash); return !anchor ? -1 : anchor.offsetTop + headerOffset - stickyOffset - _Theme.default.spacing(5); - }, [hasSidebarLinks, sidebarLinksAsStandaloneChildren, belowMd]); // For sidebarLinks pages, on successful load, if hash is present then update the current + }, [hasSidebarLinks, sidebarLinksAsStandaloneChildren, belowMd]); + /** + Effect - For sidebarLinks pages, on successful load, if hash is present then update the current + */ (0, _react.useLayoutEffect)(function () { if (error || loading || !hasSidebarLinks) { return function () {}; - } // Handle URL-defined hash on initial load + } + + var handleHashChange = function handleHashChange() { + var hash = document.location.hash; + + if (currentSidebarHash === hash) { + return; + } + + setCurrentSidebarHash(hash); // If standard sidebar mode (scroll to content) also perform the scroll offset here + + if (!sidebarLinksAsStandaloneChildren) { + window.setTimeout(function () { + window.scrollTo(0, getSidebarLinkScrollPosition(hash)); + }, 0); + } + }; // Handle URL-defined hash on initial load if (document.location.hash && !hashInitialized) { @@ -359,25 +379,25 @@ var NeonPage = function NeonPage(props) { document.location.hash = '#'; } - var hash = document.location.hash; + handleHashChange(); + setHashInitialized(true); + } // Set max-height on sidebar links container when the sidebar is sticky so the links get + // a dedicated scrollbar instead of clipping - if (currentSidebarHash !== hash) { - setCurrentSidebarHash(hash); // If standard sidebar mode (scroll to content) also perform the scroll offset here - if (!sidebarLinksAsStandaloneChildren) { - window.setTimeout(function () { - window.scrollTo(0, getSidebarLinkScrollPosition(hash)); - }, 0); - } - } - - setHashInitialized(true); - } // Set up event listener / handler for user-input scroll events for standard scrolling pages + if (!sidebarUnsticky && hasSidebarLinks && sidebarLinksContainerRef.current) { + var maxHeight = window.innerHeight - sidebarLinksContainerRef.current.offsetTop - 104; + sidebarLinksContainerRef.current.style.maxHeight = "".concat(maxHeight, "px"); + } // For sidebarLinksAsStandaloneChildren listen for hash changes to trigger a "redirect". if (sidebarLinksAsStandaloneChildren) { - return function () {}; - } + window.addEventListener('hashchange', handleHashChange); + return function () { + window.removeEventListener('hashchange', handleHashChange); + }; + } // Set up event listener / handler for user-input scroll events for standard scrolling pages + var handleScroll = function handleScroll() { var scrollBreaks = sidebarLinks.map(function (link) { @@ -403,9 +423,9 @@ var NeonPage = function NeonPage(props) { return function () { window.removeEventListener('scroll', handleScroll); }; - }, [error, loading, hasSidebarLinks, sidebarLinks, sidebarHashMap, hashInitialized, setHashInitialized, currentSidebarHash, setCurrentSidebarHash, getSidebarLinkScrollPosition, sidebarLinksAsStandaloneChildren]); + }, [error, loading, sidebarUnsticky, hasSidebarLinks, sidebarLinks, sidebarHashMap, hashInitialized, setHashInitialized, currentSidebarHash, setCurrentSidebarHash, sidebarLinksContainerRef, getSidebarLinkScrollPosition, sidebarLinksAsStandaloneChildren]); /** - Drupal CSS Loading + Effect - Load Drupal CSS */ var _useState7 = (0, _react.useState)(false), @@ -501,6 +521,10 @@ var NeonPage = function NeonPage(props) { }); })); }; + /** + Effect - Fetch notifications + */ + (0, _react.useEffect)(function () { if (fetchNotificationsStatus !== null) { @@ -706,6 +730,7 @@ var NeonPage = function NeonPage(props) { }; var fullLinks = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { + ref: sidebarLinksContainerRef, className: classes.sidebarLinksContainer }, sidebarLinks.map(function (link) { return renderLink(link); diff --git a/lib/components/SiteMap/SiteMapContext.js b/lib/components/SiteMap/SiteMapContext.js index 996df834..ac43bd5e 100644 --- a/lib/components/SiteMap/SiteMapContext.js +++ b/lib/components/SiteMap/SiteMapContext.js @@ -1103,12 +1103,14 @@ var Provider = function Provider(props) { */ (0, _react.useEffect)(function () { + var noop = function noop() {}; + var _state$focusLocation = state.focusLocation, current = _state$focusLocation.current, currentStatus = _state$focusLocation.fetch.status; if (!current || currentStatus !== _SiteMapUtils.FETCH_STATUS.AWAITING_CALL || !state.neonContextHydrated) { - return; + return noop; } // If the location is a known Domain, State, or Site then pull from NeonContext @@ -1122,7 +1124,7 @@ var Provider = function Provider(props) { var _statesData$current$c = statesData[current].center, latitude = _statesData$current$c[0], longitude = _statesData$current$c[1]; - window.setTimeout(function () { + var timeout = window.setTimeout(function () { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { @@ -1132,14 +1134,17 @@ var Provider = function Provider(props) { } }); }, 0); - return; + return function () { + return window.clearTimeout(timeout); + }; } if (Object.keys(domainsData).includes(current)) { var _domainsData$current$ = domainsData[current].center, _latitude = _domainsData$current$[0], _longitude = _domainsData$current$[1]; - window.setTimeout(function () { + + var _timeout = window.setTimeout(function () { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { @@ -1149,14 +1154,18 @@ var Provider = function Provider(props) { } }); }, 0); - return; + + return function () { + return window.clearTimeout(_timeout); + }; } if (Object.keys(state.sites).includes(current)) { var _state$sites$current = state.sites[current], _latitude2 = _state$sites$current.latitude, _longitude2 = _state$sites$current.longitude; - window.setTimeout(function () { + + var _timeout2 = window.setTimeout(function () { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { @@ -1166,7 +1175,10 @@ var Provider = function Provider(props) { } }); }, 0); - return; + + return function () { + return window.clearTimeout(_timeout2); + }; } // Trigger focus location fetch @@ -1184,6 +1196,7 @@ var Provider = function Provider(props) { error: error }); }); + return noop; }, [state.sites, state.focusLocation, state.focusLocation.fetch.status, state.neonContextHydrated, state.featureData]); /** Effect - trigger all data fetches and imports diff --git a/lib/components/SiteMap/SiteMapLeaflet.js b/lib/components/SiteMap/SiteMapLeaflet.js index 982be8ee..2ee243d7 100644 --- a/lib/components/SiteMap/SiteMapLeaflet.js +++ b/lib/components/SiteMap/SiteMapLeaflet.js @@ -244,45 +244,45 @@ var SiteMapLeaflet = function SiteMapLeaflet() { /** Effect Visually distinguish unselectable markers in the marker pane while also changing the draw order - of marker icons to put unselectable ones behind selectable ones. + of marker icons to put unselectable ones behind selectable ones. Use a 0-length setTimeout to + allow the map to complete one render cycle first. */ - var ghostUnselectables = (0, _react.useCallback)(function () { - if (!mapRef.current || !mapRef.current.leafletElement || !mapRef.current.leafletElement._panes || !mapRef.current.leafletElement._layers || !state.selection.active || !state.selection.validSet) { - return; - } + (0, _react.useLayoutEffect)(function () { + var timeout = window.setTimeout(function () { + if (!mapRef.current || !mapRef.current.leafletElement || !mapRef.current.leafletElement._panes || !mapRef.current.leafletElement._layers || !state.selection.active || !state.selection.validSet || state.view.current !== _SiteMapUtils.VIEWS.MAP) { + return; + } - var markerPane = mapRef.current.leafletElement._panes.markerPane; + var markerPane = mapRef.current.leafletElement._panes.markerPane; - if (markerPane && markerPane.children && markerPane.children.length) { - // Unselectables: apply CSS filters to appear ghosted - _toConsumableArray(markerPane.children).filter(function (marker) { - return !state.selection.validSet.has(marker.title); - }).forEach(function (marker) { - // eslint-disable-next-line no-param-reassign - marker.style.filter = _SiteMapUtils.UNSELECTABLE_MARKER_FILTER; - }); // Selecatbles: Uniformly bump the zIndexOffset to put them all on top + if (markerPane && markerPane.children && markerPane.children.length) { + // Unselectables: apply CSS filters to appear ghosted + _toConsumableArray(markerPane.children).filter(function (marker) { + return !state.selection.validSet.has(marker.title); + }).forEach(function (marker) { + // eslint-disable-next-line no-param-reassign + marker.style.filter = _SiteMapUtils.UNSELECTABLE_MARKER_FILTER; + }); // Selecatbles: Uniformly bump the zIndexOffset to put them all on top - state.selection.validSet.forEach(function (item) { - var layerIdx = Object.keys(mapRef.current.leafletElement._layers).find(function (k) { - return mapRef.current.leafletElement._layers[k].options && mapRef.current.leafletElement._layers[k].options.title === item; - }); + state.selection.validSet.forEach(function (item) { + var layerIdx = Object.keys(mapRef.current.leafletElement._layers).find(function (k) { + return mapRef.current.leafletElement._layers[k].options && mapRef.current.leafletElement._layers[k].options.title === item; + }); - if (layerIdx !== -1) { - var zIndex = (mapRef.current.leafletElement._layers[layerIdx] || {})._zIndex || 0; + if (layerIdx !== -1) { + var zIndex = (mapRef.current.leafletElement._layers[layerIdx] || {})._zIndex || 0; - mapRef.current.leafletElement._layers[layerIdx].setZIndexOffset(zIndex + 1000); - } - }); - } - }); - (0, _react.useLayoutEffect)(ghostUnselectables, [mapRef, state.selection.hideUnselectable]); // Fire ghostUnselectables with a 0-length setTimeout when changing the view to allow the newly - // rendered map to complete one render cycle first. - - (0, _react.useLayoutEffect)(function () { - window.setTimeout(ghostUnselectables, 0); - }, [state.map.bounds, state.view.current]); + mapRef.current.leafletElement._layers[layerIdx].setZIndexOffset(zIndex + 1000); + } + }); + } + }, 0); + return function () { + return window.clearTimeout(timeout); + }; + }, [state.selection.active, state.selection.validSet, state.selection.hideUnselectable, state.map.bounds, state.view]); /** Effect Force a redraw when switching to the map for the first time from another view diff --git a/package-lock.json b/package-lock.json index b892c149..87ab54ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "portal-core-components", - "version": "1.5.1", + "version": "1.5.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -21603,9 +21603,9 @@ } }, "whatwg-fetch": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", - "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", + "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==" }, "whatwg-mimetype": { "version": "2.3.0", diff --git a/package.json b/package.json index 5168e95e..415bed69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portal-core-components", - "version": "1.5.3", + "version": "1.5.4", "main": "./lib/index.js", "private": true, "homepage": "http://localhost:3010/core-components", @@ -53,7 +53,8 @@ "sockjs-client": "^1.4.0", "tinycolor2": "^1.4.1", "typeface-inter": "^3.12.0", - "universal-cookie": "^4.0.2" + "universal-cookie": "^4.0.2", + "whatwg-fetch": "^3.4.1" }, "devDependencies": { "@babel/cli": "^7.8.3", diff --git a/src/App.jsx b/src/App.jsx index 483119a6..af92285f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -133,10 +133,15 @@ const sidebarLinks = [ ]; export default function App() { + let sidebarSubtitle = null; + if (process.env.REACT_APP_VERSION) { + sidebarSubtitle = `version ${process.env.REACT_APP_VERSION}`; + } return ( { const headerRef = useRef(null); const contentRef = useRef(null); const sidebarRef = useRef(null); + const sidebarLinksContainerRef = useRef(null); const belowMd = useMediaQuery(Theme.breakpoints.down('sm')); /** @@ -287,8 +288,8 @@ const NeonPage = (props) => { const sidebarHashMap = !hasSidebarLinks ? {} : Object.fromEntries( sidebarLinks.map((link, idx) => [link.hash || '#', idx]), ); - const initialCurrectSidebarHash = hasSidebarLinks ? sidebarLinks[0].hash || '#' : '#'; - const [currentSidebarHash, setCurrentSidebarHash] = useState(initialCurrectSidebarHash); + const initialCurrentSidebarHash = hasSidebarLinks ? sidebarLinks[0].hash || '#' : '#'; + const [currentSidebarHash, setCurrentSidebarHash] = useState(initialCurrentSidebarHash); const [hashInitialized, setHashInitialized] = useState(false); const [sidebarExpanded, setSidebarExpanded] = useState(false); // for small viewports only @@ -302,29 +303,45 @@ const NeonPage = (props) => { return !anchor ? -1 : anchor.offsetTop + headerOffset - stickyOffset - Theme.spacing(5); }, [hasSidebarLinks, sidebarLinksAsStandaloneChildren, belowMd]); - // For sidebarLinks pages, on successful load, if hash is present then update the current + /** + Effect - For sidebarLinks pages, on successful load, if hash is present then update the current + */ useLayoutEffect(() => { if (error || loading || !hasSidebarLinks) { return () => {}; } + const handleHashChange = () => { + const { hash } = document.location; + if (currentSidebarHash === hash) { return; } + setCurrentSidebarHash(hash); + // If standard sidebar mode (scroll to content) also perform the scroll offset here + if (!sidebarLinksAsStandaloneChildren) { + window.setTimeout(() => { + window.scrollTo(0, getSidebarLinkScrollPosition(hash)); + }, 0); + } + }; // Handle URL-defined hash on initial load if (document.location.hash && !hashInitialized) { // Ensure the document hash maps to a defined hash or '#' at all times if (!Object.keys(sidebarHashMap).includes(document.location.hash)) { document.location.hash = '#'; } - const { hash } = document.location; - if (currentSidebarHash !== hash) { - setCurrentSidebarHash(hash); - // If standard sidebar mode (scroll to content) also perform the scroll offset here - if (!sidebarLinksAsStandaloneChildren) { - window.setTimeout(() => { - window.scrollTo(0, getSidebarLinkScrollPosition(hash)); - }, 0); - } - } + handleHashChange(); setHashInitialized(true); } + // Set max-height on sidebar links container when the sidebar is sticky so the links get + // a dedicated scrollbar instead of clipping + if (!sidebarUnsticky && hasSidebarLinks && sidebarLinksContainerRef.current) { + const maxHeight = window.innerHeight - sidebarLinksContainerRef.current.offsetTop - 104; + sidebarLinksContainerRef.current.style.maxHeight = `${maxHeight}px`; + } + // For sidebarLinksAsStandaloneChildren listen for hash changes to trigger a "redirect". + if (sidebarLinksAsStandaloneChildren) { + window.addEventListener('hashchange', handleHashChange); + return () => { + window.removeEventListener('hashchange', handleHashChange); + }; + } // Set up event listener / handler for user-input scroll events for standard scrolling pages - if (sidebarLinksAsStandaloneChildren) { return () => {}; } const handleScroll = () => { const scrollBreaks = sidebarLinks.map(link => ({ y: getSidebarLinkScrollPosition(link.hash || '#'), @@ -349,6 +366,7 @@ const NeonPage = (props) => { }, [ error, loading, + sidebarUnsticky, hasSidebarLinks, sidebarLinks, sidebarHashMap, @@ -356,12 +374,13 @@ const NeonPage = (props) => { setHashInitialized, currentSidebarHash, setCurrentSidebarHash, + sidebarLinksContainerRef, getSidebarLinkScrollPosition, sidebarLinksAsStandaloneChildren, ]); /** - Drupal CSS Loading + Effect - Load Drupal CSS */ const [drupalCssLoaded, setDrupalCssLoaded] = useState(false); useEffect(() => { @@ -424,6 +443,9 @@ const NeonPage = (props) => { setNotifications(notifications.map(n => ({ ...n, dismissed: false }))); }; + /** + Effect - Fetch notifications + */ useEffect(() => { if (fetchNotificationsStatus !== null) { return; } setFetchNotificationsStatus('fetching'); @@ -605,7 +627,10 @@ const NeonPage = (props) => { }; const fullLinks = ( -
+
{sidebarLinks.map(link => renderLink(link))}
{belowMd ? null : ( diff --git a/src/lib_components/components/SiteMap/SiteMapContext.jsx b/src/lib_components/components/SiteMap/SiteMapContext.jsx index f22b1bc5..0bb83400 100644 --- a/src/lib_components/components/SiteMap/SiteMapContext.jsx +++ b/src/lib_components/components/SiteMap/SiteMapContext.jsx @@ -895,9 +895,10 @@ const Provider = (props) => { Effect - trigger focusLocation fetch or short circuit if found in NeonContext data */ useEffect(() => { + const noop = () => {}; const { current, fetch: { status: currentStatus } } = state.focusLocation; if (!current || currentStatus !== FETCH_STATUS.AWAITING_CALL || !state.neonContextHydrated) { - return; + return noop; } // If the location is a known Domain, State, or Site then pull from NeonContext const { @@ -906,33 +907,33 @@ const Provider = (props) => { } = state.featureData[FEATURE_TYPES.BOUNDARIES]; if (Object.keys(statesData).includes(current)) { const { 0: latitude, 1: longitude } = statesData[current].center; - window.setTimeout(() => { + const timeout = window.setTimeout(() => { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { type: 'STATE', latitude, longitude }, }); }, 0); - return; + return () => window.clearTimeout(timeout); } if (Object.keys(domainsData).includes(current)) { const { 0: latitude, 1: longitude } = domainsData[current].center; - window.setTimeout(() => { + const timeout = window.setTimeout(() => { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { type: 'DOMAIN', latitude, longitude }, }); }, 0); - return; + return () => window.clearTimeout(timeout); } if (Object.keys(state.sites).includes(current)) { const { latitude, longitude } = state.sites[current]; - window.setTimeout(() => { + const timeout = window.setTimeout(() => { dispatch({ type: 'setFocusLocationFetchSucceeded', data: { type: 'SITE', latitude, longitude }, }); }, 0); - return; + return () => window.clearTimeout(timeout); } // Trigger focus location fetch dispatch({ type: 'setFocusLocationFetchStarted' }); @@ -943,6 +944,7 @@ const Provider = (props) => { .catch((error) => { dispatch({ type: 'setFocusLocationFetchFailed', error }); }); + return noop; }, [ state.sites, state.focusLocation, diff --git a/src/lib_components/components/SiteMap/SiteMapLeaflet.jsx b/src/lib_components/components/SiteMap/SiteMapLeaflet.jsx index f9da7438..bf8d6987 100644 --- a/src/lib_components/components/SiteMap/SiteMapLeaflet.jsx +++ b/src/lib_components/components/SiteMap/SiteMapLeaflet.jsx @@ -2,7 +2,6 @@ import React, { useRef, useEffect, - useCallback, useLayoutEffect, } from 'react'; import ReactDOMServer from 'react-dom/server'; @@ -216,42 +215,47 @@ const SiteMapLeaflet = () => { /** Effect Visually distinguish unselectable markers in the marker pane while also changing the draw order - of marker icons to put unselectable ones behind selectable ones. + of marker icons to put unselectable ones behind selectable ones. Use a 0-length setTimeout to + allow the map to complete one render cycle first. */ - const ghostUnselectables = useCallback(() => { - if ( - !mapRef.current || !mapRef.current.leafletElement - || !mapRef.current.leafletElement._panes || !mapRef.current.leafletElement._layers - || !state.selection.active || !state.selection.validSet - ) { return; } - const { markerPane } = mapRef.current.leafletElement._panes; - if (markerPane && markerPane.children && markerPane.children.length) { - // Unselectables: apply CSS filters to appear ghosted - [...markerPane.children] - .filter(marker => !state.selection.validSet.has(marker.title)) - .forEach((marker) => { - // eslint-disable-next-line no-param-reassign - marker.style.filter = UNSELECTABLE_MARKER_FILTER; - }); - // Selecatbles: Uniformly bump the zIndexOffset to put them all on top - state.selection.validSet.forEach((item) => { - const layerIdx = Object.keys(mapRef.current.leafletElement._layers).find(k => ( - mapRef.current.leafletElement._layers[k].options - && mapRef.current.leafletElement._layers[k].options.title === item - )); - if (layerIdx !== -1) { - const zIndex = (mapRef.current.leafletElement._layers[layerIdx] || {})._zIndex || 0; - mapRef.current.leafletElement._layers[layerIdx].setZIndexOffset(zIndex + 1000); - } - }); - } - }); - useLayoutEffect(ghostUnselectables, [mapRef, state.selection.hideUnselectable]); - // Fire ghostUnselectables with a 0-length setTimeout when changing the view to allow the newly - // rendered map to complete one render cycle first. useLayoutEffect(() => { - window.setTimeout(ghostUnselectables, 0); - }, [state.map.bounds, state.view.current]); + const timeout = window.setTimeout(() => { + if ( + !mapRef.current || !mapRef.current.leafletElement + || !mapRef.current.leafletElement._panes || !mapRef.current.leafletElement._layers + || !state.selection.active || !state.selection.validSet + || state.view.current !== VIEWS.MAP + ) { return; } + const { markerPane } = mapRef.current.leafletElement._panes; + if (markerPane && markerPane.children && markerPane.children.length) { + // Unselectables: apply CSS filters to appear ghosted + [...markerPane.children] + .filter(marker => !state.selection.validSet.has(marker.title)) + .forEach((marker) => { + // eslint-disable-next-line no-param-reassign + marker.style.filter = UNSELECTABLE_MARKER_FILTER; + }); + // Selecatbles: Uniformly bump the zIndexOffset to put them all on top + state.selection.validSet.forEach((item) => { + const layerIdx = Object.keys(mapRef.current.leafletElement._layers).find(k => ( + mapRef.current.leafletElement._layers[k].options + && mapRef.current.leafletElement._layers[k].options.title === item + )); + if (layerIdx !== -1) { + const zIndex = (mapRef.current.leafletElement._layers[layerIdx] || {})._zIndex || 0; + mapRef.current.leafletElement._layers[layerIdx].setZIndexOffset(zIndex + 1000); + } + }); + } + }, 0); + return () => window.clearTimeout(timeout); + }, [ + state.selection.active, + state.selection.validSet, + state.selection.hideUnselectable, + state.map.bounds, + state.view, + ]); /** Effect