Skip to content

Commit 16eee94

Browse files
Komzpavkozio
andauthored
feat(search): 22291 select area when location chosen (#1194)
* feat(search): 22291 highlight hovered result geometry * test(search): 22291 add unit test for highlighted atom * fix(search): 22291 clean highlight layer lifecycle * fix(search): 22291 clean highlight layer cleanup * test(search): 22291 broaden highlighted atom cases * test(search): 22291 add coverage for search features * fix(search): 22291 clear highlight on selection * Delete src/features/search/tests/initSearchHighlightLayer.test.ts * Delete src/features/search/tests/highlightedAtom.test.ts * Delete src/features/search/tests/searchAtoms.test.ts * Delete src/features/search/tests/searchLocationAtoms.test.ts --------- Co-authored-by: VK <112831093+vkozio@users.noreply.github.com>
1 parent 1560c07 commit 16eee94

File tree

9 files changed

+159
-6
lines changed

9 files changed

+159
-6
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { atom } from '@reatom/framework';
2+
import type { Feature, FeatureCollection } from 'geojson';
3+
4+
export const searchHighlightedGeometryAtom = atom<FeatureCollection | Feature>(
5+
{
6+
type: 'FeatureCollection',
7+
features: [],
8+
},
9+
'searchHighlightedGeometryAtom',
10+
);

src/features/search/componets/SearchBar/SearchBar.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SelectItem } from '@konturio/ui-kit';
22
import { useAction, useAtom } from '@reatom/npm-react';
33
import cn from 'clsx';
4-
import { forwardRef } from 'react';
4+
import { forwardRef, useEffect } from 'react';
55
import { searchLocationsAtom } from '~features/search/searchLocationAtoms';
66
import {
77
itemSelectAction,
@@ -16,6 +16,9 @@ import {
1616
MCDASuggestionAtom,
1717
} from '~features/search/searchMcdaAtoms';
1818
import { SearchInput } from '~components/Search/SearchInput/SearchInput';
19+
import { searchHighlightedGeometryAtom } from '../../atoms/highlightedGeometry';
20+
import type { Feature } from 'geojson';
21+
import { EMPTY_HIGHLIGHT } from '../../constants';
1922
import { useSearchMenu } from '~utils/hooks/useSearchMenu';
2023
import style from './SearchBar.module.css';
2124
import type { AggregatedSearchItem } from '~features/search/searchAtoms';
@@ -33,19 +36,27 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
3336
? i18n.t('search.input_placeholder_mcda')
3437
: i18n.t('search.input_placeholder');
3538

36-
const search = useAction(searchAction);
37-
const itemSelectActionFn = useAction(itemSelectAction);
38-
const reset = useAction(resetSearchAction);
39+
const search = useAction(searchAction);
40+
const itemSelectActionFn = useAction(itemSelectAction);
41+
const reset = useAction(resetSearchAction);
42+
const setHighlightedGeometry = useAction(searchHighlightedGeometryAtom);
3943

4044
const itemSelect = (item: AggregatedSearchItem) => {
4145
itemSelectActionFn(item);
46+
setHighlightedGeometry(EMPTY_HIGHLIGHT);
4247
onItemSelect?.();
4348
};
4449

4550
const [{ error, loading, data }] = useAtom(searchLocationsAtom);
4651
const emptyLocations = data ? data.length === 0 : false;
4752
const [mcdaSearchStatus] = useAtom(MCDASuggestionAtom);
48-
const [aggregatedResults] = useAtom(aggregatedSearchAtom);
53+
const [aggregatedResults] = useAtom(aggregatedSearchAtom);
54+
55+
useEffect(() => {
56+
return () => {
57+
setHighlightedGeometry(EMPTY_HIGHLIGHT);
58+
};
59+
}, [setHighlightedGeometry]);
4960

5061
const renderError = () => (
5162
<SelectItem
@@ -91,6 +102,11 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
91102
className={style.listItem}
92103
itemProps={{
93104
onClick: () => handleItemSelect(item),
105+
onMouseEnter: () =>
106+
item.geometry &&
107+
setHighlightedGeometry(item as Feature),
108+
onMouseLeave: () =>
109+
setHighlightedGeometry(EMPTY_HIGHLIGHT),
94110
role: 'option',
95111
}}
96112
/>
@@ -108,6 +124,10 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
108124
highlighted={highlightedIndex === index}
109125
itemProps={{
110126
onClick: () => handleItemSelect(item),
127+
onMouseEnter: () =>
128+
setHighlightedGeometry(EMPTY_HIGHLIGHT),
129+
onMouseLeave: () =>
130+
setHighlightedGeometry(EMPTY_HIGHLIGHT),
111131
role: 'option',
112132
}}
113133
/>
@@ -147,6 +167,10 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
147167
emptyLocations,
148168
});
149169

