@tpzdsp/next-toolkit 1.7.0 → 1.9.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.
- package/package.json +25 -1
- package/src/components/Card/Card.stories.tsx +68 -41
- package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +9 -7
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +66 -13
- package/src/components/ErrorBoundary/ErrorFallback.tsx +63 -23
- package/src/components/ErrorText/ErrorText.stories.tsx +43 -24
- package/src/components/ErrorText/ErrorText.tsx +4 -2
- package/src/components/Heading/Heading.tsx +6 -3
- package/src/components/Hint/Hint.stories.tsx +46 -28
- package/src/components/Paragraph/Paragraph.stories.tsx +65 -21
- package/src/components/Paragraph/Paragraph.tsx +3 -1
- package/src/components/SlidingPanel/SlidingPanel.stories.tsx +99 -0
- package/src/components/SlidingPanel/SlidingPanel.tsx +120 -111
- package/src/components/chip/Chip.stories.tsx +48 -26
- package/src/errors/ApiError.ts +8 -2
- package/src/http/constants.ts +2 -0
- package/src/map/LayerSwitcherControl.ts +47 -59
- package/src/map/MapComponent.tsx +1 -1
- package/src/map/basemaps.ts +4 -4
- package/src/utils/date.ts +6 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +12 -3
package/src/errors/ApiError.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import z from 'zod/v4';
|
|
4
4
|
|
|
5
|
-
import { HttpStatus, HttpStatusText } from '../http';
|
|
5
|
+
import { HttpStatus, HttpStatusText } from '../http/constants';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Schema defining the JSON shape of error responses returned by a server. (The proxy is where
|
|
@@ -53,7 +53,13 @@ export class ApiError extends Error implements z.output<typeof ApiErrorSchema> {
|
|
|
53
53
|
this.name = 'ApiError';
|
|
54
54
|
this.status = status;
|
|
55
55
|
this.details = details ?? null;
|
|
56
|
-
|
|
56
|
+
// Crypto is a NodeJS and browser global; use it if available, but fallback to a pseudo-random string otherwise.
|
|
57
|
+
// This is due to Storybook not providing the `crypto` global in some environments (e.g., Jest).
|
|
58
|
+
this.digest =
|
|
59
|
+
typeof crypto?.randomUUID === 'function'
|
|
60
|
+
? crypto.randomUUID()
|
|
61
|
+
: // eslint-disable-next-line sonarjs/pseudo-random
|
|
62
|
+
Math.random().toString(36).slice(2);
|
|
57
63
|
this.rehydrated = options?.rehydrated ?? false;
|
|
58
64
|
|
|
59
65
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
package/src/http/constants.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax */
|
|
2
|
+
import { createFocusTrap } from 'focus-trap';
|
|
3
|
+
import type { FocusTrap } from 'focus-trap';
|
|
2
4
|
import { Map } from 'ol';
|
|
3
5
|
import { Control } from 'ol/control';
|
|
4
6
|
import type { Options as ControlOptions } from 'ol/control/Control';
|
|
5
7
|
import BaseLayer from 'ol/layer/Base';
|
|
6
8
|
|
|
7
|
-
import { KeyboardKeys } from '../utils';
|
|
8
|
-
|
|
9
9
|
const TIMEOUT = 300; // Match CSS transition duration
|
|
10
10
|
const ARIA_LABEL = 'aria-label';
|
|
11
11
|
|
|
@@ -16,7 +16,8 @@ export class LayerSwitcherControl extends Control {
|
|
|
16
16
|
map!: Map;
|
|
17
17
|
panel!: HTMLElement;
|
|
18
18
|
liveRegion!: HTMLElement;
|
|
19
|
-
|
|
19
|
+
isOpen = false;
|
|
20
|
+
private focusTrap: FocusTrap | null = null;
|
|
20
21
|
|
|
21
22
|
constructor(layers: BaseLayer[], options?: ControlOptions) {
|
|
22
23
|
const button = document.createElement('button');
|
|
@@ -70,9 +71,6 @@ export class LayerSwitcherControl extends Control {
|
|
|
70
71
|
this.panel.setAttribute('aria-modal', 'true');
|
|
71
72
|
this.panel.setAttribute(ARIA_LABEL, 'Basemap switcher');
|
|
72
73
|
|
|
73
|
-
// Add a keydown listener to the panel for Escape key
|
|
74
|
-
this.panel.addEventListener('keydown', this.handlePanelKeyDown);
|
|
75
|
-
|
|
76
74
|
// Create the header for the close button
|
|
77
75
|
const header = document.createElement('div');
|
|
78
76
|
|
|
@@ -87,7 +85,6 @@ export class LayerSwitcherControl extends Control {
|
|
|
87
85
|
closeBtn.type = 'button';
|
|
88
86
|
closeBtn.addEventListener('click', () => {
|
|
89
87
|
this.toggleLayerSwitcher();
|
|
90
|
-
this.focusToggleButton();
|
|
91
88
|
});
|
|
92
89
|
header.appendChild(closeBtn);
|
|
93
90
|
|
|
@@ -124,6 +121,29 @@ export class LayerSwitcherControl extends Control {
|
|
|
124
121
|
this.panel.appendChild(header);
|
|
125
122
|
this.panel.appendChild(content);
|
|
126
123
|
|
|
124
|
+
// Create focus trap once - will be activated/deactivated as needed
|
|
125
|
+
this.focusTrap = createFocusTrap(this.panel, {
|
|
126
|
+
clickOutsideDeactivates: (event) => {
|
|
127
|
+
// if the user happens to click on the open button again we let the button handle the click event, not the focus trap
|
|
128
|
+
if (button.contains(event.target as Node)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.toggleLayerSwitcher();
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
},
|
|
136
|
+
escapeDeactivates: () => {
|
|
137
|
+
this.toggleLayerSwitcher();
|
|
138
|
+
|
|
139
|
+
return false;
|
|
140
|
+
},
|
|
141
|
+
returnFocusOnDeactivate: true,
|
|
142
|
+
fallbackFocus: () => {
|
|
143
|
+
return this.panel;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
127
147
|
button.addEventListener('click', this.toggleLayerSwitcher, false);
|
|
128
148
|
}
|
|
129
149
|
|
|
@@ -142,22 +162,6 @@ export class LayerSwitcherControl extends Control {
|
|
|
142
162
|
.find((layer: BaseLayer) => layer.get('name') === layerName);
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
private focusFirstButton() {
|
|
146
|
-
const firstBtn = this.panel.querySelector(
|
|
147
|
-
'button:not(.ol-layer-switcher-close)',
|
|
148
|
-
) as HTMLButtonElement | null;
|
|
149
|
-
|
|
150
|
-
firstBtn?.focus();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private focusToggleButton() {
|
|
154
|
-
const toggleBtn = this.element.querySelector(
|
|
155
|
-
'button.ol-layer-switcher-toggle',
|
|
156
|
-
) as HTMLButtonElement | null;
|
|
157
|
-
|
|
158
|
-
toggleBtn?.focus();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
165
|
private announceBasemapChange(layerName: string) {
|
|
162
166
|
if (this.liveRegion) {
|
|
163
167
|
this.liveRegion.textContent = `Basemap changed to ${layerName}`;
|
|
@@ -181,59 +185,33 @@ export class LayerSwitcherControl extends Control {
|
|
|
181
185
|
|
|
182
186
|
// Arrow function: 'this' is always bound to the class instance
|
|
183
187
|
toggleLayerSwitcher = () => {
|
|
184
|
-
if (this.
|
|
188
|
+
if (!this.isOpen) {
|
|
185
189
|
this.element.appendChild(this.panel);
|
|
190
|
+
|
|
186
191
|
requestAnimationFrame(() => {
|
|
187
192
|
this.panel.classList.add('open'); // Ensure animation works after adding to DOM
|
|
188
193
|
|
|
189
|
-
//
|
|
190
|
-
this.
|
|
194
|
+
// Activate the existing focus trap
|
|
195
|
+
this.focusTrap?.activate();
|
|
191
196
|
});
|
|
192
197
|
} else {
|
|
193
198
|
this.panel.classList.remove('open');
|
|
199
|
+
|
|
200
|
+
// Deactivate focus trap but keep the instance
|
|
201
|
+
this.focusTrap?.deactivate();
|
|
202
|
+
|
|
194
203
|
setTimeout(() => {
|
|
195
204
|
this.element.removeChild(this.panel);
|
|
196
205
|
}, TIMEOUT); // Matches CSS transition time to prevent flickering
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
this.
|
|
208
|
+
this.isOpen = !this.isOpen;
|
|
200
209
|
|
|
201
210
|
// Update aria-expanded on the toggle button
|
|
202
211
|
const toggleBtn = this.element.querySelector('button.ol-layer-switcher-toggle');
|
|
203
212
|
|
|
204
213
|
if (toggleBtn) {
|
|
205
|
-
toggleBtn.setAttribute('aria-expanded',
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// Arrow function for panel keydown
|
|
210
|
-
handlePanelKeyDown = (event: KeyboardEvent) => {
|
|
211
|
-
// Focus trap: cycle focus within the panel
|
|
212
|
-
if (event.key === KeyboardKeys.Tab) {
|
|
213
|
-
const focusable = Array.from(this.panel.querySelectorAll('button')) as HTMLButtonElement[];
|
|
214
|
-
|
|
215
|
-
if (focusable.length === 0) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const first = focusable[0];
|
|
220
|
-
const last = focusable[focusable.length - 1];
|
|
221
|
-
|
|
222
|
-
if (!event.shiftKey && document.activeElement === last) {
|
|
223
|
-
event.preventDefault();
|
|
224
|
-
first.focus();
|
|
225
|
-
} else if (event.shiftKey && document.activeElement === first) {
|
|
226
|
-
event.preventDefault();
|
|
227
|
-
last.focus();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Escape closes the panel and returns focus to the toggle button
|
|
232
|
-
if (event.key === KeyboardKeys.Escape) {
|
|
233
|
-
this.toggleLayerSwitcher();
|
|
234
|
-
|
|
235
|
-
// Focus back on the basemap switcher button
|
|
236
|
-
this.focusToggleButton();
|
|
214
|
+
toggleBtn.setAttribute('aria-expanded', this.isOpen.toString());
|
|
237
215
|
}
|
|
238
216
|
};
|
|
239
217
|
|
|
@@ -276,4 +254,14 @@ export class LayerSwitcherControl extends Control {
|
|
|
276
254
|
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
277
255
|
});
|
|
278
256
|
};
|
|
257
|
+
|
|
258
|
+
// Cleanup method - call this when removing the control
|
|
259
|
+
destroy() {
|
|
260
|
+
// Deactivate and destroy focus trap
|
|
261
|
+
if (this.focusTrap) {
|
|
262
|
+
this.focusTrap.deactivate();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.focusTrap = null;
|
|
266
|
+
}
|
|
279
267
|
}
|
package/src/map/MapComponent.tsx
CHANGED
package/src/map/basemaps.ts
CHANGED
|
@@ -8,7 +8,7 @@ import SatelliteMapTilerImage from './images/basemaps/satellite-map-tiler.png';
|
|
|
8
8
|
import SatelliteImage from './images/basemaps/satellite.png';
|
|
9
9
|
import StreetsImage from './images/basemaps/streets.png';
|
|
10
10
|
|
|
11
|
-
export const initializeBasemapLayers = () => {
|
|
11
|
+
export const initializeBasemapLayers = (baseUrl: string) => {
|
|
12
12
|
const osmLayer = new TileLayer({
|
|
13
13
|
preload: Infinity,
|
|
14
14
|
source: new OSM(),
|
|
@@ -24,7 +24,7 @@ export const initializeBasemapLayers = () => {
|
|
|
24
24
|
const osMapsLight = new TileLayer({
|
|
25
25
|
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
26
26
|
source: new XYZ({
|
|
27
|
-
url:
|
|
27
|
+
url: `${baseUrl}/api/maps/raster/v1/zxy/Light_3857/{z}/{x}/{y}.png`,
|
|
28
28
|
}),
|
|
29
29
|
visible: false,
|
|
30
30
|
});
|
|
@@ -36,7 +36,7 @@ export const initializeBasemapLayers = () => {
|
|
|
36
36
|
const osMapsOutdoor = new TileLayer({
|
|
37
37
|
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
38
38
|
source: new XYZ({
|
|
39
|
-
url:
|
|
39
|
+
url: `${baseUrl}/api/maps/raster/v1/zxy/Outdoor_3857/{z}/{x}/{y}.png`,
|
|
40
40
|
}),
|
|
41
41
|
visible: false,
|
|
42
42
|
});
|
|
@@ -50,7 +50,7 @@ export const initializeBasemapLayers = () => {
|
|
|
50
50
|
const osMapsRoad = new TileLayer({
|
|
51
51
|
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
52
52
|
source: new XYZ({
|
|
53
|
-
url:
|
|
53
|
+
url: `${baseUrl}/api/maps/raster/v1/zxy/Road_3857/{z}/{x}/{y}.png`,
|
|
54
54
|
}),
|
|
55
55
|
visible: false,
|
|
56
56
|
});
|
package/src/utils/index.ts
CHANGED
package/src/utils/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { verify } from 'jsonwebtoken';
|
|
2
2
|
import { twMerge } from 'tailwind-merge';
|
|
3
3
|
|
|
4
4
|
import type { Credentials, DecodedJWT } from '../types/auth';
|
|
@@ -9,10 +9,19 @@ export const decodeAuthToken = (token: string): Credentials | null => {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
-
|
|
12
|
+
// eslint-disable-next-line no-undef
|
|
13
|
+
const secret = process.env.JWT_SECRET;
|
|
14
|
+
|
|
15
|
+
if (!secret) {
|
|
16
|
+
throw new Error('JWT secret not set');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const user = verify(token, secret) as DecodedJWT;
|
|
13
20
|
|
|
14
21
|
return { token, user };
|
|
15
|
-
} catch {
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error decoding auth token:', error);
|
|
24
|
+
|
|
16
25
|
return null;
|
|
17
26
|
}
|
|
18
27
|
};
|