@tpzdsp/next-toolkit 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -6
  3. package/src/assets/styles/ol.css +147 -176
  4. package/src/components/InfoBox/InfoBox.stories.tsx +457 -0
  5. package/src/components/InfoBox/InfoBox.test.tsx +382 -0
  6. package/src/components/InfoBox/InfoBox.tsx +177 -0
  7. package/src/components/InfoBox/hooks/index.ts +3 -0
  8. package/src/components/InfoBox/hooks/useInfoBoxPosition.test.ts +187 -0
  9. package/src/components/InfoBox/hooks/useInfoBoxPosition.ts +69 -0
  10. package/src/components/InfoBox/hooks/useInfoBoxState.test.ts +168 -0
  11. package/src/components/InfoBox/hooks/useInfoBoxState.ts +71 -0
  12. package/src/components/InfoBox/hooks/usePortalMount.test.ts +62 -0
  13. package/src/components/InfoBox/hooks/usePortalMount.ts +15 -0
  14. package/src/components/InfoBox/types.ts +6 -0
  15. package/src/components/InfoBox/utils/focusTrapConfig.test.ts +310 -0
  16. package/src/components/InfoBox/utils/focusTrapConfig.ts +59 -0
  17. package/src/components/InfoBox/utils/index.ts +2 -0
  18. package/src/components/InfoBox/utils/positionUtils.test.ts +170 -0
  19. package/src/components/InfoBox/utils/positionUtils.ts +89 -0
  20. package/src/components/index.ts +8 -0
  21. package/src/map/FullScreenControl.ts +126 -0
  22. package/src/map/LayerSwitcherControl.ts +87 -181
  23. package/src/map/LayerSwitcherPanel.tsx +173 -0
  24. package/src/map/MapComponent.tsx +6 -35
  25. package/src/map/createControlButton.ts +72 -0
  26. package/src/map/geocoder/Geocoder.test.tsx +115 -0
  27. package/src/map/geocoder/Geocoder.tsx +393 -0
  28. package/src/map/geocoder/groupResults.ts +12 -0
  29. package/src/map/geocoder/index.ts +4 -0
  30. package/src/map/geocoder/types.ts +11 -0
  31. package/src/map/index.ts +4 -1
  32. package/src/map/osOpenNamesSearch.ts +112 -57
  33. package/src/test/renderers.tsx +9 -20
  34. package/src/map/geocoder.ts +0 -61
  35. package/src/ol-geocoder.d.ts +0 -1
@@ -0,0 +1,11 @@
1
+ export type GeocoderResult = {
2
+ id: string;
3
+ label: string;
4
+ group?: string;
5
+ extent?: [number, number, number, number];
6
+ center?: [number, number];
7
+ /** Optional zoom level for this result (e.g., city=12, street=16, building=18) */
8
+ zoom?: number;
9
+ };
10
+
11
+ export type GroupedResults = Record<string, GeocoderResult[]>;
package/src/map/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export * from './basemaps';
2
- export * from './geocoder';
2
+ export * from './geocoder/index';
3
3
  export * from './geometries';
4
+ export * from './createControlButton';
4
5
  export * from './LayerSwitcherControl';
6
+ export * from './LayerSwitcherPanel';
7
+ export * from './FullScreenControl';
5
8
  export * from './utils';
6
9
  export * from './MapComponent';
7
10
  export * from './MapContext';
@@ -1,71 +1,126 @@
1
1
  import type { FeatureCollection } from 'geojson';
2
+ import { transform, transformExtent } from 'ol/proj';
2
3
 
