diff --git a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx index 973785e63078e5201eac60db07a5f1ecda9bb398..5e92826059af1ed40834e13d57ab5a60f6ac9ca5 100644 --- a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx +++ b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx @@ -14,6 +14,7 @@ import { ClickCoordinates } from '@/services/pluginsManager/pluginContextMenu/pl import { currentModelSelector } from '@/redux/models/models.selectors'; import { mapDataLastPositionSelector } from '@/redux/map/map.selectors'; import { DEFAULT_ZOOM } from '@/constants/map'; +import { OutsideClickWrapper } from '@/shared/OutsideClickWrapper'; export const ContextMenu = (): React.ReactNode => { const pluginContextMenu = PluginsContextMenu.menuItems; @@ -29,15 +30,19 @@ export const ContextMenu = (): React.ReactNode => { return isUnitProtIdAvailable() ? unitProtId : 'no UnitProt ID available'; }; + const closeContextMenuFunction = (): void => { + dispatch(closeContextMenu()); + }; + const handleOpenMolArtClick = (): void => { if (isUnitProtIdAvailable()) { - dispatch(closeContextMenu()); + closeContextMenuFunction(); dispatch(openMolArtModalById(unitProtId)); } }; const handleAddCommentClick = (): void => { - dispatch(closeContextMenu()); + closeContextMenuFunction(); dispatch(openAddCommentModal()); }; @@ -47,7 +52,7 @@ export const ContextMenu = (): React.ReactNode => { callback: (coordinates: ClickCoordinates, element: BioEntity | NewReaction | undefined) => void, ) => { return () => { - dispatch(closeContextMenu()); + closeContextMenuFunction(); return callback( { modelId, @@ -61,55 +66,57 @@ export const ContextMenu = (): React.ReactNode => { }; return ( - <div - className={twMerge( - 'absolute z-10 rounded-lg border border-[#DBD9D9] bg-white p-4', - isOpen ? '' : 'hidden', - )} - style={{ - left: `${coordinates[FIRST_ARRAY_ELEMENT]}px`, - top: `${coordinates[SECOND_ARRAY_ELEMENT]}px`, - }} - data-testid="context-modal" - > - <button + <OutsideClickWrapper onOutsideClick={closeContextMenuFunction}> + <div className={twMerge( - 'w-full cursor-pointer text-left text-xs font-normal', - !isUnitProtIdAvailable() ? 'cursor-not-allowed text-greyscale-700' : '', + 'absolute z-10 rounded-lg border border-[#DBD9D9] bg-white p-4', + isOpen ? '' : 'hidden', )} - onClick={handleOpenMolArtClick} - type="button" - data-testid="open-molart" - > - Open MolArt ({getUnitProtId()}) - </button> - <hr /> - <button - className={twMerge('w-full cursor-pointer text-left text-xs font-normal')} - onClick={handleAddCommentClick} - type="button" - data-testid="add-comment" + style={{ + left: `${coordinates[FIRST_ARRAY_ELEMENT]}px`, + top: `${coordinates[SECOND_ARRAY_ELEMENT]}px`, + }} + data-testid="context-modal" > - Add comment - </button> - {pluginContextMenu.length && <hr />} - - {pluginContextMenu.map(contextMenuEntry => ( <button - key={contextMenuEntry.id} - id={contextMenuEntry.id} className={twMerge( - 'cursor-pointer text-xs font-normal', - contextMenuEntry.style, - !contextMenuEntry.enabled ? 'cursor-not-allowed text-greyscale-700' : '', + 'w-full cursor-pointer text-left text-xs font-normal', + !isUnitProtIdAvailable() ? 'cursor-not-allowed text-greyscale-700' : '', )} - onClick={handleCallback(contextMenuEntry.callback)} + onClick={handleOpenMolArtClick} type="button" - data-testid={contextMenuEntry.id} + data-testid="open-molart" > - {contextMenuEntry.name} + Open MolArt ({getUnitProtId()}) </button> - ))} - </div> + <hr /> + <button + className={twMerge('w-full cursor-pointer text-left text-xs font-normal')} + onClick={handleAddCommentClick} + type="button" + data-testid="add-comment" + > + Add comment + </button> + {pluginContextMenu.length && <hr />} + + {pluginContextMenu.map(contextMenuEntry => ( + <button + key={contextMenuEntry.id} + id={contextMenuEntry.id} + className={twMerge( + 'cursor-pointer text-xs font-normal', + contextMenuEntry.style, + !contextMenuEntry.enabled ? 'cursor-not-allowed text-greyscale-700' : '', + )} + onClick={handleCallback(contextMenuEntry.callback)} + type="button" + data-testid={contextMenuEntry.id} + > + {contextMenuEntry.name} + </button> + ))} + </div> + </OutsideClickWrapper> ); }; diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts index 5bc52a7fd486077744fe048effab865d59c6639e..614d3dcf06a521ffb3fe170362e760b59905086b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts @@ -4,6 +4,11 @@ export const VECTOR_MAP_LAYER_TYPE = 'vectorMapLayer'; export const COMPLEX_SBO_TERMS = ['SBO:0000253', 'SBO:0000297', 'SBO:0000289']; +export const TEXT_CUTOFF_SCALE = 0.34; +export const OUTLINE_CUTOFF_SCALE = 0.18; +export const COMPLEX_CONTENTS_CUTOFF_SCALE = 0.215; +export const REACTION_ELEMENT_CUTOFF_SCALE = 0.125; + export const WHITE_COLOR: Color = { alpha: 255, rgb: 16777215, @@ -20,27 +25,27 @@ export const TRANSPARENT_COLOR: Color = { }; export const REACTION_ELEMENT_TYPES = { - OPERATOR: 'operator', - SQUARE: 'square', - LINE: 'line', - ARROW: 'arrow', + OPERATOR: 'OPERATOR', + SQUARE: 'SQUARE', + LINE: 'LINE', + ARROW: 'ARROW', }; export const MAP_ELEMENT_TYPES = { - TEXT: 'text', - MODIFICATION: 'modification', - ACTIVITY_BORDER: 'activityBorder', - ENTITY: 'entity', - OVERLAY: 'overlay', - COMPARTMENT: 'compartment', + TEXT: 'TEXT', + MODIFICATION: 'MODIFICATION', + ACTIVITY_BORDER: 'ACTIVITY_BORDER', + ENTITY: 'ENTITY', + OVERLAY: 'OVERLAY', + COMPARTMENT: 'COMPARTMENT', }; export const LAYER_ELEMENT_TYPES = { - TEXT: 'text', - OVAL: 'oval', - RECT: 'rect', - LINE: 'line', - ARROW: 'arrow', + TEXT: 'TEXT', + OVAL: 'OVAL', + RECT: 'RECT', + LINE: 'LINE', + ARROW: 'ARROW', }; export const COMPARTMENT_SQUARE_POINTS: Array<ShapeRelAbs | ShapeRelAbsBezierPoint> = [ diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts index 3a100b052ddf2783c7792c7033589efc069606f5..553893c422c52f5565777cfc798ec7b4d5d8c0cf 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/BaseMultiPolygon.ts @@ -14,8 +14,11 @@ import { rgbToHex } from '@/components/Map/MapViewer/MapViewerVector/utils/shape import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import { Color } from '@/types/models'; import { + COMPLEX_CONTENTS_CUTOFF_SCALE, COMPLEX_SBO_TERMS, MAP_ELEMENT_TYPES, + OUTLINE_CUTOFF_SCALE, + TEXT_CUTOFF_SCALE, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import VectorSource from 'ol/source/Vector'; import MapBackgroundsEnum from '@/redux/map/map.enums'; @@ -259,12 +262,16 @@ export default abstract class BaseMultiPolygon { let cover = false; let largestExtent: Extent | null; - if (this.complexId && !COMPLEX_SBO_TERMS.includes(this.sboTerm) && scale < 0.215) { + if ( + this.complexId && + !COMPLEX_SBO_TERMS.includes(this.sboTerm) && + scale < COMPLEX_CONTENTS_CUTOFF_SCALE + ) { return []; } let hide = false; - if (this.mapBackgroundType === MapBackgroundsEnum.SEMANTIC && scale < 0.34) { + if (this.mapBackgroundType === MapBackgroundsEnum.SEMANTIC && scale < TEXT_CUTOFF_SCALE) { const semanticViewData = handleSemanticView({ vectorSource: this.vectorSource, feature, @@ -311,18 +318,21 @@ export default abstract class BaseMultiPolygon { return; } - if ([MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && scale < 0.34) { + if ( + [MAP_ELEMENT_TYPES.MODIFICATION, MAP_ELEMENT_TYPES.TEXT].includes(type) && + scale < TEXT_CUTOFF_SCALE + ) { return; } const textStyle = style.getText(); - if (type === 'text' && textStyle) { + if (type === MAP_ELEMENT_TYPES.TEXT && textStyle) { textStyle.setScale(scale); } if (strokeStyle) { if ( !this.overlaysVisible && - scale < 0.18 && + scale < OUTLINE_CUTOFF_SCALE && !COMPLEX_SBO_TERMS.includes(this.sboTerm) && this.type !== MAP_ELEMENT_TYPES.COMPARTMENT ) { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts index 8906e75dab9219c5aa4b370254c564f5dc3eedc5..987fd2cee18029603d330e2f7512ac7069554ba5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Compartment.ts @@ -97,7 +97,7 @@ export default abstract class Compartment extends BaseMultiPolygon { mapSize, }: CompartmentProps) { super({ - type: 'COMPARTMENT', + type: MAP_ELEMENT_TYPES.COMPARTMENT, id, complexId, compartmentId, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts index e758a681ec3addb34553e58e79ed8085b72aa6e2..2850176f0d50cf9617d7fa0ea50bd33aa6732b57 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway.ts @@ -87,7 +87,7 @@ export default class CompartmentPathway extends BaseMultiPolygon { mapSize, }: CompartmentPathwayProps) { super({ - type: 'COMPARTMENT', + type: MAP_ELEMENT_TYPES.COMPARTMENT, id, complexId, compartmentId, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts index 224921672e47f9a81753cd1a80ee4e679003f237..781903923472b930d15b88020345424fadebd913 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/handleSemanticView.ts @@ -4,7 +4,10 @@ import findLargestExtent from '@/components/Map/MapViewer/MapViewerVector/utils/ import Feature from 'ol/Feature'; import VectorSource from 'ol/source/Vector'; import { Extent } from 'ol/extent'; -import { COMPLEX_SBO_TERMS } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; +import { + COMPLEX_SBO_TERMS, + MAP_ELEMENT_TYPES, +} from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import isFeatureInCompartment from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/isFeatureInCompartment'; export default function handleSemanticView({ @@ -34,7 +37,7 @@ export default function handleSemanticView({ let largestExtent: Extent | null = null; if ( getMapExtent instanceof Function && - (type === 'COMPARTMENT' || COMPLEX_SBO_TERMS.includes(sboTerm)) + (type === MAP_ELEMENT_TYPES.COMPARTMENT || COMPLEX_SBO_TERMS.includes(sboTerm)) ) { const mapExtent = getMapExtent(resolution); const featureExtent = feature.getGeometry()?.getExtent(); @@ -49,7 +52,7 @@ export default function handleSemanticView({ let remainingExtents = [featureExtent]; vectorSource.forEachFeatureIntersectingExtent(featureExtent, intersectingFeature => { if ( - intersectingFeature.get('type') === 'COMPARTMENT' && + intersectingFeature.get('type') === MAP_ELEMENT_TYPES.COMPARTMENT && intersectingFeature.get('zIndex') > feature.get('zIndex') && intersectingFeature.get('filled') && !isFeatureInCompartment(+featureId, vectorSource, intersectingFeature) diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index 29f17e00af39fb20d135bf1784b5abeb03a83737..0b8d6403d8ebc05700fe6e32a8ce71e988000aaf 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -21,6 +21,7 @@ import Style from 'ol/style/Style'; import { ArrowTypeDict, LineTypeDict } from '@/redux/shapes/shapes.types'; import { LAYER_ELEMENT_TYPES, + REACTION_ELEMENT_CUTOFF_SCALE, TRANSPARENT_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; @@ -305,7 +306,7 @@ export default class Layer { let strokeStyle: Stroke | undefined; const type = feature.get('elementType'); - if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= 0.08) { + if (type === LAYER_ELEMENT_TYPES.ARROW && scale <= REACTION_ELEMENT_CUTOFF_SCALE) { return []; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts index e6e2de63ab686dec013cd8d7ee202be207771c05..ccefcd64da957e5a1ffef93fcb9398108eb54017 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction.ts @@ -7,7 +7,9 @@ import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/st import Polygon from 'ol/geom/Polygon'; import Style from 'ol/style/Style'; import { + REACTION_ELEMENT_CUTOFF_SCALE, REACTION_ELEMENT_TYPES, + TEXT_CUTOFF_SCALE, WHITE_COLOR, } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import { FeatureLike } from 'ol/Feature'; @@ -372,10 +374,7 @@ export default class Reaction { const type = feature.get('elementType'); let strokeStyle: Stroke | undefined; - if (type === REACTION_ELEMENT_TYPES.OPERATOR && scale < 0.34) { - return []; - } - if (type === REACTION_ELEMENT_TYPES.ARROW && scale <= 0.125) { + if (type === REACTION_ELEMENT_TYPES.OPERATOR && scale < TEXT_CUTOFF_SCALE) { return []; } @@ -406,7 +405,7 @@ export default class Reaction { const scale = this.minResolution / resolution; let strokeStyle: Stroke | undefined; - if (scale <= 0.125) { + if (scale <= REACTION_ELEMENT_CUTOFF_SCALE) { return []; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts index 6d84b083bd1cfda2dd3fc15f5ebcdc0a4d417070..27b8ec6f59260848cfeb6581ee1c8e91b99f61d8 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/text/Text.ts @@ -13,6 +13,7 @@ import { MapInstance } from '@/types/map'; import getTextCoords from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextCoords'; import getTextStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/text/getTextStyle'; import { Color } from '@/types/models'; +import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; export interface TextProps { x: number; @@ -101,15 +102,10 @@ export default class Text { if (getTextScale instanceof Function) { textScale = getTextScale(resolution); } - - if (this.style.getText()) { - if (this.fontSize * textScale > 4) { - this.style.getText()?.setScale(textScale); - this.style.getText()?.setText(this.text); - } else { - this.style.getText()?.setText(undefined); - } + if (textScale < TEXT_CUTOFF_SCALE) { + return undefined; } + this.style.getText()?.setScale(textScale); return this.style; } } diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.ts index cdf5b8f5c668c8336b7e332d854d2c72c9aa874e..d76947fdfa250784062178211202a1f7a3fff1d8 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.ts @@ -1,5 +1,5 @@ /* eslint-disable no-magic-numbers */ -import { OPTIONS, ZOOM_RESCALING_FACTOR } from '@/constants/map'; +import { DEFAULT_EXTENT_PADDING, OPTIONS, ZOOM_RESCALING_FACTOR } from '@/constants/map'; import { mapDataInitialPositionSelector, mapDataSizeSelector } from '@/redux/map/map.selectors'; import { MapInstance, Point } from '@/types/map'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; @@ -34,13 +34,13 @@ export const useOlMapView = ({ mapInstance }: UseOlMapViewInput): MapConfig['vie heightPadding = mapSize.width / mapInstanceWidthToHeightRatio - mapSize.height; } const topLeftPoint: Point = { - x: mapSize.width + widthPadding / 2, - y: mapSize.height + heightPadding / 2, + x: mapSize.width + widthPadding / 2 + DEFAULT_EXTENT_PADDING, + y: mapSize.height + heightPadding / 2 + DEFAULT_EXTENT_PADDING, }; const bottomRightPoint: Point = { - x: -widthPadding / 2, - y: -heightPadding / 2, + x: -widthPadding / 2 - DEFAULT_EXTENT_PADDING, + y: -heightPadding / 2 - DEFAULT_EXTENT_PADDING, }; return boundingExtent([topLeftPoint, bottomRightPoint].map(pointToProjection)); diff --git a/src/constants/map.ts b/src/constants/map.ts index d7efb594341aee93bf286ec885e543fb87036e8e..c995fdad5c637c292fc4162dd24dc6ac07e1021d 100644 --- a/src/constants/map.ts +++ b/src/constants/map.ts @@ -11,7 +11,7 @@ export const DEFAULT_CENTER_Y = 0; // eslint-disable-next-line no-magic-numbers export const LATLNG_FALLBACK: LatLng = [0, 0]; export const EXTENT_PADDING_MULTIPLICATOR = 1; - +export const DEFAULT_EXTENT_PADDING = 20; export const ZOOM_RESCALING_FACTOR = 1; export const DEFAULT_CENTER_POINT: Point = { diff --git a/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.test.tsx b/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9070b6bb1b12d609e8e447e98d93c92611de3ad3 --- /dev/null +++ b/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.test.tsx @@ -0,0 +1,40 @@ +/* eslint-disable no-magic-numbers */ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { OutsideClickWrapper } from '.'; + +describe('OutsideClickWrapper', () => { + it('should call onOutsideClick when click outside the component', () => { + const handleOutsideClick = jest.fn(); + const { getByText } = render( + <OutsideClickWrapper onOutsideClick={handleOutsideClick}> + <div>Inner element</div> + </OutsideClickWrapper>, + ); + + const innerElement = getByText('Inner element'); + + fireEvent.mouseDown(document.body); + + expect(handleOutsideClick).toHaveBeenCalledTimes(1); + + fireEvent.mouseDown(innerElement); + + expect(handleOutsideClick).toHaveBeenCalledTimes(1); + }); + + it('should not call onOutsideClick when click inside the component', () => { + const handleOutsideClick = jest.fn(); + const { getByText } = render( + <OutsideClickWrapper onOutsideClick={handleOutsideClick}> + <div>Inner element</div> + </OutsideClickWrapper>, + ); + + const innerElement = getByText('Inner element'); + + fireEvent.mouseDown(innerElement); + + expect(handleOutsideClick).not.toHaveBeenCalled(); + }); +}); diff --git a/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.tsx b/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..48c15e1fb985e3c885f9ccffbd92f797ada2211d --- /dev/null +++ b/src/shared/OutsideClickWrapper/OutsideClickWrapper.component.tsx @@ -0,0 +1,29 @@ +import React, { useEffect, useRef, ReactNode } from 'react'; + +interface OutsideClickHandlerProps { + onOutsideClick: () => void; + children: ReactNode; +} + +export const OutsideClickWrapper: React.FC<OutsideClickHandlerProps> = ({ + onOutsideClick, + children, +}) => { + const ref = useRef<HTMLDivElement>(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + if (ref.current && !ref.current.contains(event.target as Node)) { + onOutsideClick(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onOutsideClick]); + + return <div ref={ref}>{children}</div>; +}; diff --git a/src/shared/OutsideClickWrapper/index.tsx b/src/shared/OutsideClickWrapper/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1535d4cf27cd460f7eccc7724fe653d3e5a1e2b1 --- /dev/null +++ b/src/shared/OutsideClickWrapper/index.tsx @@ -0,0 +1 @@ +export { OutsideClickWrapper } from './OutsideClickWrapper.component';