@defra/interactive-map 0.0.36-alpha → 0.0.38-alpha

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 (248) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-es/src/events.js +1 -1
  15. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  19. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  20. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  21. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  22. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
  24. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  26. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  27. package/plugins/draw/dist/esm/index.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  30. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  32. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  33. package/plugins/draw/dist/umd/index.js +2 -0
  34. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  35. package/plugins/draw/src/DrawInit.jsx +89 -0
  36. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  37. package/plugins/draw/src/adapterEvents.js +127 -0
  38. package/plugins/draw/src/adapterEvents.test.js +20 -0
  39. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  41. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  43. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  44. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  45. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  49. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  98. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  100. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  104. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  106. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  107. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  109. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  111. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  112. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  114. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  115. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  121. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  149. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  159. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  166. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  167. package/plugins/draw/src/api/addFeature.js +22 -0
  168. package/plugins/draw/src/api/addFeature.test.js +41 -0
  169. package/plugins/draw/src/api/deleteFeature.js +11 -0
  170. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  171. package/plugins/draw/src/api/editFeature.js +54 -0
  172. package/plugins/draw/src/api/editFeature.test.js +134 -0
  173. package/plugins/draw/src/api/merge.js +30 -0
  174. package/plugins/draw/src/api/merge.test.js +57 -0
  175. package/plugins/draw/src/api/newLine.js +41 -0
  176. package/plugins/draw/src/api/newLine.test.js +93 -0
  177. package/plugins/draw/src/api/newPolygon.js +41 -0
  178. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  179. package/plugins/draw/src/api/split.js +117 -0
  180. package/plugins/draw/src/api/split.test.js +204 -0
  181. package/plugins/draw/src/defaults.js +52 -0
  182. package/plugins/draw/src/defaults.test.js +18 -0
  183. package/plugins/draw/src/draw.scss +43 -0
  184. package/plugins/draw/src/events.js +237 -0
  185. package/plugins/draw/src/events.test.js +369 -0
  186. package/plugins/draw/src/index.js +10 -0
  187. package/plugins/draw/src/index.test.js +24 -0
  188. package/plugins/draw/src/manifest.js +170 -0
  189. package/plugins/draw/src/manifest.test.js +147 -0
  190. package/plugins/draw/src/reducer.js +69 -0
  191. package/plugins/draw/src/reducer.test.js +85 -0
  192. package/plugins/draw/src/utils/debounce.js +16 -0
  193. package/plugins/draw/src/utils/debounce.test.js +40 -0
  194. package/plugins/draw/src/utils/eventBus.js +36 -0
  195. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  197. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  199. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  200. package/plugins/draw/src/utils/resolveColors.js +39 -0
  201. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  202. package/plugins/draw/src/utils/spatial.js +243 -0
  203. package/plugins/draw/src/utils/spatial.test.js +243 -0
  204. package/plugins/draw/src/utils/touchTarget.js +95 -0
  205. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  206. package/plugins/draw/src/utils/undoStack.js +31 -0
  207. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  209. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  210. package/plugins/draw/src/validation/liveStroke.js +85 -0
  211. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  212. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  213. package/plugins/draw/src/validation/rules.js +186 -0
  214. package/plugins/draw/src/validation/rules.test.js +120 -0
  215. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  216. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  217. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  218. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  220. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  221. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  222. package/plugins/search/dist/css/index.css +1 -317
  223. package/rollup.esm.mjs +35 -28
  224. package/sonar-project.properties +2 -2
  225. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  226. package/src/App/components/Hints/Hints.module.scss +5 -4
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  228. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  229. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  230. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  231. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  232. package/src/App/hooks/useHintsAPI.js +67 -0
  233. package/src/App/hooks/useHintsAPI.test.js +145 -0
  234. package/src/App/renderer/PluginInits.jsx +4 -0
  235. package/src/App/renderer/PluginInits.test.jsx +7 -1
  236. package/src/InteractiveMap/InteractiveMap.js +20 -0
  237. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  238. package/src/config/events.js +4 -0
  239. package/src/scss/settings/_dimensions.scss +3 -0
  240. package/src/services/announcer.js +38 -3
  241. package/src/services/announcer.test.js +133 -1
  242. package/src/services/hints.js +3 -0
  243. package/src/services/hints.test.js +19 -0
  244. package/src/types.js +12 -0
  245. package/src/utils/detectInterfaceType.js +5 -1
  246. package/src/utils/isMac.js +15 -0
  247. package/src/utils/isMac.test.js +37 -0
  248. package/webpack.umd.mjs +1 -0