3
- export type SearchOption = {
4
- url?: string;
4
+ import type { GeocoderResult } from './geocoder/types';
5
+ import { EPSG_3857, EPSG_4326 } from './geometries';
6
+
7
+ export type OsOpenNamesSearchOptions = {
8
+ url: string;
5
9
  };
6
10
 
7
- export type SearchResult = {
8
- lon: number;
9
- lat: number;
10
- address?: string;
11
- bbox?: number[];
11
+ // Zoom levels based on OS Open Names feature types
12
+ const ZOOM_BY_TYPE: Record<string, number> = {
13
+ // Large areas
14
+ City: 12,
15
+ Town: 13,
16
+ 'Suburban Area': 14,
17
+ Village: 15,
18
+ Hamlet: 15,
19
+ // Streets and localities
20
+ 'Named Road': 16,
21
+ Section: 16,
22
+ 'Numbered Road': 16,
23
+ Other: 14,
24
+ // Precise locations
25
+ Postcode: 17,
26
+ 'Postcode Unit': 18,
12
27
  };
13
28
 
14
29
  /**
15
- * Custom provider for OS OpenNames search.
30
+ * Creates a search function for OS Open Names API that returns GeocoderResult[].
31
+ *
32
+ * @param options - Configuration options including the API URL.
33
+ * @returns A search function compatible with the Geocoder component.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const search = createOsOpenNamesSearch({ url: '/api/geocode' });
38
+ * <Geocoder map={map} search={search} />
39
+ * ```
16
40
  */
17
- export const osOpenNamesSearch = (options?: SearchOption) => {
18
- const { url } = options ?? {};
19
-
20
- return {
21
- /**
22
- * Get the URL and parameters needed for the request.
23
- *
24
- * @param {object} opt - Options object containing the query.
25
- */
26
- getParameters: (opt: { query: string }) => {
27
- return {
28
- url,
29
- params: {
30
- query: opt.query,
31
- },
32
- };
33
- },
34
-
35
- /**
36
- * Process the API response and format it for ol-geocoder.
37
- *
38
- * @param {object} results - OS Names API response.
39
- */
40
- handleResponse: (response: FeatureCollection) => {
41
- const { features } = response;
42
-
43
- if (!features?.length) {
44
- return [];
45
- }
46
-
47
- return features
48
- .map((feature) => {
49
- if (feature.geometry.type === 'Point') {
50
- const result: SearchResult = {
51
- lon: feature.geometry.coordinates[0]!,
52
- lat: feature.geometry.coordinates[1]!,
53
- address: feature?.properties?.address,
54
- };
55
-
56
- // Only include bbox if it's not empty
57
- if (feature.bbox && Array.isArray(feature.bbox) && feature.bbox.length > 0) {
58
- result.bbox = feature.bbox;
59
- }
60
-
61
- return result;
62
- }
41
+ export const createOsOpenNamesSearch = (options: OsOpenNamesSearchOptions) => {
42
+ const { url } = options;
43
+
44
+ return async (query: string): Promise<GeocoderResult[]> => {
45
+ const searchUrl = new URL(url, globalThis.location.origin);
46
+
47
+ searchUrl.searchParams.set('query', query);
48
+
49
+ const response = await fetch(searchUrl.toString());
50
+
51
+ if (!response.ok) {
52
+ console.error('OS Open Names search failed:', response.statusText);
53
+
54
+ return [];
55
+ }
63
56
 
57
+ const data: FeatureCollection = await response.json();
58
+ const { features } = data;
59
+
60
+ if (!features?.length) {
61
+ return [];
62
+ }
63
+
64
+ return features
65
+ .map((feature, index): GeocoderResult | null => {
66
+ if (feature.geometry.type !== 'Point') {
64
67
  console.error('Geometry type is not Point');
65
68
 
66
69
  return null;
67
- })
68
- .filter(Boolean);
69
- },
70
+ }
71
+
72
+ const [lon, lat] = feature.geometry.coordinates;
73
+
74
+ // Type guard: ensure coordinates are valid numbers
75
+ if (typeof lon !== 'number' || typeof lat !== 'number') {
76
+ console.error('Invalid coordinates: lon or lat is not a number');
77
+
78
+ return null;
79
+ }
80
+
81
+ const properties = feature.properties ?? {};
82
+
83
+ // Handle address which can be an object or string
84
+ let label: string;
85
+
86
+ if (properties.address && typeof properties.address === 'object') {
87
+ const addr = properties.address as { name?: string; town?: string; country?: string };
88
+
89
+ label = [addr.name, addr.town, addr.country].filter(Boolean).join(', ');
90
+ } else {
91
+ label = properties.address ?? properties.name ?? `${lat}, ${lon}`;
92
+ }
93
+
94
+ const [x, y] = transform([lon, lat], EPSG_4326, EPSG_3857);
95
+
96
+ // Type guard: ensure transformed coordinates are valid numbers
97
+ if (typeof x !== 'number' || typeof y !== 'number') {
98
+ console.error('Transform failed: x or y is not a number');
99
+
100
+ return null;
101
+ }
102
+
103
+ const result: GeocoderResult = {
104
+ id: properties.id ?? `result-${index}`,
105
+ label,
106
+ group: properties.type ?? properties.localType,
107
+ center: [x, y],
108
+ // Assign zoom level based on type, fallback to 14
109
+ zoom: ZOOM_BY_TYPE[properties.type] ?? ZOOM_BY_TYPE[properties.localType] ?? 14,
110
+ };
111
+
112
+ // Transform bbox to map projection if available
113
+ if (feature.bbox && Array.isArray(feature.bbox) && feature.bbox.length === 4) {
114
+ result.extent = transformExtent(feature.bbox, EPSG_4326, EPSG_3857) as [
115
+ number,
116
+ number,
117
+ number,
118
+ number,
119
+ ];
120
+ }
121
+
122
+ return result;
123
+ })
124
+ .filter((result): result is GeocoderResult => result !== null);
70
125
  };
71
126
  };
@@ -3,6 +3,7 @@ import type { ReactElement, ReactNode } from 'react';
3
3
  import {
4
4
  type RenderHookOptions,
5
5
  type RenderHookResult,
6
+ type RenderOptions,
6
7
  render,
7
8
  renderHook,
8
9
  } from '@testing-library/react';
@@ -11,11 +12,6 @@ type WrapperParams = {
11
12
  children: ReactNode;
12
13
  };
13
14
 
14
- type Options = {
15
- initialEntries?: string[];
16
- renderHookOptions?: RenderHookOptions<unknown>;
17
- };
18
-
19
15
  /**
20
16
  * A custom testing-library component renderer that wraps the component being
21
17
  * rendered with any Providers used in the app. Any test can pre-configure the
@@ -26,13 +22,10 @@ type Options = {
26
22
  *
27
23
  * @returns The rendered component.
28
24
  */
29
- const customRender = (ui: ReactElement, options?: Options) => {
30
- const { renderHookOptions } = options ?? {};
25
+ const customRender = (ui: ReactElement, options?: RenderOptions) => {
26
+ const wrapper = ({ children }: WrapperParams): ReactElement => <>{children}</>;
31
27
 
32
- return render(ui, {
33
- wrapper: ({ children }: WrapperParams): ReactElement => <>{children}</>,
34
- ...renderHookOptions,
35
- });
28
+ return render(ui, { wrapper, ...options });
36
29
  };
37
30
 
38
31
  /**
@@ -45,17 +38,13 @@ const customRender = (ui: ReactElement, options?: Options) => {
45
38
  *
46
39
  * @returns The rendered hook.
47
40
  */
48
- const customRenderHook = <T, P>(
49
- callback: () => unknown,
50
- options?: Options,
51
- ): RenderHookResult<T, P> => {
52
- const { renderHookOptions } = options ?? {};
53
-
41
+ const customRenderHook = <Result, Props>(
42
+ callback: (initialProps: Props) => Result,
43
+ options?: RenderHookOptions<Props>,
44
+ ): RenderHookResult<Result, Props> => {
54
45
  const wrapper = ({ children }: WrapperParams): ReactElement => <>{children}</>;
55
46
 
56
- const utils = renderHook(() => callback(), { wrapper, ...renderHookOptions });
57
-
58
- return utils as RenderHookResult<T, P>;
47
+ return renderHook(callback, { wrapper, ...options });
59
48
  };
60
49
 
61
50
  export { screen, waitFor, within, act } from '@testing-library/react';
@@ -1,61 +0,0 @@
1
- import { Map } from 'ol';
2
- import { transformExtent } from 'ol/proj';
3
- import Geocoder from 'ol-geocoder';
4
-
5
- import { EPSG_3857, EPSG_4326 } from './geometries';
6
- import { osOpenNamesSearch } from './osOpenNamesSearch';
7
-
8
- type AddressChosenEvent = {
9
- place: {
10
- lon: number;
11
- lat: number;
12
- bbox?: [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]
13
- address: {
14
- name: string;
15
- city?: string;
16
- state?: string;
17
- country?: string;
18
- };
19
- };
20
- };
21
-
22
- export const initializeGeocoder = (apiKey: string, geoCoderUrl: string, map: Map) => {
23
- if (!apiKey) {
24
- console.warn('No API key provided for geocoder initialization.');
25
-
26
- throw new Error(
27
- 'API key is required for geocoder initialization. Please provide a valid API key.',
28
- );
29
- }
30
-
31
- const provider = osOpenNamesSearch({
32
- url: geoCoderUrl,
33
- });
34
-
35
- const geocoder = new Geocoder('nominatim', {
36
- provider,
37
- key: apiKey,
38
- lang: 'en',
39
- placeholder: 'Search for ...',
40
- targetType: 'text-input',
41
- keepOpen: false,
42
- preventMarker: true,
43
- debug: true,
44
- });
45
-
46
- // Since we provide a bbox in the custom provider response,
47
- // we also need to handle the address chosen event.
48
- geocoder.on('addresschosen', (event: AddressChosenEvent) => {
49
- if (event.place.bbox) {
50
- const extent3857 = transformExtent(event.place.bbox, EPSG_4326, EPSG_3857);
51
-
52
- map.getView().fit(extent3857, {
53
- duration: 500,
54
- padding: [50, 50, 50, 50],
55
- maxZoom: 18,
56
- });
57
- }
58
- });
59
-
60
- return geocoder;
61
- };
@@ -1 +0,0 @@
1
- declare module 'ol-geocoder';