diff --git a/packages/geoview-core/src/core/components/app-bar/app-bar-helper.ts b/packages/geoview-core/src/core/components/app-bar/app-bar-helper.ts index cb991842162..ef7e515cc62 100644 --- a/packages/geoview-core/src/core/components/app-bar/app-bar-helper.ts +++ b/packages/geoview-core/src/core/components/app-bar/app-bar-helper.ts @@ -20,7 +20,8 @@ export const helpOpenClosePanelByIdState = ( buttonId: string, groupName: string | undefined, setterCallback: Dispatch>, - status: boolean + status: boolean, + isFocusTrapped: boolean = false ): void => { // Read the group name const theGroupName = groupName || helpFindGroupName(buttonPanelGroups, buttonId); @@ -32,7 +33,13 @@ export const helpOpenClosePanelByIdState = ( panelGroups[buttonPanelGroupName] = Object.entries(buttonPanelGroup).reduce((acc, [buttonGroupName, buttonGroup]) => { acc[buttonGroupName] = { ...buttonGroup, - ...(buttonGroup.panel && { panel: { ...buttonGroup.panel, status: buttonGroupName === buttonId ? status : false } }), + ...(buttonGroup.panel && { + panel: { + ...buttonGroup.panel, + status: buttonGroupName === buttonId ? status : false, + isFocusTrapped: buttonGroupName === buttonId ? isFocusTrapped : false, + }, + }), }; return acc; @@ -47,13 +54,14 @@ export const helpOpenPanelById = ( buttonPanelGroups: ButtonPanelGroupType, buttonId: string, groupName: string | undefined, - setterCallback: Dispatch> + setterCallback: Dispatch>, + isFocusTrapped?: boolean ): void => { // Read the group name const theGroupName = groupName || helpFindGroupName(buttonPanelGroups, buttonId); // Open the panel - helpOpenClosePanelByIdState(buttonPanelGroups, buttonId, theGroupName, setterCallback, true); + helpOpenClosePanelByIdState(buttonPanelGroups, buttonId, theGroupName, setterCallback, true, isFocusTrapped); }; export const helpClosePanelById = ( diff --git a/packages/geoview-core/src/core/components/app-bar/app-bar.tsx b/packages/geoview-core/src/core/components/app-bar/app-bar.tsx index f742578d81f..08d62746613 100644 --- a/packages/geoview-core/src/core/components/app-bar/app-bar.tsx +++ b/packages/geoview-core/src/core/components/app-bar/app-bar.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useState, useRef, useEffect, useCallback, Fragment, useMemo, ReactNode } from 'react'; +import { useState, useRef, useEffect, useCallback, Fragment, useMemo, ReactNode, KeyboardEvent } from 'react'; import { capitalize, camelCase } from 'lodash'; import { useTheme } from '@mui/material/styles'; import { @@ -51,6 +51,7 @@ import { AbstractPlugin } from '@/api/plugin/abstract-plugin'; import { CV_DEFAULT_APPBAR_CORE, CV_DEFAULT_APPBAR_TABS_ORDER } from '@/api/config/types/config-constants'; import { CONTAINER_TYPE } from '@/core/utils/constant'; import { TypeValidAppBarCoreProps } from '@/api/config/types/map-schema-types'; +import { handleEscapeKey } from '@/core/utils/utilities'; interface GroupPanelType { icon: ReactNode; @@ -92,7 +93,7 @@ export function AppBar(props: AppBarProps): JSX.Element { const activeModalId = useUIActiveFocusItem().activeElementId; const interaction = useMapInteraction(); const appBarComponents = useUIAppbarComponents(); - const { tabId, tabGroup, isOpen } = useActiveAppBarTab(); + const { tabId, tabGroup, isOpen, isFocusTrapped } = useActiveAppBarTab(); const { hideClickMarker } = useMapStoreActions(); const isMapFullScreen = useAppFullscreenActive(); @@ -151,9 +152,9 @@ export function AppBar(props: AppBarProps): JSX.Element { // Log logger.logTraceUseCallback('APP-BAR - openPanelById', buttonId); // Redirect to helper - helpOpenPanelById(buttonPanelGroups, buttonId, groupName, setButtonPanelGroups); + helpOpenPanelById(buttonPanelGroups, buttonId, groupName, setButtonPanelGroups, isFocusTrapped); }, - [buttonPanelGroups] + [buttonPanelGroups, isFocusTrapped] ); const handleButtonClicked = useCallback( @@ -439,6 +440,11 @@ export function AppBar(props: AppBarProps): JSX.Element { button={buttonPanel.button} onPanelOpened={buttonPanel.onPanelOpened} onPanelClosed={hideClickMarker} + handleKeyDown={(e: KeyboardEvent) => + handleEscapeKey(e.key, tabId, isFocusTrapped, () => { + handleGeneralCloseClicked(buttonPanel.button?.id ?? '', buttonPanel?.groupName ?? ''); + }) + } onGeneralCloseClicked={() => handleGeneralCloseClicked(buttonPanel.button?.id ?? '', buttonPanel?.groupName ?? '')} /> ); diff --git a/packages/geoview-core/src/core/components/app-bar/buttons/version.tsx b/packages/geoview-core/src/core/components/app-bar/buttons/version.tsx index fea3a57560a..9f8b2af5a4c 100644 --- a/packages/geoview-core/src/core/components/app-bar/buttons/version.tsx +++ b/packages/geoview-core/src/core/components/app-bar/buttons/version.tsx @@ -7,6 +7,7 @@ import { GeoCaIcon, IconButton, Popper } from '@/ui'; import { useGeoViewMapId } from '@/core/stores/geoview-store'; import { useMapInteraction } from '@/core/stores/store-interface-and-intial-values/map-state'; import { GitHubIcon } from '@/ui/icons'; +import { handleEscapeKey } from '@/core/utils/utilities'; // eslint-disable-next-line no-underscore-dangle declare const __VERSION__: TypeAppVersion; @@ -30,6 +31,7 @@ export default function Version(): JSX.Element { const mapId = useGeoViewMapId(); const interaction = useMapInteraction(); + const mapElem = document.getElementById(`shell-${mapId}`); const [anchorEl, setAnchorEl] = useState(null); @@ -92,7 +94,14 @@ export default function Version(): JSX.Element { - + handleEscapeKey(key, '', false, callBackFn)} + > {t('appbar.version')} diff --git a/packages/geoview-core/src/core/components/common/layout.tsx b/packages/geoview-core/src/core/components/common/layout.tsx index d4aabce1335..1fe7e821e62 100644 --- a/packages/geoview-core/src/core/components/common/layout.tsx +++ b/packages/geoview-core/src/core/components/common/layout.tsx @@ -34,7 +34,7 @@ export function Layout({ const responsiveLayoutRef = useRef(null); const theme = useTheme(); - const { setSelectedFooterLayerListItem } = useUIStoreActions(); + const { setSelectedFooterLayerListItemId } = useUIStoreActions(); /** * Handles clicks to layers in left panel. Sets selected layer. * @@ -47,9 +47,9 @@ export function Layout({ responsiveLayoutRef.current?.setIsRightPanelVisible(true); responsiveLayoutRef.current?.setRightPanelFocus(); // set the focus item when layer item clicked. - setSelectedFooterLayerListItem(`${layer.layerUniqueId}`); + setSelectedFooterLayerListItemId(`${layer.layerUniqueId}`); }, - [onLayerListClicked, setSelectedFooterLayerListItem] + [onLayerListClicked, setSelectedFooterLayerListItemId] ); /** diff --git a/packages/geoview-core/src/core/components/common/responsive-grid-layout.tsx b/packages/geoview-core/src/core/components/common/responsive-grid-layout.tsx index 3e452aa976e..152c0889c54 100644 --- a/packages/geoview-core/src/core/components/common/responsive-grid-layout.tsx +++ b/packages/geoview-core/src/core/components/common/responsive-grid-layout.tsx @@ -10,9 +10,10 @@ import FullScreenDialog from './full-screen-dialog'; import { logger } from '@/core/utils/logger'; import { ArrowBackIcon, ArrowForwardIcon, CloseIcon, QuestionMarkIcon } from '@/ui/icons'; import { useAppGuide, useAppFullscreenActive } from '@/core/stores/store-interface-and-intial-values/app-state'; -import { useUISelectedFooterLayerListItem } from '@/core/stores/store-interface-and-intial-values/ui-state'; +import { useUISelectedFooterLayerListItemId } from '@/core/stores/store-interface-and-intial-values/ui-state'; import { TypeContainerBox } from '@/core/types/global-types'; import { CONTAINER_TYPE } from '@/core/utils/constant'; +import { handleEscapeKey } from '@/core/utils/utilities'; interface ResponsiveGridLayoutProps { leftTop?: ReactNode; @@ -53,7 +54,7 @@ const ResponsiveGridLayout = forwardRef( const { t } = useTranslation(); const guide = useAppGuide(); const isMapFullScreen = useAppFullscreenActive(); - const selectedFooterLayerListItem = useUISelectedFooterLayerListItem(); + const selectedFooterLayerListItemId = useUISelectedFooterLayerListItemId(); const [isRightPanelVisible, setIsRightPanelVisible] = useState(false); const [isGuideOpen, setIsGuideOpen] = useState(false); @@ -98,22 +99,27 @@ const ResponsiveGridLayout = forwardRef( } }, [hideEnlargeBtn, isEnlarged]); + // Callback to be executed after escape key is pressed. + const handleEscapeKeyCallback = useCallback((): void => { + if (rightMainRef.current && selectedFooterLayerListItemId.length) { + rightMainRef.current.tabIndex = -1; + } + }, [selectedFooterLayerListItemId]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent): void => handleEscapeKey(event.key, selectedFooterLayerListItemId, true, handleEscapeKeyCallback), + [handleEscapeKeyCallback, selectedFooterLayerListItemId] + ); + // return back the focus to layeritem for which right panel was opened. useEffect(() => { - const handleEscapeKey = (event: KeyboardEvent): void => { - if (event.key === 'Escape' && selectedFooterLayerListItem.length && rightMainRef.current) { - rightMainRef.current.tabIndex = -1; - document.getElementById(selectedFooterLayerListItem)?.focus(); - } - }; - const rightPanel = rightMainRef.current; - rightPanel?.addEventListener('keydown', handleEscapeKey); + rightPanel?.addEventListener('keydown', handleKeyDown); return () => { - rightPanel?.removeEventListener('keydown', handleEscapeKey); + rightPanel?.removeEventListener('keydown', handleKeyDown); }; - }, [selectedFooterLayerListItem]); + }, [handleKeyDown]); /** * Handles click on the Enlarge button. diff --git a/packages/geoview-core/src/core/components/data-table/data-table.tsx b/packages/geoview-core/src/core/components/data-table/data-table.tsx index fff00e6a33c..b5f0a5032fb 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-table.tsx @@ -26,6 +26,7 @@ import { type MRT_RowVirtualizer as MRTRowVirtualizer, type MRT_ColumnFiltersState as MRTColumnFiltersState, type MRT_DensityState as MRTDensityState, + type MRT_ColumnVirtualizer as MRTColumnVirtualizer, Box, Button, IconButton, @@ -72,6 +73,7 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): // internal state const [density, setDensity] = useState('compact'); const rowVirtualizerInstanceRef = useRef(null); + const columnVirtualizerInstanceRef = useRef(null); const [sorting, setSorting] = useState([]); // get store actions and values @@ -139,8 +141,13 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): ); } + // convert string to react component. - return typeof cellValue === 'string' ? : cellValue; + return (typeof cellValue === 'string' && cellValue.length) || typeof cellValue === 'number' ? ( + + ) : ( + cellValue + ); }, [initLightBox, t] ); @@ -183,7 +190,7 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): const formattedDate = DateMgt.formatDate(date, 'YYYY-MM-DDThh:mm:ss'); return ( - {formattedDate} + {formattedDate} ); }, []); @@ -431,6 +438,7 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): enableRowVirtualization: true, muiTableContainerProps: { sx: { maxHeight: tableHeight } }, rowVirtualizerInstanceRef, + columnVirtualizerInstanceRef, rowVirtualizerOptions: { overscan: 5 }, columnVirtualizerOptions: { overscan: 2 }, localization: dataTableLocalization, diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx index d91f371b018..b8928eb4f9d 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx @@ -116,14 +116,14 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null { // inject guide tab at last position of tabs. return Object.keys({ ...tabsList, ...{ guide: {} } }).map((tab, index) => { return { - id: tab, + id: `${mapId}-${tab}${index}`, value: index, label: `${camelCase(tab)}.title`, icon: allTabs[tab]?.icon ?? '', content: allTabs[tab]?.content ?? '', } as TypeTabs; }); - }, [memoTabs, tabsList]); + }, [memoTabs, tabsList, mapId]); /** * Calculate resize values from popover values defined in store. diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index c40a0dd91a4..5933925bf23 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -12,6 +12,7 @@ import { logger } from '@/core/utils/logger'; import { CV_DEFAULT_APPBAR_CORE } from '@/api/config/types/config-constants'; import { FocusTrapContainer } from '@/core/components/common'; import { useGeoViewMapId } from '@/core/stores/geoview-store'; +import { handleEscapeKey } from '@/core/utils/utilities'; export interface GeoListItem { key: string; @@ -46,8 +47,10 @@ export function Geolocator(): JSX.Element { const { tabGroup, isOpen } = useActiveAppBarTab(); const urlRef = useRef(`${geolocatorServiceURL}&lang=${displayLanguage}`); + const geolocatorRef = useRef(); const abortControllerRef = useRef(null); const fetchTimerRef = useRef(); + const searchInputRef = useRef(); const MIN_SEARCH_LENGTH = 3; /** @@ -203,6 +206,16 @@ export function Geolocator(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue]); + useEffect(() => { + // Log + logger.logTraceUseEffect('GEOLOCATOR - mount'); + + const handleGeolocatorEscapeKey = (e: KeyboardEvent): void => { + handleEscapeKey(e.key, '', false, () => resetSearch()); + }; + geolocatorRef.current?.addEventListener('keydown', handleGeolocatorEscapeKey); + }, [mapId, resetSearch]); + useEffect(() => { return () => { // Cleanup function to abort any pending requests @@ -213,6 +226,13 @@ export function Geolocator(): JSX.Element { }; }, []); + useEffect(() => { + // Set the focus on search field when geolocator is opened. + if (isOpen && tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && searchInputRef.current) { + searchInputRef.current.querySelector('input')?.focus(); + } + }, [isOpen, tabGroup]); + /** * Effect that will track fetch call, so that after 15 seconds if no response comes back, * Error will be displayed. @@ -236,6 +256,7 @@ export function Geolocator(): JSX.Element { visibility={tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && isOpen ? 'visible' : 'hidden'} id="geolocator-search" tabIndex={tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && isOpen ? 0 : -1} + ref={geolocatorRef} > @@ -249,7 +270,13 @@ export function Geolocator(): JSX.Element { } }} > - + (false); const { setSelectedLayerPath } = useLayerStoreActions(); - const { setSelectedFooterLayerListItem } = useUIStoreActions(); + const { setSelectedFooterLayerListItemId } = useUIStoreActions(); const responsiveLayoutRef = useRef(null); @@ -34,7 +34,7 @@ export function LayersPanel({ containerType }: TypeLayersPanel): JSX.Element { responsiveLayoutRef.current?.setIsRightPanelVisible(true); responsiveLayoutRef.current?.setRightPanelFocus(); // set the focus item when layer item clicked. - setSelectedFooterLayerListItem(`${layer.layerId}`); + setSelectedFooterLayerListItemId(`${layer.layerId}`); }; const leftPanel = (): JSX.Element => { diff --git a/packages/geoview-core/src/core/components/layers/left-panel/delete-undo-button.tsx b/packages/geoview-core/src/core/components/layers/left-panel/delete-undo-button.tsx index 35f28b9b9f1..a8bdd56ef0b 100644 --- a/packages/geoview-core/src/core/components/layers/left-panel/delete-undo-button.tsx +++ b/packages/geoview-core/src/core/components/layers/left-panel/delete-undo-button.tsx @@ -56,7 +56,7 @@ export function DeleteUndoButton(props: DeleteUndoButtonProps): JSX.Element { // get store actions const { deleteLayer, setLayerDeleteInProgress, getLayerDeleteInProgress } = useLayerStoreActions(); const { getVisibilityFromOrderedLayerInfo, setOrToggleLayerVisibility } = useMapStoreActions(); - const { setSelectedFooterLayerListItem } = useUIStoreActions(); + const { setSelectedFooterLayerListItemId } = useUIStoreActions(); const handleDeleteClick = (): void => { if (getVisibilityFromOrderedLayerInfo(layer.layerPath)) setOrToggleLayerVisibility(layer.layerPath); @@ -74,14 +74,14 @@ export function DeleteUndoButton(props: DeleteUndoButtonProps): JSX.Element { if (e.key === 'Enter') { e.preventDefault(); handleDeleteClick(); - setSelectedFooterLayerListItem(layer.layerId); + setSelectedFooterLayerListItemId(layer.layerId); } }; const handleUndoDeleteKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Enter') { handleUndoClick(); - setSelectedFooterLayerListItem(''); + setSelectedFooterLayerListItemId(''); e.preventDefault(); } }; diff --git a/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx b/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx index d227147ddbd..76a4d537dd9 100644 --- a/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx +++ b/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx @@ -35,7 +35,7 @@ import { useDataTableLayerSettings, useDataTableStoreActions } from '@/core/stor import { ArrowDownwardIcon, ArrowUpIcon, TableViewIcon } from '@/ui/icons'; import { Divider } from '@/ui/divider/divider'; import { useGeoViewMapId } from '@/core/stores/geoview-store'; -import { useUISelectedFooterLayerListItem } from '@/core/stores/store-interface-and-intial-values/ui-state'; +import { useUISelectedFooterLayerListItemId } from '@/core/stores/store-interface-and-intial-values/ui-state'; interface SingleLayerProps { layer: TypeLegendLayer; @@ -76,7 +76,7 @@ export function SingleLayer({ const displayState = useLayerDisplayState(); const datatableSettings = useDataTableLayerSettings(); const selectedLayerSortingArrowId = useSelectedLayerSortingArrowId(); - const selectedFooterLayerListItem = useUISelectedFooterLayerListItem(); + const selectedFooterLayerListItemId = useUISelectedFooterLayerListItemId(); const legendLayers = useLayerLegendLayers(); useDataTableStoreActions(); @@ -340,7 +340,7 @@ export function SingleLayer({ useEffect(() => { // set the focus to first layer, after layer has been deleted. - if (displayState === 'remove' && selectedFooterLayerListItem.length) { + if (displayState === 'remove' && selectedFooterLayerListItemId.length) { const firstLayer = document.getElementById('layers-left-panel'); if (firstLayer?.getElementsByTagName('li')) { const listItems = firstLayer?.getElementsByTagName('li'); diff --git a/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx b/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx index 25c9a636109..26c9c5283aa 100644 --- a/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx @@ -8,6 +8,7 @@ import { useGeoViewMapId } from '@/core/stores/geoview-store'; import { TypeButtonPanel } from '@/ui/panel/panel-types'; import { logger } from '@/core/utils/logger'; import { HtmlToReact } from '@/core/containers/html-to-react'; +import { handleEscapeKey } from '@/core/utils/utilities'; interface NavbarPanelButtonType { buttonPanel: TypeButtonPanel; @@ -72,6 +73,7 @@ export default function NavbarPanelButton({ buttonPanel }: NavbarPanelButtonType onClose={handleClickAway} container={shellContainer} sx={{ marginRight: '5px !important' }} + handleKeyDown={(key, callBackFn) => handleEscapeKey(key, '', false, callBackFn)} > {(buttonPanel.panel?.title as string) ?? ''} diff --git a/packages/geoview-core/src/core/components/notifications/notifications.tsx b/packages/geoview-core/src/core/components/notifications/notifications.tsx index c5e61a86fba..28a80ff8a10 100644 --- a/packages/geoview-core/src/core/components/notifications/notifications.tsx +++ b/packages/geoview-core/src/core/components/notifications/notifications.tsx @@ -26,6 +26,7 @@ import { useGeoViewMapId } from '@/core/stores/geoview-store'; import { logger } from '@/core/utils/logger'; import { useMapInteraction } from '@/core/stores/store-interface-and-intial-values/map-state'; import { useShake } from '@/core/utils/useSpringAnimations'; +import { handleEscapeKey } from '@/core/utils/utilities'; export type NotificationDetailsType = { key: string; @@ -168,7 +169,14 @@ export default function Notifications(): JSX.Element { - + handleEscapeKey(key, '', false, callBackFn)} + > diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/ui-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/ui-state.ts index 5760d1571d8..8cea41b0ebb 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/ui-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/ui-state.ts @@ -32,7 +32,7 @@ export interface IUIState { footerPanelResizeValue: number; footerPanelResizeValues: number[]; footerBarIsCollapsed: boolean; - selectedFooterLayerListItem: string; + selectedFooterLayerListItemId: string; setDefaultConfigValues: (geoviewConfig: TypeMapFeaturesConfig) => void; actions: { @@ -46,7 +46,7 @@ export interface IUIState { setFooterPanelResizeValue: (value: number) => void; setMapInfoExpanded: (expanded: boolean) => void; setFooterBarIsCollapsed: (collapsed: boolean) => void; - setSelectedFooterLayerListItem: (layerListItem: string) => void; + setSelectedFooterLayerListItemId: (layerListItemId: string) => void; }; setterActions: { @@ -59,7 +59,7 @@ export interface IUIState { setHiddenTabs: (hiddenTabs: string[]) => void; setMapInfoExpanded: (expanded: boolean) => void; setFooterBarIsCollapsed: (collapsed: boolean) => void; - setSelectedFooterLayerListItem: (layerListItem: string) => void; + setSelectedFooterLayerListItemId: (layerListItemId: string) => void; }; } @@ -85,7 +85,7 @@ export function initializeUIState(set: TypeSetStore, get: TypeGetStore): IUIStat footerPanelResizeValue: 35, footerPanelResizeValues: [35, 50, 100], footerBarIsCollapsed: false, - selectedFooterLayerListItem: '', + selectedFooterLayerListItemId: '', // initialize default stores section from config information when store receive configuration file setDefaultConfigValues: (geoviewConfig: TypeMapFeaturesConfig) => { @@ -152,9 +152,9 @@ export function initializeUIState(set: TypeSetStore, get: TypeGetStore): IUIStat // Redirect to setter get().uiState.setterActions.setActiveAppBarTab(tabId, tabGroup, isOpen, isFocusTrapped); }, - setSelectedFooterLayerListItem: (layerListItem: string) => { + setSelectedFooterLayerListItemId: (layerListItemId: string) => { // Redirect to setter - get().uiState.setterActions.setSelectedFooterLayerListItem(layerListItem); + get().uiState.setterActions.setSelectedFooterLayerListItemId(layerListItemId); }, }, @@ -237,11 +237,11 @@ export function initializeUIState(set: TypeSetStore, get: TypeGetStore): IUIStat }, }); }, - setSelectedFooterLayerListItem: (layerListItem: string) => { + setSelectedFooterLayerListItemId: (layerListItemId: string) => { set({ uiState: { ...get().uiState, - selectedFooterLayerListItem: layerListItem, + selectedFooterLayerListItemId: layerListItemId, }, }); }, @@ -275,7 +275,7 @@ export const useUIHiddenTabs = (): string[] => useStore(useGeoViewStore(), (stat export const useUIMapInfoExpanded = (): boolean => useStore(useGeoViewStore(), (state) => state.uiState.mapInfoExpanded); export const useUINavbarComponents = (): TypeNavBarProps => useStore(useGeoViewStore(), (state) => state.uiState.navBarComponents); export const useUIFooterBarIsCollapsed = (): boolean => useStore(useGeoViewStore(), (state) => state.uiState.footerBarIsCollapsed); -export const useUISelectedFooterLayerListItem = (): string => - useStore(useGeoViewStore(), (state) => state.uiState.selectedFooterLayerListItem); +export const useUISelectedFooterLayerListItemId = (): string => + useStore(useGeoViewStore(), (state) => state.uiState.selectedFooterLayerListItemId); export const useUIStoreActions = (): UIActions => useStore(useGeoViewStore(), (state) => state.uiState.actions); diff --git a/packages/geoview-core/src/core/utils/utilities.ts b/packages/geoview-core/src/core/utils/utilities.ts index 7dce6bca74e..d35a28a835b 100644 --- a/packages/geoview-core/src/core/utils/utilities.ts +++ b/packages/geoview-core/src/core/utils/utilities.ts @@ -521,3 +521,21 @@ export async function createGuideObject( return undefined; } } + +/** + * Callback function which is fired when keyboard key is pressed. + * @param {string} key The keyboard key pressed by user. + * @param {string} callbackId The Id of element which init the focus trap. + * @param {boolean} isFocusTrapped Component is focus trapped enabled. + * @param {Function} cb The callback function to be fired, if needed. + */ +export function handleEscapeKey(key: string, callbackId: string, isFocusTrapped?: boolean, cb?: () => void): void { + if (key === 'Escape') { + if (isFocusTrapped && callbackId) { + setTimeout(() => { + document.getElementById(callbackId ?? '')?.focus(); + }, 100); + } + cb?.(); + } +} diff --git a/packages/geoview-core/src/ui/panel/panel.tsx b/packages/geoview-core/src/ui/panel/panel.tsx index 9f910cb45e6..2c498280be3 100644 --- a/packages/geoview-core/src/ui/panel/panel.tsx +++ b/packages/geoview-core/src/ui/panel/panel.tsx @@ -30,6 +30,8 @@ export type TypePanelAppProps = { onPanelOpened?: () => void; // Callback when the panel has been closed onPanelClosed?: () => void; + // Callback when the panel has been closed by escape key + handleKeyDown?: (event: KeyboardEvent) => void; }; /** @@ -39,7 +41,7 @@ export type TypePanelAppProps = { * @returns {JSX.Element} the created Panel element */ export function Panel(props: TypePanelAppProps): JSX.Element { - const { panel, button, onPanelOpened, onPanelClosed, onGeneralCloseClicked, ...rest } = props; + const { panel, button, onPanelOpened, onPanelClosed, onGeneralCloseClicked, handleKeyDown, ...rest } = props; const { status: open = false, isFocusTrapped = false, panelStyles, panelGroupName } = panel; const { t } = useTranslation(); @@ -130,11 +132,7 @@ export function Panel(props: TypePanelAppProps): JSX.Element { ...(panelStyles?.panelCard && { ...panelStyles.panelCard }), }} ref={panelRef as React.MutableRefObject} - onKeyDown={(e: KeyboardEvent) => { - if (e.key === 'Escape') { - onGeneralCloseClicked?.(); - } - }} + onKeyDown={(e: KeyboardEvent) => handleKeyDown?.(e)} {...{ 'data-id': button.id }} {...rest} > diff --git a/packages/geoview-core/src/ui/popper/popper.tsx b/packages/geoview-core/src/ui/popper/popper.tsx index 7239536f3ae..fa99f6ee710 100644 --- a/packages/geoview-core/src/ui/popper/popper.tsx +++ b/packages/geoview-core/src/ui/popper/popper.tsx @@ -5,6 +5,7 @@ import { animated, useSpring, easings } from '@react-spring/web'; interface EnhancedPopperProps extends PopperProps { // eslint-disable-next-line react/require-default-props onClose?: () => void; + handleKeyDown?: (key: string, callbackFn: () => void) => void; } /** @@ -14,23 +15,18 @@ interface EnhancedPopperProps extends PopperProps { * @returns {JSX.Element} returns popover component */ /* eslint-disable-next-line react/function-component-definition */ -export const Popper: React.FC = ({ open, onClose, ...restProps }) => { +export const Popper: React.FC = ({ open, onClose, handleKeyDown, ...restProps }) => { const popperRef = useRef(null); useEffect(() => { - const handleEscapeKey = (event: KeyboardEvent): void => { - if (event.key === 'Escape' && open && onClose) { - // Close the Popper when 'Escape' key is pressed - onClose(); - } + const onKeyDown = (event: KeyboardEvent): void => { + handleKeyDown?.(event.key, () => open && onClose?.()); }; - - document.addEventListener('keydown', handleEscapeKey); - + document.addEventListener('keydown', onKeyDown); return () => { - document.removeEventListener('keydown', handleEscapeKey); + document.removeEventListener('keydown', onKeyDown); }; - }, [open, onClose]); + }, [open, onClose, handleKeyDown]); const springProps = useSpring({ config: { duration: 250, easing: easings.easeInExpo }, diff --git a/packages/geoview-core/src/ui/tabs/tab-panel.tsx b/packages/geoview-core/src/ui/tabs/tab-panel.tsx index 35c9a4f12cd..0ee13699e80 100644 --- a/packages/geoview-core/src/ui/tabs/tab-panel.tsx +++ b/packages/geoview-core/src/ui/tabs/tab-panel.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/require-default-props */ -import { ReactNode } from 'react'; +import { forwardRef, ReactNode } from 'react'; import { Box } from '@/ui'; import { FocusTrapContainer } from '@/core/components/common'; import { TypeContainerBox } from '@/core/types/global-types'; @@ -24,14 +24,16 @@ export interface TypeTabPanelProps { * @param {TypeTabPanelProps} props properties for the tab panel * @returns {JSX.Element} returns the tab panel */ -export function TabPanel(props: TypeTabPanelProps): JSX.Element { +export const TabPanel = forwardRef((props: TypeTabPanelProps, ref) => { const { children, value, index, id, containerType, tabId, ...other } = props; return ( - ); }