diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts index 2bce99e9fd1..3bbd40cb3ca 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts @@ -353,7 +353,15 @@ export class LegendEventProcessor extends AbstractEventProcessor { createNewLegendEntries(2, layers); // Update the legend layers with the updated array, triggering the subscribe - this.getLayerState(mapId).setterActions.setLegendLayers(layers); + // Reorder the array so legend tab is in synch + const sortedLayers = layers.sort((a, b) => + MapEventProcessor.getMapIndexFromOrderedLayerInfo(mapId, a.layerPath) > + MapEventProcessor.getMapIndexFromOrderedLayerInfo(mapId, b.layerPath) + ? 1 + : -1 + ); + + this.getLayerState(mapId).setterActions.setLegendLayers(sortedLayers); } // #endregion diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx index cb10c5b3f58..d8b227fc069 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@mui/material'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { Box, Collapse, List } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; import { getSxClasses } from './legend-styles'; @@ -24,6 +24,14 @@ interface WMSLegendImageProps { sxClasses: Record; } +// Constant style outside of render +const styles = { + wmsImage: { + maxWidth: '90%', + cursor: 'pointer', + }, +} as const; + // Extracted WMS Legend Component const WMSLegendImage = memo( ({ imgSrc, initLightBox, legendExpanded, sxClasses }: WMSLegendImageProps): JSX.Element => ( @@ -32,7 +40,7 @@ const WMSLegendImage = memo( component="img" tabIndex={0} src={imgSrc} - sx={{ maxWidth: '90%', cursor: 'pointer' }} + sx={styles.wmsImage} onClick={() => initLightBox(imgSrc, '', 0, 2)} onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} /> @@ -51,7 +59,7 @@ export const CollapsibleContent = memo(function CollapsibleContent({ // Hooks const theme = useTheme(); - const sxClasses = getSxClasses(theme); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); // Props extraction const { children, items } = layer; diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx index 3c913aced08..bea5dcea6fc 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx @@ -29,6 +29,11 @@ type ControlActions = { handleZoomTo: (e: React.MouseEvent) => void; }; +// Constant style outside of render +const styles = { + btnMargin: { marginTop: '-0.3125rem' }, +} as const; + // Custom hook for control actions const useControlActions = (layerPath: string): ControlActions => { const { setOrToggleLayerVisibility } = useMapStoreActions(); @@ -73,7 +78,7 @@ const useSubtitle = (children: TypeLegendLayer[], items: TypeLegendItem[]): stri }, [children.length, items, t]); }; -// SecondaryControls component +// SecondaryControls component (no memo to force re render from layers panel modifications) export function SecondaryControls({ layer, visibility }: SecondaryControlsProps): JSX.Element { logger.logTraceRender('components/legend/legend-layer-ctrl'); @@ -111,12 +116,7 @@ export function SecondaryControls({ layer, visibility }: SecondaryControlsProps) > {visibility ? : } - + {highlightedLayer === layer.layerPath ? : } diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx index 6b03220e074..440f39bbd5f 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx @@ -22,6 +22,7 @@ const LegendListItem = memo( ); LegendListItem.displayName = 'LegendListItem'; +// Item list component (no memo to force re render from layers panel modifications) export const ItemsList = memo(function ItemsList({ items }: ItemsListProps): JSX.Element | null { logger.logTraceRender('components/legend/legend-layer-items'); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer.tsx b/packages/geoview-core/src/core/components/legend/legend-layer.tsx index 315498bc911..78a11f4adf7 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTheme } from '@mui/material'; import { Box, ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; @@ -8,6 +8,7 @@ import { LayerIcon } from '../common/layer-icon'; import { SecondaryControls } from './legend-layer-ctrl'; import { CollapsibleContent } from './legend-layer-container'; import { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; interface LegendLayerProps { layer: TypeLegendLayer; @@ -20,6 +21,13 @@ interface LegendLayerHeaderProps { onExpandClick: (e: React.MouseEvent) => void; } +// Constant style outside of render +const styles = { + listItemText: { + '&:hover': { cursor: 'pointer' }, + }, +} as const; + // Extracted Header Component const LegendLayerHeader = memo( ({ layer, isCollapsed, isVisible, onExpandClick }: LegendLayerHeaderProps): JSX.Element => ( @@ -27,7 +35,7 @@ const LegendLayerHeader = memo( getSxClasses(theme), [theme]); // Stores const { initLightBox, LightBoxComponent } = useLightBox(); @@ -90,5 +100,3 @@ export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { ); } - -export default LegendLayer; diff --git a/packages/geoview-core/src/core/components/legend/legend.tsx b/packages/geoview-core/src/core/components/legend/legend.tsx index 2bf0657c9dc..dc82e84f48e 100644 --- a/packages/geoview-core/src/core/components/legend/legend.tsx +++ b/packages/geoview-core/src/core/components/legend/legend.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@mui/material'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Typography } from '@/ui'; import { useGeoViewMapId } from '@/core/stores/'; @@ -18,44 +18,78 @@ interface LegendType { containerType?: 'appBar' | 'footerBar'; } +// Constant style outside of render (styles) +const styles = { + noLayersContainer: { + padding: '2rem', + margin: '2rem', + width: '100%', + textAlign: 'center', + }, + layerBox: { + paddingRight: '0.65rem', + }, + flexContainer: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + }, +} as const; + +// Constant style outside of render (responsive widths) +const responsiveWidths = { + full: { xs: '100%' }, + responsive: { + xs: '100%', + sm: '50%', + md: '33.33%', + lg: '25%', + xl: '25%', + }, +} as const; + export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): JSX.Element { logger.logTraceRender('components/legend/legend'); - const mapId = useGeoViewMapId(); + // Hooks const { t } = useTranslation(); - const theme = useTheme(); const sxClasses = getSxClasses(theme); - // internal state + // State const [legendLayers, setLegendLayers] = useState([]); const [formattedLegendLayerList, setFormattedLegendLayersList] = useState([]); - // store state + // Store + const mapId = useGeoViewMapId(); const orderedLayerInfo = useMapOrderedLayerInfo(); const layersList = useLayerLegendLayers(); // Custom hook for calculating the height of footer panel const { leftPanelRef } = useFooterPanelHeight({ footerPanelTab: 'legend' }); + // Memoize breakpoint values + const breakpoints = useMemo( + () => ({ + sm: theme.breakpoints.values.sm, + md: theme.breakpoints.values.md, + lg: theme.breakpoints.values.lg, + }), + [theme.breakpoints.values.sm, theme.breakpoints.values.md, theme.breakpoints.values.lg] + ); + /** * Get the size of list based on window size. */ - const getLegendLayerListSize = useMemo(() => { - return () => { - let size = 4; - // when legend is loaded in appbar size will always be 1. - if (containerType === CONTAINER_TYPE.APP_BAR) return 1; - if (window.innerWidth < theme.breakpoints.values.sm) { - size = 1; - } else if (window.innerWidth < theme.breakpoints.values.md) { - size = 2; - } else if (window.innerWidth < theme.breakpoints.values.lg) { - size = 3; - } - return size; - }; - }, [theme.breakpoints.values.lg, theme.breakpoints.values.md, theme.breakpoints.values.sm, containerType]); + const getLegendLayerListSize = useCallback(() => { + if (containerType === CONTAINER_TYPE.APP_BAR) return 1; + + const { innerWidth } = window; + if (innerWidth < breakpoints.sm) return 1; + if (innerWidth < breakpoints.md) return 2; + if (innerWidth < breakpoints.lg) return 3; + return 4; + }, [breakpoints, containerType]); /** * Transform the list of the legends into subsets of lists. @@ -64,73 +98,69 @@ export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): * @param {TypeLegendLayer} layers array of layers. * @returns List of array of layers */ - const updateLegendLayerListByWindowSize = (layers: TypeLegendLayer[]): void => { - const arrSize = getLegendLayerListSize(); - - // create list of arrays based on size of the window. - const list = Array.from({ length: arrSize }, () => []) as Array; - layers.forEach((layer, index) => { - const idx = index % arrSize; - list[idx].push(layer); - }); - setFormattedLegendLayersList(list); - }; + const updateLegendLayerListByWindowSize = useCallback( + (layers: TypeLegendLayer[]): void => { + const arrSize = getLegendLayerListSize(); + const list = Array.from({ length: arrSize }, () => []) as Array; + + layers.forEach((layer, index) => { + list[index % arrSize].push(layer); + }); + + setFormattedLegendLayersList(list); + }, + [getLegendLayerListSize] + ); + // Handle initial layer setup useEffect(() => { - // Log - logger.logTraceUseEffect('LEGEND - visibleLayers', orderedLayerInfo.length, orderedLayerInfo); + logger.logTraceUseEffect('LEGEND - layer setup', orderedLayerInfo.length, orderedLayerInfo, layersList); setLegendLayers(layersList); updateLegendLayerListByWindowSize(layersList); + }, [orderedLayerInfo, layersList, updateLegendLayerListByWindowSize]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderedLayerInfo, layersList]); - + // Handle window resize useEffect(() => { - // Log - logger.logTraceUseEffect('LEGEND - legendLayers', legendLayers); + logger.logTraceUseEffect('LEGEND - window resize', legendLayers); // update subsets of list when window size updated. const formatLegendLayerList = (): void => { - // Log logger.logTraceCore('LEGEND - window resize event'); updateLegendLayerListByWindowSize(legendLayers); }; window.addEventListener('resize', formatLegendLayerList); + return () => window.removeEventListener('resize', formatLegendLayerList); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [legendLayers]); + }, [legendLayers, updateLegendLayerListByWindowSize]); + + // Memoize the rendered content based on whether there are legend layers + const content = useMemo(() => { + if (!legendLayers.length) { + return ( + + + {t('legend.noLayersAdded')} + + + {t('legend.noLayersAddedDescription')} + + + ); + } + + return formattedLegendLayerList.map((layers, idx) => ( + + {layers.map((layer) => ( + + ))} + + )); + }, [legendLayers, formattedLegendLayerList, fullWidth, sxClasses, t]); return ( - - {!!legendLayers.length && - formattedLegendLayerList.map((layers, idx) => { - return ( - - {layers.map((layer) => { - return ; - })} - - ); - })} - - {/* Show legend Instructions when no layer found. */} - {!legendLayers.length && ( - - - {t('legend.noLayersAdded')} - - - {t('legend.noLayersAddedDescription')} - - - )} - + {content} ); } diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts index 3a690232e96..f186826bc57 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts @@ -22,6 +22,7 @@ import { MapEventProcessor } from '@/api/event-processors/event-processor-childr import { TypeGeoviewLayerType, TypeVectorLayerStyles } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; import { esriQueryRecordsByUrlObjectIds } from '@/geo/layer/gv-layers/utils'; +import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; // #region INTERFACES & TYPES @@ -393,8 +394,11 @@ export const useSelectedLayer = (): TypeLegendLayer | undefined => { export const useIconLayerSet = (layerPath: string): string[] => { const layers = useStore(useGeoViewStore(), (state) => state.layerState.legendLayers); const layer = LegendEventProcessor.findLayerByPath(layers, layerPath); - if (layer) { + if (layer && layer.type !== CV_CONST_LAYER_TYPES.WMS) { return layer.items.map((item) => item.icon).filter((d) => d !== null) as string[]; } + if (layer && layer.type === CV_CONST_LAYER_TYPES.WMS) { + return layer.icons.map((item) => item.iconImage).filter((d) => d !== null) as string[]; + } return []; };