@@ -19,15 +19,35 @@ import { debounce } from '../utils/debounce.js'
19
19
  * action result is holding priority so they can never clobber what
20
20
  * the user actually requested.
21
21
  */
22
- export function createAnnouncer (mapStatusRef) {
22
+ export function createAnnouncer (mapStatusRef, { startupGraceDelay = 1000 } = {}) {
23
23
  const CLEAR_DELAY = 100
24
24
  const DEBOUNCE_DELAY = 500
25
25
  // How long an action result keeps priority over ambient messages. Covers the
26
26
  // debounce + clear write (600ms) plus a short buffer so a hint firing right
27
27
  // after a user action can't stomp the action's message before it is read.
28
28
  const ACTION_HOLD_DELAY = 1000
29
+ // Invisible, unpronounced marker appended to alternate repeats of the same
30
+ // message. Some screen readers (VoiceOver in particular) de-dupe against the
31
+ // last utterance they spoke for a live region, not just the DOM's final
32
+ // state — so a genuine ''-then-text mutation can still be silently skipped
33
+ // if the text matches what was already read. Toggling this marker in keeps
34
+ // consecutive identical announcements from ever being byte-identical.
35
+ const REPEAT_MARKER = '\u200B'
36
+ // A screen reader may still be building its accessibility tree for a
37
+ // newly-loaded page — the live region's DOM node can exist and mutate
38
+ // correctly while the AT hasn't registered it yet, so an announcement fired
39
+ // this soon after boot (e.g. a consumer calling showHint() on 'app:ready')
40
+ // can be silently missed even though nothing here did anything wrong. Give
41
+ // any announcement inside this window from creation an extra head start;
42
+ // announcements after it behave exactly as before, with zero extra delay.
43
+ // Configurable (default 1000ms) so tests can set it to 0 and not have to
44
+ // account for it in every unrelated timing assertion.
45
+ const STARTUP_GRACE_DELAY = startupGraceDelay
46
+ const startedAt = Date.now()
29
47
 
30
48
  let actionHoldTimer = null
49
+ let lastAnnouncedMsg = null
50
+ let repeatMarkerOn = false
31
51
 
32
52
  // Core function to write to the live region
33
53
  const setLiveRegion = (msg) => {
@@ -35,14 +55,18 @@ export function createAnnouncer (mapStatusRef) {
35
55
  return
36
56
  }
37
57
 
58
+ const startupDelay = Math.max(0, STARTUP_GRACE_DELAY - (Date.now() - startedAt))
59
+
38
60
  // Clear first (for SR to re-announce)
39
61
  mapStatusRef.current.textContent = ''
40
62
  setTimeout(() => {
41
63
  if (!mapStatusRef.current) {
42
64
  return
43
65
  }
44
- mapStatusRef.current.textContent = msg
45
- }, CLEAR_DELAY)
66
+ repeatMarkerOn = msg === lastAnnouncedMsg ? !repeatMarkerOn : false
67
+ lastAnnouncedMsg = msg
68
+ mapStatusRef.current.textContent = repeatMarkerOn ? msg + REPEAT_MARKER : msg
69
+ }, CLEAR_DELAY + startupDelay)
46
70
  }
47
71
 
48
72
  // Debounced announcer to group rapid action events down to the latest one
@@ -76,5 +100,16 @@ export function createAnnouncer (mapStatusRef) {
76
100
  debouncedAnnounce(msg)
77
101
  }
78
102
 
103
+ // Blanks the live region without announcing anything. Used when a message is
104
+ // dismissed (e.g. a hint toast auto-dismissing) so a later identical message
105
+ // starts from a genuinely empty region — otherwise clear-then-set-same-text
106
+ // nets out to no change and some screen readers skip the re-announcement.
107
+ announce.clear = () => {
108
+ if (!mapStatusRef?.current) {
109
+ return
110
+ }
111
+ mapStatusRef.current.textContent = ''
112
+ }
113
+
79
114
  return announce
80
115
  }