170+
useEffect(() => {
171+
if (!isMenuOpen) setHighlightedGeometry(EMPTY_HIGHLIGHT);
172+
}, [isMenuOpen, setHighlightedGeometry]);
173+
150174
return (
151175
<>
152176
<div className={cn(style.searchBar, searchBarClass)} ref={searchBarRef}>

src/features/search/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { FeatureCollection } from 'geojson';
2+
3+
export const SEARCH_HIGHLIGHT_LAYER_ID = 'search-highlight';
4+
export const SEARCH_HIGHLIGHT_COLOR = '#000000';
5+
export const EMPTY_HIGHLIGHT: FeatureCollection = {
6+
type: 'FeatureCollection',
7+
features: [],
8+
};

src/features/search/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { i18n } from '~core/localization';
77
import { useShortPanelState } from '~utils/hooks/useShortPanelState';
88
import { useAutoCollapsePanel } from '~utils/hooks/useAutoCollapsePanel';
99
import { SearchBar } from '~features/search/componets/SearchBar/SearchBar';
10+
import { initSearchHighlightLayer } from './initSearchHighlightLayer';
1011
import s from './styles.module.css';
1112

1213
export function Search() {
@@ -18,6 +19,13 @@ export function Search() {
1819
const inputRef = useRef<HTMLInputElement>(null);
1920
const isMobile = useMediaQuery(IS_MOBILE_QUERY);
2021

22+
useEffect(() => {
23+
const destroyHighlightLayer = initSearchHighlightLayer();
24+
return () => {
25+
destroyHighlightLayer?.();
26+
};
27+
}, []);
28+
2129
useEffect(() => {
2230
if (isOpen && isMobile) {
2331
inputRef?.current?.focus(); // triggers phone keyboard
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { currentMapAtom } from '~core/shared_state';
2+
import { store } from '~core/store/store';
3+
import type { LogicalLayerState } from '~core/logical_layers/types/logicalLayer';
4+
import type { Feature, FeatureCollection } from 'geojson';
5+
import {
6+
SEARCH_HIGHLIGHT_LAYER_ID,
7+
SEARCH_HIGHLIGHT_COLOR,
8+
} from './constants';
9+
import { searchHighlightedGeometryAtom } from './atoms/highlightedGeometry';
10+
import { SearchHighlightRenderer } from './renderers/SearchHighlightRenderer';
11+
12+
let cleanUp: (() => void) | null = null;
13+
14+
export function initSearchHighlightLayer() {
15+
if (cleanUp) return cleanUp;
16+
17+
const ctx = store.v3ctx;
18+
const map = ctx.get(currentMapAtom.v3atom);
19+
if (!map) return () => {};
20+
21+
const sourceId = `${SEARCH_HIGHLIGHT_LAYER_ID}-source`;
22+
const renderer = new SearchHighlightRenderer({
23+
layerId: SEARCH_HIGHLIGHT_LAYER_ID,
24+
sourceId,
25+
color: SEARCH_HIGHLIGHT_COLOR,
26+
});
27+
28+
const state: LogicalLayerState = {
29+
id: SEARCH_HIGHLIGHT_LAYER_ID,
30+
isDownloadable: false,
31+
isVisible: true,
32+
isLoading: false,
33+
isEnabled: true,
34+
isEditable: false,
35+
isMounted: true,
36+
source: {
37+
id: sourceId,
38+
source: { type: 'geojson', data: { type: 'FeatureCollection', features: [] } },
39+
},
40+
legend: null,
41+
meta: null,
42+
settings: null,
43+
error: null,
44+
contextMenu: null,
45+
style: null,
46+
editor: null,
47+
};
48+
49+
renderer.willMount({ map, state });
50+
51+
const unsubscribe = ctx.subscribe(
52+
searchHighlightedGeometryAtom,
53+
(geometry: FeatureCollection | Feature) => {
54+
renderer.willSourceUpdate({
55+
map,
56+
state: {
57+
...state,
58+
source: {
59+
id: sourceId,
60+
source: { type: 'geojson', data: geometry },
61+
},
62+
},
63+
});
64+
},
65+
);
66+
67+
cleanUp = () => {
68+
unsubscribe();
69+
renderer.willUnMount({ map });
70+
cleanUp = null;
71+
};
72+
73+
return cleanUp;
74+
}

src/features/search/readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Search feature
2+
3+
This feature provides a search bar to find locations or MCDA analysis suggestions.
4+
Selected locations become the focused area on the map.
5+
While hovering over location results the geometry is highlighted on the map with a black outline.
6+
7+
## Main parts
8+
- `SearchBar` - UI component with dropdown results.
9+
- `searchLocationAtoms.ts` - handles locations fetching and selection.
10+
- `searchAtoms.ts` - aggregates MCDA and location results.
11+
- `atoms/highlightedGeometry.ts` - stores geometry highlighted on hover.
12+
- `initSearchHighlightLayer` - registers temporary map layer for hovered geometries.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { BoundarySelectorRenderer } from '~features/boundary_selector/renderers/BoundarySelectorRenderer';
2+
3+
export class SearchHighlightRenderer extends BoundarySelectorRenderer {}

src/features/search/searchAtoms.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
selectMCDAItemAction,
1313
isMCDASearchEnabled,
1414
} from '~features/search/searchMcdaAtoms';
15+
import { searchHighlightedGeometryAtom } from './atoms/highlightedGeometry';
16+
import { EMPTY_HIGHLIGHT } from './constants';
1517
import type { LocationProperties } from '~core/api/search';
1618
import type { Geometry } from 'geojson';
1719
import type { MCDAConfig } from '~core/logical_layers/renderers/stylesConfigs/mcda/types';
@@ -63,6 +65,7 @@ export const itemSelectAction = action((ctx, item: AggregatedSearchItem) => {
6365
} else if (item.source === 'mcda') {
6466
selectMCDAItemAction(ctx);
6567
}
68+
searchHighlightedGeometryAtom(ctx, EMPTY_HIGHLIGHT);
6669
});
6770

6871
export const resetSearchAction = action((ctx) => {

src/features/search/searchLocationAtoms.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
withStatusesAtom,
99
} from '@reatom/framework';
1010
import { getLocations } from '~core/api/search';
11-
import { setCurrentMapBbox } from '~core/shared_state/currentMapPosition';
11+
import {
12+
setCurrentMapBbox,
13+
focusOnGeometry,
14+
} from '~core/shared_state/currentMapPosition';
15+
import { focusedGeometryAtom } from '~core/focused_geometry/model';
1216

1317
export const fetchLocationsAsyncResource = reatomAsync(
1418
(ctx, query: string) => getLocations(query, ctx.controller),
@@ -28,6 +32,13 @@ export const searchLocationsAtom = atom((ctx) => {
2832
export const selectLocationItemAction = action((ctx, item) => {
2933
const bbox = item.properties.bbox;
3034
setCurrentMapBbox(ctx, bbox);
35+
if (item.geometry) {
36+
focusedGeometryAtom.setFocusedGeometry.v3action(ctx, {
37+
source: { type: 'custom' },
38+
geometry: item,
39+
});
40+
focusOnGeometry(ctx, item);
41+
}
3142
});
3243

3344
export const resetLocationSearchAction = action((ctx) => {

0 commit comments

Comments
 (0)