Skip to content

Commit

Permalink
Fix other component
Browse files Browse the repository at this point in the history
  • Loading branch information
jolevesq committed Dec 6, 2024
1 parent d0a12cb commit ba6da55
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 284 deletions.
Original file line number Diff line number Diff line change
@@ -1,40 +1,70 @@
import { ReactNode, useEffect } from 'react';
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FocusTrap, Box, Button } from '@/ui';
import { logger } from '@/core/utils/logger';
import { useUIActiveFocusItem, useUIActiveTrapGeoView, useUIStoreActions } from '@/core/stores/store-interface-and-intial-values/ui-state';
import { TypeContainerBox } from '@/core/types/global-types';
import { CONTAINER_TYPE } from '@/core/utils/constant';

interface FocusTrapContainerType {
interface FocusTrapContainerProps {
children: ReactNode;
id: string;
containerType?: TypeContainerBox;
open?: boolean;
}

// Constants outside component to prevent recreating every render
const EXIT_BUTTON_STYLES = {
width: '95%',
margin: '10px auto',
} as const;

const FOCUS_DELAY = 0;

/**
* Focus trap container which will trap the focus when navigating through keyboard tab.
* @param {TypeChildren} children dom elements wrapped in Focus trap.
* @param {boolean} open enable and disabling of focus trap.
* @returns {JSX.Element}
*/
export function FocusTrapContainer({ children, open = false, id, containerType }: FocusTrapContainerType): JSX.Element {
// Log
// Memoizes entire component, preventing re-renders if props haven't changed
export const FocusTrapContainer = memo(function FocusTrapContainer({
children,
open = false,
id,
containerType,
}: FocusTrapContainerProps): JSX.Element {
logger.logTraceRender('component/common/FocusTrapContainer', containerType);

// Hooks
const { t } = useTranslation<string>();

// get values from the store
// Store
const { disableFocusTrap } = useUIStoreActions();
const activeTrapGeoView = useUIActiveTrapGeoView();
const focusItem = useUIActiveFocusItem();

const handleClose = (): void => {
// Callbacks
const handleClose = useCallback((): void => {
disableFocusTrap(id);
};
}, [disableFocusTrap, id]);

// Memoize
const isActive = useMemo(() => id === focusItem.activeElementId || open, [id, focusItem.activeElementId, open]);

const showExitButton = useMemo(
() => containerType === CONTAINER_TYPE.FOOTER_BAR && activeTrapGeoView,
[containerType, activeTrapGeoView]
);

const exitButtonStyles = useMemo(
() => ({
...EXIT_BUTTON_STYLES,
display: activeTrapGeoView ? 'block' : 'none',
}),
[activeTrapGeoView]
);

// #region REACT HOOKS
// if keyboard navigation if turned off, remove trap settings
useEffect(() => {
// Log
Expand All @@ -49,27 +79,20 @@ export function FocusTrapContainer({ children, open = false, id, containerType }
logger.logTraceUseEffect('FOCUS-TRAP-ELEMENT - focusItem', focusItem);

if (id === focusItem.activeElementId) {
setTimeout(() => document.getElementById(`${id}-exit-btn`)?.focus(), 0);
setTimeout(() => document.getElementById(`${id}-exit-btn`)?.focus(), FOCUS_DELAY);
}
}, [focusItem, id]);
// #endregion

return (
<FocusTrap open={id === focusItem.activeElementId || open} disableAutoFocus>
<Box tabIndex={id === focusItem.activeElementId || open ? 0 : -1} sx={{ height: '100%' }}>
{containerType === CONTAINER_TYPE.FOOTER_BAR && activeTrapGeoView && (
<Button
id={`${id}-exit-btn`}
type="text"
autoFocus
onClick={handleClose}
sx={{ display: activeTrapGeoView ? 'block' : 'none', width: '95%', margin: '10px auto' }}
>
<FocusTrap open={isActive} disableAutoFocus>
<Box tabIndex={isActive || open ? 0 : -1} sx={{ height: '100%' }}>
{showExitButton && (
<Button id={`${id}-exit-btn`} type="text" autoFocus onClick={handleClose} sx={exitButtonStyles}>
{t('general.exit')}
</Button>
)}
{children}
</Box>
</FocusTrap>
);
}
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo, ReactNode } from 'react';
import { DialogProps } from '@mui/material';
import { ReactNode } from 'react';
import { CloseIcon, Dialog, DialogContent, IconButton } from '@/ui';

interface FullScreenDialogProps extends DialogProps {
Expand All @@ -8,17 +8,32 @@ interface FullScreenDialogProps extends DialogProps {
children: ReactNode;
}

function FullScreenDialog({ open, onClose, children }: FullScreenDialogProps): JSX.Element {
// Constant styles to prevent recreation on each render
const DIALOG_CONTENT_STYLES = {
display: 'flex',
flexDirection: 'column',
alignItems: 'end',
} as const;

const CLOSE_BUTTON_STYLES = {
marginBottom: '1.5rem',
} as const;

// Memoizes entire component, preventing re-renders if props haven't changed
export const FullScreenDialog = memo(function FullScreenDialog({
open,
onClose,
children,
...dialogProps
}: FullScreenDialogProps): JSX.Element {
return (
<Dialog fullScreen maxWidth="xl" open={open} onClose={onClose} disablePortal>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'end' }}>
<IconButton onClick={onClose} color="primary" className="buttonFilledOutline" sx={{ marginBottom: '1.5rem' }}>
<Dialog fullScreen maxWidth="xl" open={open} onClose={onClose} disablePortal {...dialogProps}>
<DialogContent sx={DIALOG_CONTENT_STYLES}>
<IconButton onClick={onClose} color="primary" className="buttonFilledOutline" sx={CLOSE_BUTTON_STYLES}>
<CloseIcon />
</IconButton>
{children}
</DialogContent>
</Dialog>
);
}

export default FullScreenDialog;
});
178 changes: 107 additions & 71 deletions packages/geoview-core/src/core/components/common/layer-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { memo, useCallback, useMemo } from 'react';
import { useTheme } from '@mui/material/styles';
import { Box, CircularProgressBase, ErrorIcon, GroupWorkOutlinedIcon, IconButton, BrowserNotSupportedIcon } from '@/ui';

import { TypeLegendLayer } from '@/core/components/layers/types';
import { getSxClasses } from './layer-icon-style';
import { useIconLayerSet } from '@/core/stores/store-interface-and-intial-values/layer-state';
Expand All @@ -11,95 +13,129 @@ export interface TypeIconStackProps {
onStackIconClick?: (e: React.KeyboardEvent<HTMLElement>) => void;
}

interface LayerIconProps {
layer: TypeLegendLayer | LayerListEntry;
}

// Constants outside component to prevent recreating every render
const LOADING_BOX_STYLES = {
padding: '5px',
marginRight: '10px',
} as const;

const ICON_BUTTON_BASE_PROPS = {
color: 'primary' as const,
size: 'small' as const,
tabIndex: -1,
'aria-hidden': true,
};

/**
* Icon Stack to represent layer icons
*
* @param {string} layerPath
* @returns {JSX.Element} the icon stack item
*/
function IconStack({ layerPath, onIconClick, onStackIconClick }: TypeIconStackProps): JSX.Element | null {
// Memoizes entire component, preventing re-renders if props haven't changed
const IconStack = memo(function IconStack({ layerPath, onIconClick, onStackIconClick }: TypeIconStackProps): JSX.Element | null {
// Hooks
const theme = useTheme();
const sxClasses = getSxClasses(theme);

// Store
const iconData = useIconLayerSet(layerPath);

const iconImage: string = iconData?.length > 0 ? iconData[0] : '';
const iconImageStacked: string = iconData?.length > 1 ? iconData[1] : '';
const numOfIcons: number | undefined = iconData?.length;

const iconStackContent = (): JSX.Element | null => {
if (numOfIcons === 1) {
return (
<IconButton
tabIndex={-1}
sx={sxClasses.iconPreview}
color="primary"
size="small"
onClick={iconImage === 'no data' ? undefined : onIconClick}
aria-hidden="true"
>
{iconImage === 'no data' ? (
<BrowserNotSupportedIcon />
) : (
<Box sx={sxClasses.legendIcon}>
<Box component="img" alt="icon" src={iconImage} sx={sxClasses.maxIconImg} />
</Box>
)}
const { iconImage, iconImageStacked, numOfIcons } = useMemo(
() => ({
iconImage: iconData?.length > 0 ? iconData[0] : '',
iconImageStacked: iconData?.length > 1 ? iconData[1] : '',
numOfIcons: iconData?.length,
}),
[iconData]
);

const renderSingleIcon = useCallback(
(): JSX.Element => (
<IconButton {...ICON_BUTTON_BASE_PROPS} sx={sxClasses.iconPreview} onClick={iconImage === 'no data' ? undefined : onIconClick}>
{iconImage === 'no data' ? (
<BrowserNotSupportedIcon />
) : (
<Box sx={sxClasses.legendIcon}>
<Box component="img" alt="icon" src={iconImage} sx={sxClasses.maxIconImg} />
</Box>
)}
</IconButton>
),
[iconImage, onIconClick, sxClasses.iconPreview, sxClasses.legendIcon, sxClasses.maxIconImg]
);

const renderStackedIcons = useCallback(
(): JSX.Element => (
<Box tabIndex={-1} onClick={onIconClick} sx={sxClasses.stackIconsBox} onKeyDown={onStackIconClick} aria-hidden="true">
<IconButton {...ICON_BUTTON_BASE_PROPS} sx={sxClasses.iconPreviewStacked}>
<Box sx={sxClasses.legendIconTransparent}>
{iconImageStacked && <Box component="img" alt="icon" src={iconImageStacked} sx={sxClasses.maxIconImg} />}
</Box>
</IconButton>
);
}
if (numOfIcons && numOfIcons > 0) {
return (
<Box tabIndex={-1} onClick={onIconClick} sx={sxClasses.stackIconsBox} onKeyDown={(e) => onStackIconClick?.(e)} aria-hidden="true">
<IconButton sx={sxClasses.iconPreviewStacked} color="primary" size="small" tabIndex={-1} aria-hidden="true">
<Box sx={sxClasses.legendIconTransparent}>
{iconImageStacked && <Box component="img" alt="icon" src={iconImageStacked} sx={sxClasses.maxIconImg} />}
</Box>
</IconButton>
<IconButton sx={sxClasses.iconPreviewHoverable} color="primary" size="small" tabIndex={-1} aria-hidden="true">
<Box sx={sxClasses.legendIcon}>{iconImage && <Box component="img" alt="icon" src={iconImage} sx={sxClasses.maxIconImg} />}</Box>
</IconButton>
</Box>
);
}
if (layerPath !== '' && iconData.length === 0 && layerPath.charAt(0) !== '!') {
return (
<Box tabIndex={-1} onClick={onIconClick} sx={sxClasses.stackIconsBox} onKeyDown={(e) => onStackIconClick?.(e)} aria-hidden="true">
<IconButton sx={sxClasses.iconPreviewStacked} color="primary" size="small" tabIndex={-1} aria-hidden="true">
<Box sx={sxClasses.legendIconTransparent}>
<BrowserNotSupportedIcon />
</Box>
</IconButton>
</Box>
);
}
return null;
};

return iconStackContent();
}
<IconButton {...ICON_BUTTON_BASE_PROPS} sx={sxClasses.iconPreviewHoverable}>
<Box sx={sxClasses.legendIcon}>{iconImage && <Box component="img" alt="icon" src={iconImage} sx={sxClasses.maxIconImg} />}</Box>
</IconButton>
</Box>
),
[
iconImage,
iconImageStacked,
onIconClick,
onStackIconClick,
sxClasses.iconPreviewHoverable,
sxClasses.iconPreviewStacked,
sxClasses.legendIcon,
sxClasses.legendIconTransparent,
sxClasses.maxIconImg,
sxClasses.stackIconsBox,
]
);

interface LayerIconProps {
layer: TypeLegendLayer | LayerListEntry;
}
const renderNoDataIcon = useCallback(
(): JSX.Element => (
<Box tabIndex={-1} onClick={onIconClick} sx={sxClasses.stackIconsBox} onKeyDown={onStackIconClick} aria-hidden="true">
<IconButton {...ICON_BUTTON_BASE_PROPS} sx={sxClasses.iconPreviewStacked}>
<Box sx={sxClasses.legendIconTransparent}>
<BrowserNotSupportedIcon />
</Box>
</IconButton>
</Box>
),
[onIconClick, onStackIconClick, sxClasses.iconPreviewStacked, sxClasses.legendIconTransparent, sxClasses.stackIconsBox]
);

export function LayerIcon({ layer }: LayerIconProps): JSX.Element {
if (layer.layerStatus === 'error' || ('queryStatus' in layer && layer.queryStatus === 'error')) {
return <ErrorIcon color="error" />;
if (numOfIcons === 1) return renderSingleIcon();
if (numOfIcons && numOfIcons > 0) return renderStackedIcons();
if (layerPath !== '' && iconData.length === 0 && layerPath.charAt(0) !== '!') {
return renderNoDataIcon();
}
if (
layer.layerStatus === 'processing' ||
layer.layerStatus === 'loading' ||
('queryStatus' in layer && layer.queryStatus === 'processing')
) {
return null;
});

export const LayerIcon = memo(function LayerIcon({ layer }: LayerIconProps): JSX.Element {
const isError = layer.layerStatus === 'error' || ('queryStatus' in layer && layer.queryStatus === 'error');

const isLoading =
layer.layerStatus === 'processing' || layer.layerStatus === 'loading' || ('queryStatus' in layer && layer.queryStatus === 'processing');

const hasChildren = 'children' in layer && layer?.children.length;

if (isError) return <ErrorIcon color="error" />;

if (isLoading) {
return (
<Box sx={{ padding: '5px', marginRight: '10px' }}>
<Box sx={LOADING_BOX_STYLES}>
<CircularProgressBase size={20} />
</Box>
);
}
if ('children' in layer && layer?.children.length) {
return <GroupWorkOutlinedIcon color="primary" />;
}

if (hasChildren) return <GroupWorkOutlinedIcon color="primary" />;

return <IconStack layerPath={layer.layerPath} />;
}
});
Loading

0 comments on commit ba6da55

Please sign in to comment.