@@ -8,7 +8,9 @@ describe('createAnnouncer', () => {
8
8
 
9
9
  beforeEach(() => {
10
10
  mapStatusRef = { current: { textContent: '' } }
11
- announce = createAnnouncer(mapStatusRef)
11
+ // startupGraceDelay: 0 — these tests exercise everything except the startup
12
+ // grace window itself, which has its own dedicated describe block below.
13
+ announce = createAnnouncer(mapStatusRef, { startupGraceDelay: 0 })
12
14
  })
13
15
 
14
16
  afterEach(() => {
@@ -106,6 +108,74 @@ describe('createAnnouncer', () => {
106
108
  expect(mapStatusRef.current.textContent).toBe('Keyboard hint')
107
109
  })
108
110
 
111
+ it('exposes a clear() that blanks the live region without announcing', () => {
112
+ announce('Hint message', 'ambient')
113
+ jest.advanceTimersByTime(100)
114
+ expect(mapStatusRef.current.textContent).toBe('Hint message')
115
+
116
+ announce.clear()
117
+ expect(mapStatusRef.current.textContent).toBe('')
118
+ })
119
+
120
+ it('clear() is a no-op when mapStatusRef.current is null', () => {
121
+ const announceWithNull = createAnnouncer({ current: null })
122
+ expect(() => announceWithNull.clear()).not.toThrow()
123
+ })
124
+
125
+ it('a repeated identical ambient message re-announces after clear() blanks stale text', () => {
126
+ // First announcement: region starts empty, so clear-then-set is a real transition.
127
+ announce('Same message', 'ambient')
128
+ jest.advanceTimersByTime(100)
129
+ expect(mapStatusRef.current.textContent).toBe('Same message')
130
+
131
+ // Something (e.g. a hint dismissing) blanks the region before the retry, the way
132
+ // hints.dismiss() now does — without this, clear-then-set-same-text nets to no
133
+ // visible change and some screen readers skip the re-announcement.
134
+ announce.clear()
135
+ expect(mapStatusRef.current.textContent).toBe('')
136
+
137
+ announce('Same message', 'ambient')
138
+ jest.advanceTimersByTime(100)
139
+ // Repeat marker kicks in because this is a repeat of the last *spoken* message,
140
+ // regardless of the clear() in between — see the dedicated marker tests below.
141
+ expect(mapStatusRef.current.textContent).toBe('Same message' + '\u200B')
142
+ })
143
+
144
+ it('alternates an invisible marker on consecutive identical messages so the text is never byte-identical twice in a row', () => {
145
+ // VoiceOver has been observed to de-dupe against the last utterance it spoke for a
146
+ // live region, not just the DOM's final state — so a real ''-then-text mutation can
147
+ // still be silently skipped if the text matches what it already read out. Repeats
148
+ // must therefore differ at the string level even though they sound identical.
149
+ announce('Same message', 'ambient')
150
+ jest.advanceTimersByTime(100)
151
+ const first = mapStatusRef.current.textContent
152
+ expect(first).toBe('Same message')
153
+
154
+ announce('Same message', 'ambient')
155
+ jest.advanceTimersByTime(100)
156
+ const second = mapStatusRef.current.textContent
157
+ expect(second).not.toBe(first)
158
+ expect(second).toBe('Same message' + '\u200B')
159
+
160
+ announce('Same message', 'ambient')
161
+ jest.advanceTimersByTime(100)
162
+ const third = mapStatusRef.current.textContent
163
+ expect(third).not.toBe(second)
164
+ expect(third).toBe(first) // toggles back to the plain form
165
+ })
166
+
167
+ it('resets the repeat marker once a different message interrupts a run of repeats', () => {
168
+ announce('A', 'ambient')
169
+ jest.advanceTimersByTime(100)
170
+ announce('A', 'ambient')
171
+ jest.advanceTimersByTime(100)
172
+ expect(mapStatusRef.current.textContent).toBe('A' + '\u200B')
173
+
174
+ announce('B', 'ambient')
175
+ jest.advanceTimersByTime(100)
176
+ expect(mapStatusRef.current.textContent).toBe('B') // fresh message, no marker
177
+ })
178
+
109
179
  it('latest action wins: rapid actions announce only the most recent', () => {
110
180
  announce('First', 'action')
111
181
  jest.advanceTimersByTime(200) // still within debounce window
@@ -115,3 +185,65 @@ describe('createAnnouncer', () => {
115
185
  expect(mapStatusRef.current.textContent).toBe('Second')
116
186
  })
117
187
  })
188
+
189
+ describe('createAnnouncer — startup grace delay', () => {
190
+ // A consumer calling showHint() the instant the app is ready (e.g. on
191
+ // 'app:ready') can fire before the screen reader has finished registering
192
+ // the live region in its own accessibility tree, even though the DOM node
193
+ // already exists and mutates correctly. These tests use a small explicit
194
+ // window (rather than the real 1000ms default) purely so assertions don't
195
+ // need to advance unrealistic amounts of fake time.
196
+ const GRACE = 300
197
+ let mapStatusRef
198
+ let announce
199
+
200
+ beforeEach(() => {
201
+ mapStatusRef = { current: { textContent: '' } }
202
+ announce = createAnnouncer(mapStatusRef, { startupGraceDelay: GRACE })
203
+ })
204
+
205
+ afterEach(() => {
206
+ jest.clearAllTimers()
207
+ })
208
+
209
+ it('delays an announcement fired immediately after creation by the full grace window', () => {
210
+ announce('Ready', 'ambient')
211
+ jest.advanceTimersByTime(100) // normal CLEAR_DELAY only
212
+ expect(mapStatusRef.current.textContent).toBe('') // still withheld
213
+
214
+ jest.advanceTimersByTime(GRACE) // the rest of the grace window
215
+ expect(mapStatusRef.current.textContent).toBe('Ready')
216
+ })
217
+
218
+ it('only waits out the remaining grace window, not the full window again', () => {
219
+ jest.advanceTimersByTime(200) // partway through the grace window already
220
+ announce('Ready', 'ambient')
221
+
222
+ // Remaining grace (100) + CLEAR_DELAY (100) = 200 total from this point
223
+ jest.advanceTimersByTime(199)
224
+ expect(mapStatusRef.current.textContent).toBe('')
225
+
226
+ jest.advanceTimersByTime(1)
227
+ expect(mapStatusRef.current.textContent).toBe('Ready')
228
+ })
229
+
230
+ it('adds no extra delay once the grace window has fully elapsed', () => {
231
+ jest.advanceTimersByTime(GRACE)
232
+ announce('Later', 'ambient')
233
+
234
+ jest.advanceTimersByTime(100) // normal CLEAR_DELAY only
235
+ expect(mapStatusRef.current.textContent).toBe('Later')
236
+ })
237
+
238
+ it('defaults to a 1000ms grace window when not configured', () => {
239
+ const defaultAnnounce = createAnnouncer(mapStatusRef)
240
+ defaultAnnounce('Ready', 'ambient')
241
+
242
+ // 1000ms grace + 100ms CLEAR_DELAY = 1100 total
243
+ jest.advanceTimersByTime(1099)
244
+ expect(mapStatusRef.current.textContent).toBe('')
245
+
246
+ jest.advanceTimersByTime(1)
247
+ expect(mapStatusRef.current.textContent).toBe('Ready')
248
+ })
249
+ })
@@ -25,6 +25,9 @@ export function createHints (announce) {
25
25
  clearTimer()
26
26
  current = null
27
27
  notify()
28
+ // Blank the live region so a later identical hint re-announces instead of
29
+ // silently netting-out against the stale text left by this dismissal.
30
+ announce.clear?.()
28
31
  }
29
32
 
30
33
  const show = (html, options = {}) => {
@@ -85,6 +85,25 @@ describe('dismiss', () => {
85
85
  it('is safe to call when no hint is active', () => {
86
86
  expect(() => hints.dismiss()).not.toThrow()
87
87
  })
88
+
89
+ it('blanks the live region via announce.clear so a later identical hint re-announces', () => {
90
+ announce.clear = jest.fn()
91
+ hints.show('hello')
92
+ hints.dismiss()
93
+ expect(announce.clear).toHaveBeenCalled()
94
+ })
95
+
96
+ it('is safe to call when announce has no clear method', () => {
97
+ hints.show('hello')
98
+ expect(() => hints.dismiss()).not.toThrow()
99
+ })
100
+
101
+ it('blanks the live region on auto-dismiss too, not just explicit dismiss', () => {
102
+ announce.clear = jest.fn()
103
+ hints.show('hello', { duration: 3000 })
104
+ jest.advanceTimersByTime(3000)
105
+ expect(announce.clear).toHaveBeenCalled()
106
+ })
88
107
  })
89
108
 
90
109
  // ─── subscribe / unsubscribe ──────────────────────────────────────────────────
package/src/types.js CHANGED
@@ -525,6 +525,18 @@
525
525
  *
526
526
  */
527
527
 
528
+ /**
529
+ * Options for a toast hint shown via `showHint()`.
530
+ *
531
+ * @typedef {Object} HintOptions
532
+ *
533
+ * @property {number} [duration=4000]
534
+ * Auto-dismiss delay in milliseconds. Pass `0` to persist until `dismissHint()` is called.
535
+ *
536
+ * @property {string} [announce]
537
+ * Plain-text override for the screen-reader announcement. Defaults to `text` with any HTML tags stripped.
538
+ */
539
+
528
540
  /**
529
541
  * Defines a panel that can be rendered in the UI at various breakpoints.
530
542
  *
@@ -59,7 +59,11 @@ function createInterfaceDetector () {
59
59
  }
60
60
 
61
61
  const handleKeyDown = e => {
62
- if (e.key === 'Tab') {
62
+ // Recognize keyboard mode from Tab (explicit focus), arrow keys (navigation),
63
+ // Enter (confirmation), or other significant keys. This allows the interface type
64
+ // to update even when keyboard input happens during drawing (where focus is on map).
65
+ const keyboardModeKeys = new Set(['Tab', 'Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Escape'])
66
+ if (keyboardModeKeys.has(e.key)) {
63
67
  notifyListeners('keyboard')
64
68
  }
65
69
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Detects whether the current platform is macOS.
3
+ *
4
+ * Prefers the modern User-Agent Client Hints API and falls back to the legacy
5
+ * `navigator.platform`. Guarded so it is safe to call in non-browser
6
+ * environments (Node/SSR/tests), where it returns `false`.
7
+ *
8
+ * @returns {boolean} True when running on macOS.
9
+ */
10
+ export const isMac = () => {
11
+ if (typeof navigator === 'undefined') {
12
+ return false
13
+ }
14
+ return /mac/i.test(navigator.userAgentData?.platform || navigator.platform || '')
15
+ }
@@ -0,0 +1,37 @@
1
+ import { isMac } from './isMac.js'
2
+
3
+ describe('isMac', () => {
4
+ const original = Object.getOwnPropertyDescriptor(global, 'navigator')
5
+
6
+ afterEach(() => {
7
+ if (original) {
8
+ Object.defineProperty(global, 'navigator', original)
9
+ } else {
10
+ delete global.navigator
11
+ }
12
+ })
13
+
14
+ const setNavigator = (value) => {
15
+ Object.defineProperty(global, 'navigator', { value, configurable: true, writable: true })
16
+ }
17
+
18
+ test('returns false when navigator is undefined', () => {
19
+ setNavigator(undefined)
20
+ expect(isMac()).toBe(false)
21
+ })
22
+
23
+ test('detects macOS via userAgentData.platform', () => {
24
+ setNavigator({ userAgentData: { platform: 'macOS' }, platform: '' })
25
+ expect(isMac()).toBe(true)
26
+ })
27
+
28
+ test('falls back to navigator.platform', () => {
29
+ setNavigator({ platform: 'MacIntel' })
30
+ expect(isMac()).toBe(true)
31
+ })
32
+
33
+ test('returns false on non-mac platforms', () => {
34
+ setNavigator({ userAgentData: { platform: 'Windows' }, platform: 'Win32' })
35
+ expect(isMac()).toBe(false)
36
+ })
37
+ })
package/webpack.umd.mjs CHANGED
@@ -146,6 +146,7 @@ const ALL_BUILDS = [
146
146
  { entryPath: './plugins/interact/src/index.js', libraryPath: 'interactPlugin', outDir: 'plugins/interact/dist/umd' },
147
147
  { entryPath: './plugins/datasets/src/index.js', libraryPath: 'datasetsPlugin', outDir: 'plugins/datasets/dist/umd', cssOutDir: 'plugins/datasets/dist' },
148
148
  { entryPath: './plugins/beta/map-styles/src/index.js', libraryPath: 'mapStylesPlugin', outDir: 'plugins/beta/map-styles/dist/umd' },
149
+ { entryPath: './plugins/draw/src/index.js', libraryPath: 'drawPlugin', outDir: 'plugins/draw/dist/umd' },
149
150
  { entryPath: './plugins/beta/draw-ml/src/index.js', libraryPath: 'drawMLPlugin', outDir: 'plugins/beta/draw-ml/dist/umd' },
150
151
  { entryPath: './plugins/beta/frame/src/index.js', libraryPath: 'framePlugin', outDir: 'plugins/beta/frame/dist/umd' }
151
152
  ]