svelte-dnd-action 0.9.76 → 0.9.78

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/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # SVELTE DND ACTION [![Known Vulnerabilities](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action/badge.svg?targetFile=package.json)](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action?targetFile=package.json)
2
2
 
3
- > Shameless plug: If you are making AI Apps check out my brand-new, open source [dev tool](https://github.com/pragmaticfish/sushify)
4
-
5
3
  This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case, any input device and is fully accessible. <br />
6
4
  It requires very minimal configuration, while offering a rich set of primitives that allow overriding basically any of its default behaviours (using the handler functions). <br /><br />
7
5
  See full features list below. <br />
@@ -118,7 +116,7 @@ An options-object with the following attributes:
118
116
  | `dropTargetStyle` | Object&lt;String&gt; | No | `{outline: 'rgba(255, 255, 102, 0.7) solid 2px'}` | An object of styles to apply to the dnd-zone when items can be dragged into it. Note: the styles override any inline styles applied to the dnd-zone. When the styles are removed, any original inline styles will be lost |
119
117
  | `dropTargetClasses`| Array&lt;String&gt; | No | `[]` | A list of classes to apply to the dnd-zone when items can be dragged into it. Note: make sure the classes you use are global. |
120
118
  | `transformDraggedElement` | Function | No | `() => {}` | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. <br />Signature:<br />function(element, data, index) {}<br />**element**: The dragged element. <br />**data**: The data of the item from the items array.<br />**index**: The index the dragged element will become in the new dnd-zone.<br /><br />This allows you to override properties on the dragged element, such as innerHTML to change how it displays. If what you are after is altering styles, do it to the children, not to the dragged element itself |
121
- | `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. |
119
+ | `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. <br />**If you only want to change the wording (ex: to translate it), use `setAriaStrings` instead and leave this off.** |
122
120
  | `centreDraggedOnCursor` | Boolean | No | `false` | Setting it to true will cause elements from this dnd-zone to position their center on the cursor on drag start, effectively turning the cursor to the focal point that triggers all the dnd events (ex: entering another zone). Useful for dnd-zones with large items that can be dragged over small items. |
123
121
  | `useCursorForDetection` | Boolean | No | `false` | Setting it to true will use the cursor position instead of the dragged element's center for drop zone detection. This improves accuracy when dragging large elements over small drop targets. Unlike `centreDraggedOnCursor`, this option does not reposition the dragged element. |
124
122
  | `dropAnimationDisabled` | Boolean | No | `false` | Setting it to true will disable the animation of the dropped element to its final place. |
@@ -167,13 +165,74 @@ If you don't provide the aria-labels everything will still work, but the message
167
165
  _Note_: in general you probably want to use semantic-html (ex: `ol` and `li` elements rather than `section` and `div`) but the library is screen readers friendly regardless (or at least that's the goal :)).
168
166
  If you want to implement your own custom screen-reader alerts, roles and instructions, you can use the `autoAriaDisabled` options and wire everything up yourself using markup and the `consider` and `finalize` handlers (for example: [unsortable list](https://svelte.dev/playground/e020ea1051dc4ae3ac2b697064f234bc?version=3)).
169
167
 
168
+ #### Choosing the keyboard drag trigger
169
+
170
+ By default both _Space_ and _Enter_ start and stop a keyboard drag. If your app needs one of
171
+ those keys for something else - ex: _Enter_ to open or edit the focused item - narrow the trigger:
172
+
173
+ ```javascript
174
+ import {setKeyboardDragTrigger} from "svelte-dnd-action";
175
+
176
+ setKeyboardDragTrigger("space");
177
+ ```
178
+
179
+ Accepted values are `"space"`, `"enter"` and `"space_or_enter"` (the default). Pass `null` or
180
+ `undefined` (or call with no argument) to restore the default. Any other value throws.
181
+
182
+ Keys outside the trigger are never claimed. _Escape_ still ends a drag under every trigger.
183
+
184
+ The setting is global to the document and applies to every dndzone; different zones cannot use
185
+ different triggers at the same time. It can be called at any time, including after the zones have
186
+ rendered. The built-in screen-reader instruction follows it automatically.
187
+
188
+ #### Translating the screen-reader messages
189
+
190
+ `autoAriaDisabled` disables all automatically added ARIA attributes, roles, instructions and alerts. If you only want to translate the screen-reader wording, import `setAriaStrings` and leave `autoAriaDisabled` off:
191
+
192
+ ```javascript
193
+ import {setAriaStrings} from "svelte-dnd-action";
194
+
195
+ const instructionsParTouche = {
196
+ space: `Tabulez jusqu'à un élément et appuyez sur espace pour le déplacer`,
197
+ enter: `Tabulez jusqu'à un élément et appuyez sur entrée pour le déplacer`,
198
+ space_or_enter: `Tabulez jusqu'à un élément et appuyez sur espace ou entrée pour le déplacer`
199
+ };
200
+
201
+ setAriaStrings({
202
+ dragStarted: ({itemLabel, zoneLabel, canMoveBetweenZones}) =>
203
+ `Déplacement de ${itemLabel} commencé. Utilisez les flèches pour le déplacer dans la liste ${zoneLabel}` +
204
+ (canMoveBetweenZones ? `, ou tabulez vers une autre liste` : ``),
205
+ movedToPosition: ({itemLabel, zoneLabel, position}) => `${itemLabel} déplacé en position ${position} dans la liste ${zoneLabel}`,
206
+ movedToZoneEnd: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé à la fin de la liste ${zoneLabel}`,
207
+ movedToZoneStart: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé au début de la liste ${zoneLabel}`,
208
+ // The default message omits the destination, but custom messages can include it
209
+ dropped: ({itemLabel, zoneLabel, position, count}) => `${itemLabel} déposé dans ${zoneLabel}, ${position} sur ${count}`,
210
+ zoneActiveInstruction: ({keyboardDragTrigger}) => instructionsParTouche[keyboardDragTrigger],
211
+ zoneDragDisabledInstruction: `Cette liste de glisser-déposer est désactivée`
212
+ });
213
+ ```
214
+
215
+ Every key is optional. Omitted keys use their English defaults. The five announcement keys are formatter
216
+ functions, so translations can control word order and pluralisation. Each formatter receives `itemLabel`
217
+ and `zoneLabel` from the consumer-provided `aria-label` attributes, plus `position` and `count` for the
218
+ item's 1-based position and the number of items in the relevant zone. `dragStarted` also receives
219
+ `canMoveBetweenZones`. Destructure only the fields your wording needs; the built-in English messages do not
220
+ use every field, but custom messages can.
221
+
222
+ `zoneActiveInstruction` takes a string or a formatter receiving the value set through
223
+ [`setKeyboardDragTrigger`](#choosing-the-keyboard-drag-trigger).
224
+
225
+ Call `setAriaStrings` during app-level initialization and again whenever the application locale changes. Each call defines the complete active locale by applying its overrides to the English defaults, so omitted keys return to English rather than retaining values from the previous locale. Existing instruction elements update immediately. Pass `null` to restore all English defaults. Unknown keys and values of the wrong type throw an error.
226
+
227
+ The setting is global to the document and applies to every dndzone. Different zones cannot use different strings at the same time.
228
+
170
229
  ##### Keyboard support
171
230
 
172
231
  - Tab into a dnd container to get a description and instructions
173
- - Tab into an item and press the _Space_/_Enter_ key to enter dragging-mode. The reader will tell the user a drag has started.
232
+ - Tab into an item and press the _Space_/_Enter_ key to enter dragging-mode (see `setKeyboardDragTrigger` to narrow this to one key). The reader will tell the user a drag has started.
174
233
  - Use the _arrow keys_ while in dragging-mode to change the item's position in the list (down and right are the same, up and left are the same). The reader will tell the user about position changes.
175
234
  - Tab to another dnd container while in dragging-mode in order to move the item to it (the item will be moved to it when it gets focus). The reader will tell the user that item was added to the new list.
176
- - Press _Space_/_Enter_ key while focused on an item, or the _Escape_ key anywhere to exit dragging mode. The reader will tell the user that they are no longer dragging.
235
+ - Press the drag-trigger key while focused on an item, or the _Escape_ key anywhere to exit dragging mode. The reader will tell the user that they are no longer dragging.
177
236
  - Clicking on another item while in drag mode will make it the new drag target. Clicking outside of any draggable will exit dragging-mode (and tell the user)
178
237
  - Mouse drag and drop can be preformed independently of keyboard dragging (as in an item can be dragged with the mouse while in or out of keyboard initiated dragging-mode)
179
238
  - Keyboard drag uses the same `consider` (only on drag start) and `finalize` (every time the item is moved) events but share only some of the `TRIGGERS`. The same handlers should work fine for both.
package/dist/index.d.ts CHANGED
@@ -82,6 +82,36 @@ export interface DndZoneAttributes<T> {
82
82
  */
83
83
  export declare function alertToScreenReader(txt: string): void;
84
84
 
85
+ // Every message key receives the same core context - itemLabel, zoneLabel, position, count -
86
+ // so a consumer can word any of them positionally. Only dragStarted carries an extra.
87
+ export interface AriaStrings {
88
+ dragStarted?: (ctx: {itemLabel: string; zoneLabel: string; position: number; count: number; canMoveBetweenZones: boolean}) => string;
89
+ movedToPosition?: (ctx: {itemLabel: string; zoneLabel: string; position: number; count: number}) => string;
90
+ movedToZoneEnd?: (ctx: {itemLabel: string; zoneLabel: string; position: number; count: number}) => string;
91
+ movedToZoneStart?: (ctx: {itemLabel: string; zoneLabel: string; position: number; count: number}) => string;
92
+ dropped?: (ctx: {itemLabel: string; zoneLabel: string; position: number; count: number}) => string;
93
+ zoneActiveInstruction?: string | ((ctx: {keyboardDragTrigger: "space" | "enter" | "space_or_enter"}) => string);
94
+ zoneDragDisabledInstruction?: string;
95
+ }
96
+
97
+ /**
98
+ * Overrides the strings the library speaks to screen readers. Merges over the built-in English defaults,
99
+ * so each call describes a whole locale rather than patching the previous one - anything a call leaves
100
+ * out goes back to English.
101
+ * This is global and applies to all dndzones.
102
+ * Pass null to restore the built-in English strings.
103
+ */
104
+ export declare function setAriaStrings(overrides: AriaStrings | null): void;
105
+
106
+ /**
107
+ * Chooses which key(s) start and stop a keyboard drag on a focused item. Keys outside the trigger are left
108
+ * completely untouched by the library, so the app can use them - ex: "space" keeps Enter free to activate the
109
+ * focused item. This is global and applies to all dndzones. It can be called at any time.
110
+ * Pass null or undefined (or call with no argument) to restore the default, "space_or_enter".
111
+ * @throws {Error} if given anything other than the documented values, null, or undefined
112
+ */
113
+ export declare function setKeyboardDragTrigger(trigger?: "space" | "enter" | "space_or_enter" | null): void;
114
+
85
115
  /**
86
116
  * Allows using another key instead of "id" in the items data. This is global and applies to all dndzones.
87
117
  * Has to be called when there are no rendered dndzones whatsoever.
package/dist/index.js CHANGED
@@ -293,6 +293,24 @@
293
293
  POINTER: "pointer",
294
294
  KEYBOARD: "keyboard"
295
295
  };
296
+
297
+ // Maps each valid global keyboardDragTrigger value (see setKeyboardDragTrigger) to the
298
+ // KeyboardEvent.key values it covers. Keys that are not part of the active trigger are left
299
+ // completely untouched by the library.
300
+ var KEYBOARD_DRAG_TRIGGER_KEYS = {
301
+ space: [" "],
302
+ enter: ["Enter"],
303
+ space_or_enter: [" ", "Enter"]
304
+ };
305
+ var DEFAULT_KEYBOARD_DRAG_TRIGGER = "space_or_enter";
306
+
307
+ // Human-readable phrase for each trigger, used by the default screen-reader instruction (see
308
+ // src/helpers/aria.js). Must have exactly the same keys as KEYBOARD_DRAG_TRIGGER_KEYS above.
309
+ var KEYBOARD_DRAG_TRIGGER_PHRASES = {
310
+ space: "space-bar",
311
+ enter: "enter",
312
+ space_or_enter: "space-bar or enter"
313
+ };
296
314
  var SHADOW_ITEM_MARKER_PROPERTY_NAME = "isDndShadowItem";
297
315
  var SHADOW_ELEMENT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-internal";
298
316
  var SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-hint";
@@ -2207,12 +2225,54 @@
2207
2225
  };
2208
2226
  }
2209
2227
 
2210
- var _ID_TO_INSTRUCTION;
2228
+ var _INSTRUCTION_ID_TO_ST;
2211
2229
  var INSTRUCTION_IDs$1 = {
2212
2230
  DND_ZONE_ACTIVE: "dnd-zone-active",
2213
2231
  DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled"
2214
2232
  };
2215
- var ID_TO_INSTRUCTION = (_ID_TO_INSTRUCTION = {}, _defineProperty(_ID_TO_INSTRUCTION, INSTRUCTION_IDs$1.DND_ZONE_ACTIVE, "Tab to one the items and press space-bar or enter to start dragging it"), _defineProperty(_ID_TO_INSTRUCTION, INSTRUCTION_IDs$1.DND_ZONE_DRAG_DISABLED, "This is a disabled drag and drop list"), _ID_TO_INSTRUCTION);
2233
+ var INSTRUCTION_ID_TO_STRING_KEY = (_INSTRUCTION_ID_TO_ST = {}, _defineProperty(_INSTRUCTION_ID_TO_ST, INSTRUCTION_IDs$1.DND_ZONE_ACTIVE, "zoneActiveInstruction"), _defineProperty(_INSTRUCTION_ID_TO_ST, INSTRUCTION_IDs$1.DND_ZONE_DRAG_DISABLED, "zoneDragDisabledInstruction"), _INSTRUCTION_ID_TO_ST);
2234
+ var DEFAULT_ARIA_STRINGS = {
2235
+ dragStarted: function dragStarted(_ref) {
2236
+ var itemLabel = _ref.itemLabel,
2237
+ zoneLabel = _ref.zoneLabel,
2238
+ canMoveBetweenZones = _ref.canMoveBetweenZones;
2239
+ return "Started dragging item ".concat(itemLabel, ". Use the arrow keys to move it within its list ").concat(zoneLabel) + (canMoveBetweenZones ? ", or tab to another list in order to move the item into it" : "");
2240
+ },
2241
+ movedToPosition: function movedToPosition(_ref2) {
2242
+ var itemLabel = _ref2.itemLabel,
2243
+ zoneLabel = _ref2.zoneLabel,
2244
+ position = _ref2.position;
2245
+ return "Moved item ".concat(itemLabel, " to position ").concat(position, " in the list ").concat(zoneLabel);
2246
+ },
2247
+ movedToZoneEnd: function movedToZoneEnd(_ref3) {
2248
+ var itemLabel = _ref3.itemLabel,
2249
+ zoneLabel = _ref3.zoneLabel;
2250
+ return "Moved item ".concat(itemLabel, " to the end of the list ").concat(zoneLabel);
2251
+ },
2252
+ movedToZoneStart: function movedToZoneStart(_ref4) {
2253
+ var itemLabel = _ref4.itemLabel,
2254
+ zoneLabel = _ref4.zoneLabel;
2255
+ return "Moved item ".concat(itemLabel, " to the beginning of the list ").concat(zoneLabel);
2256
+ },
2257
+ dropped: function dropped(_ref5) {
2258
+ var itemLabel = _ref5.itemLabel;
2259
+ return "Stopped dragging item ".concat(itemLabel);
2260
+ },
2261
+ zoneActiveInstruction: function zoneActiveInstruction(_ref6) {
2262
+ var keyboardDragTrigger = _ref6.keyboardDragTrigger;
2263
+ return "Tab to one the items and press ".concat(KEYBOARD_DRAG_TRIGGER_PHRASES[keyboardDragTrigger], " to start dragging it");
2264
+ },
2265
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
2266
+ };
2267
+ var FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
2268
+ // zoneActiveInstruction names the key that starts a drag, so a translation may need to vary it with the trigger.
2269
+ var STRING_OR_FUNCTION_ARIA_STRING_KEYS = ["zoneActiveInstruction"];
2270
+ // zoneDragDisabledInstruction has no drag to start and thus no key to name, so it stays string-only.
2271
+ var STRING_ONLY_ARIA_STRING_KEYS = ["zoneDragDisabledInstruction"];
2272
+ var ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2273
+ var instructionCtx = {
2274
+ keyboardDragTrigger: DEFAULT_KEYBOARD_DRAG_TRIGGER
2275
+ };
2216
2276
  var ALERT_DIV_ID = "dnd-action-aria-alert";
2217
2277
  var alertsDiv;
2218
2278
  function initAriaOnBrowser() {
@@ -2238,11 +2298,11 @@
2238
2298
  document.body.prepend(alertsDiv);
2239
2299
 
2240
2300
  // setting the instructions
2241
- Object.entries(ID_TO_INSTRUCTION).forEach(function (_ref) {
2242
- var _ref2 = _slicedToArray(_ref, 2),
2243
- id = _ref2[0],
2244
- txt = _ref2[1];
2245
- return document.body.prepend(instructionToHiddenDiv(id, txt));
2301
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref7) {
2302
+ var _ref8 = _slicedToArray(_ref7, 2),
2303
+ id = _ref8[0],
2304
+ key = _ref8[1];
2305
+ return document.body.prepend(instructionToHiddenDiv(id, formatWithFallback(key, instructionCtx)));
2246
2306
  });
2247
2307
  }
2248
2308
 
@@ -2265,7 +2325,7 @@
2265
2325
  */
2266
2326
  function destroyAria() {
2267
2327
  if (isOnServer || !alertsDiv) return;
2268
- Object.keys(ID_TO_INSTRUCTION).forEach(function (id) {
2328
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (id) {
2269
2329
  var _document$getElementB;
2270
2330
  return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.remove();
2271
2331
  });
@@ -2275,12 +2335,18 @@
2275
2335
  function instructionToHiddenDiv(id, txt) {
2276
2336
  var div = document.createElement("div");
2277
2337
  div.id = id;
2278
- div.innerHTML = "<p>".concat(txt, "</p>");
2338
+ renderInstruction(div, txt);
2279
2339
  div.style.display = "none";
2280
2340
  div.style.position = "fixed";
2281
2341
  div.style.zIndex = "-5";
2282
2342
  return div;
2283
2343
  }
2344
+ function renderInstruction(div, txt) {
2345
+ div.replaceChildren();
2346
+ var paragraph = document.createElement("p");
2347
+ paragraph.textContent = txt;
2348
+ div.appendChild(paragraph);
2349
+ }
2284
2350
 
2285
2351
  /**
2286
2352
  * Will make the screen reader alert the provided text to the user
@@ -2299,6 +2365,133 @@
2299
2365
  alertsDiv.style.display = "inline";
2300
2366
  }
2301
2367
 
2368
+ /**
2369
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
2370
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
2371
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
2372
+ * Pass null to restore the built-in English strings.
2373
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
2374
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
2375
+ * zoneActiveInstruction (a string, or a function taking {keyboardDragTrigger} and returning a string -
2376
+ * useful because a hard-coded translation can't name the right key if the app varies the trigger);
2377
+ * zoneDragDisabledInstruction (a string only - a disabled zone has no drag to start, so there is no key
2378
+ * for a formatter to name)
2379
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
2380
+ * the wrong type. Validation completes before the active strings change.
2381
+ */
2382
+ function setAriaStrings(overrides) {
2383
+ if (overrides === null || overrides === undefined) {
2384
+ ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2385
+ } else {
2386
+ if (_typeof(overrides) !== "object" || Array.isArray(overrides)) {
2387
+ throw new Error("setAriaStrings expects an object or null but instead got a ".concat(_typeof(overrides), ", ").concat(toString(overrides)));
2388
+ }
2389
+ Object.keys(overrides).forEach(function (key) {
2390
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
2391
+ throw new Error("Can't set non existing aria string ".concat(key, "! Supported strings: ").concat(Object.keys(DEFAULT_ARIA_STRINGS)));
2392
+ }
2393
+ var value = overrides[key];
2394
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
2395
+ throw new Error("".concat(key, " should be a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2396
+ }
2397
+ if (STRING_OR_FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "string" && typeof value !== "function") {
2398
+ throw new Error("".concat(key, " should be a string or a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2399
+ }
2400
+ if (STRING_ONLY_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
2401
+ throw new Error("".concat(key, " should be a string but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2402
+ }
2403
+ });
2404
+ ariaStrings = _objectSpread2(_objectSpread2({}, DEFAULT_ARIA_STRINGS), overrides);
2405
+ }
2406
+ refreshInstructions();
2407
+ }
2408
+ function formatAriaString(strings, key, ctx) {
2409
+ var ariaString = strings[key];
2410
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
2411
+ }
2412
+ function formatWithFallback(key, ctx) {
2413
+ try {
2414
+ return formatAriaString(ariaStrings, key, ctx);
2415
+ } catch (err) {
2416
+ printDebug(function () {
2417
+ return ["aria string formatter for \"".concat(key, "\" threw, falling back to the default"), err];
2418
+ });
2419
+ try {
2420
+ return formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
2421
+ } catch (defaultErr) {
2422
+ printDebug(function () {
2423
+ return ["default aria string formatter for \"".concat(key, "\" also threw"), defaultErr];
2424
+ });
2425
+ return "";
2426
+ }
2427
+ }
2428
+ }
2429
+ function refreshInstructions() {
2430
+ if (isOnServer) return;
2431
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref9) {
2432
+ var _ref10 = _slicedToArray(_ref9, 2),
2433
+ id = _ref10[0],
2434
+ key = _ref10[1];
2435
+ var div = document.getElementById(id);
2436
+ if (div) renderInstruction(div, formatWithFallback(key, instructionCtx));
2437
+ });
2438
+ }
2439
+
2440
+ /**
2441
+ * Sets the context the static instruction formatters receive, and re-renders them. Internal - the
2442
+ * public entry point is setKeyboardDragTrigger.
2443
+ * @param {{keyboardDragTrigger: "space"|"enter"|"space_or_enter"}} ctx
2444
+ */
2445
+ function setInstructionContext(ctx) {
2446
+ instructionCtx = _objectSpread2({}, ctx);
2447
+ refreshInstructions();
2448
+ }
2449
+
2450
+ /**
2451
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
2452
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
2453
+ * escape because this function runs during the drag lifecycle.
2454
+ * @param {string} key - one of the keys accepted by setAriaStrings
2455
+ * @param {Object} [ctx] - the interpolation context for that key
2456
+ */
2457
+ function announceToScreenReader(key, ctx) {
2458
+ alertToScreenReader(formatWithFallback(key, ctx));
2459
+ }
2460
+
2461
+ var activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
2462
+
2463
+ /**
2464
+ * Chooses which key(s) start and stop a keyboard drag on a focused item. Keys outside the trigger are
2465
+ * left completely untouched by the library (no preventDefault, no stopPropagation), so the app can use
2466
+ * them - ex: "space" keeps Enter free to activate the focused item.
2467
+ * This setting is global to the document and applies to every dndzone. It can be called at any time.
2468
+ * Pass null or undefined (or call with no argument) to restore the default, "space_or_enter".
2469
+ * @param {"space"|"enter"|"space_or_enter"|null|undefined} [trigger]
2470
+ * @throws {Error} if given anything other than the documented values, null, or undefined
2471
+ */
2472
+ function setKeyboardDragTrigger(trigger) {
2473
+ if (trigger === null || trigger === undefined) {
2474
+ activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
2475
+ } else if (typeof trigger !== "string" || !Object.prototype.hasOwnProperty.call(KEYBOARD_DRAG_TRIGGER_KEYS, trigger)) {
2476
+ throw new Error("setKeyboardDragTrigger expects one of ".concat(Object.keys(KEYBOARD_DRAG_TRIGGER_KEYS).map(function (key) {
2477
+ return "\"".concat(key, "\"");
2478
+ }).join(", "), " or null but instead got a ").concat(_typeof(trigger), ", ").concat(toString(trigger)));
2479
+ } else {
2480
+ activeTrigger = trigger;
2481
+ }
2482
+ setInstructionContext({
2483
+ keyboardDragTrigger: activeTrigger
2484
+ });
2485
+ }
2486
+
2487
+ /**
2488
+ * @param {string} key - a KeyboardEvent.key value
2489
+ * @return {boolean} whether this key starts/stops a drag under the active trigger
2490
+ */
2491
+ function isKeyboardDragTriggerKey(key) {
2492
+ return KEYBOARD_DRAG_TRIGGER_KEYS[activeTrigger].includes(key);
2493
+ }
2494
+
2302
2495
  var DEFAULT_DROP_ZONE_TYPE = "--any--";
2303
2496
  var DEFAULT_DROP_TARGET_STYLE = {
2304
2497
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
@@ -2428,12 +2621,22 @@
2428
2621
  if (newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left) {
2429
2622
  targetItems.push(itemToMove);
2430
2623
  if (!autoAriaDisabled) {
2431
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the end of the list ").concat(focusedDzLabel));
2624
+ announceToScreenReader("movedToZoneEnd", {
2625
+ itemLabel: focusedItemLabel,
2626
+ zoneLabel: focusedDzLabel,
2627
+ position: targetItems.length,
2628
+ count: targetItems.length
2629
+ });
2432
2630
  }
2433
2631
  } else {
2434
2632
  targetItems.unshift(itemToMove);
2435
2633
  if (!autoAriaDisabled) {
2436
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the beginning of the list ").concat(focusedDzLabel));
2634
+ announceToScreenReader("movedToZoneStart", {
2635
+ itemLabel: focusedItemLabel,
2636
+ zoneLabel: focusedDzLabel,
2637
+ position: 1,
2638
+ count: targetItems.length
2639
+ });
2437
2640
  }
2438
2641
  }
2439
2642
  var dzFrom = focusedDz;
@@ -2470,7 +2673,17 @@
2470
2673
  var droppedItemType = draggedItemType;
2471
2674
  if (!droppedConfig) return;
2472
2675
  if (!droppedConfig.autoAriaDisabled) {
2473
- alertToScreenReader("Stopped dragging item ".concat(focusedItemLabel));
2676
+ // Include the destination and final position so localized messages can describe the completed drop.
2677
+ var droppedItems = droppedConfig.items;
2678
+ var droppedIdx = droppedItems.findIndex(function (item) {
2679
+ return item[ITEM_ID_KEY] === droppedItemId;
2680
+ });
2681
+ announceToScreenReader("dropped", {
2682
+ itemLabel: focusedItemLabel,
2683
+ zoneLabel: focusedDzLabel,
2684
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
2685
+ count: droppedItems.length
2686
+ });
2474
2687
  }
2475
2688
  if (allDragTargets.has(document.activeElement)) {
2476
2689
  document.activeElement.blur();
@@ -2527,6 +2740,10 @@
2527
2740
  case "Enter":
2528
2741
  case " ":
2529
2742
  {
2743
+ // keys outside the configured trigger belong to the consumer - don't claim them in any way
2744
+ if (!isKeyboardDragTriggerKey(e.key)) {
2745
+ return;
2746
+ }
2530
2747
  // we don't want to affect nested input elements or clickable elements
2531
2748
  if ((e.target.disabled !== undefined || e.target.href || e.target.isContentEditable) && !allDragTargets.has(e.target)) {
2532
2749
  return;
@@ -2557,7 +2774,12 @@
2557
2774
  });
2558
2775
  if (idx < children.length - 1) {
2559
2776
  if (!config.autoAriaDisabled) {
2560
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(idx + 2, " in the list ").concat(focusedDzLabel));
2777
+ announceToScreenReader("movedToPosition", {
2778
+ itemLabel: focusedItemLabel,
2779
+ zoneLabel: focusedDzLabel,
2780
+ position: idx + 2,
2781
+ count: items.length
2782
+ });
2561
2783
  }
2562
2784
  swap(items, idx, idx + 1);
2563
2785
  dispatchFinalizeEvent(node, items, {
@@ -2583,7 +2805,12 @@
2583
2805
  });
2584
2806
  if (_idx > 0) {
2585
2807
  if (!config.autoAriaDisabled) {
2586
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(_idx, " in the list ").concat(focusedDzLabel));
2808
+ announceToScreenReader("movedToPosition", {
2809
+ itemLabel: focusedItemLabel,
2810
+ zoneLabel: focusedDzLabel,
2811
+ position: _idx,
2812
+ count: _items.length
2813
+ });
2587
2814
  }
2588
2815
  swap(_items, _idx, _idx - 1);
2589
2816
  dispatchFinalizeEvent(node, _items, {
@@ -2614,11 +2841,18 @@
2614
2841
  return dzToConfig.get(dz).dropTargetClasses;
2615
2842
  });
2616
2843
  if (!config.autoAriaDisabled) {
2617
- var msg = "Started dragging item ".concat(focusedItemLabel, ". Use the arrow keys to move it within its list ").concat(focusedDzLabel);
2618
- if (dropTargets.length > 1) {
2619
- msg += ", or tab to another list in order to move the item into it";
2620
- }
2621
- alertToScreenReader(msg);
2844
+ // Include the starting position so localized messages can describe where the item was picked up.
2845
+ var startItems = dzToConfig.get(node).items;
2846
+ var startIdx = startItems.findIndex(function (item) {
2847
+ return item[ITEM_ID_KEY] === focusedItemId;
2848
+ });
2849
+ announceToScreenReader("dragStarted", {
2850
+ itemLabel: focusedItemLabel,
2851
+ zoneLabel: focusedDzLabel,
2852
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
2853
+ count: startItems.length,
2854
+ canMoveBetweenZones: dropTargets.length > 1
2855
+ });
2622
2856
  }
2623
2857
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {
2624
2858
  trigger: TRIGGERS.DRAG_STARTED,
@@ -2995,7 +3229,7 @@
2995
3229
  window.addEventListener("touchend", resetStartDrag);
2996
3230
  }
2997
3231
  function handleKeyDown(e) {
2998
- if (e.key === "Enter" || e.key === " ") isItemsDragDisabled.set(false);
3232
+ if (isKeyboardDragTriggerKey(e.key)) isItemsDragDisabled.set(false);
2999
3233
  }
3000
3234
  function resetStartDrag() {
3001
3235
  isItemsDragDisabled.set(true);
@@ -3043,8 +3277,10 @@
3043
3277
  exports.dragHandle = dragHandle;
3044
3278
  exports.dragHandleZone = dragHandleZone;
3045
3279
  exports.overrideItemIdKeyNameBeforeInitialisingDndZones = overrideItemIdKeyNameBeforeInitialisingDndZones;
3280
+ exports.setAriaStrings = setAriaStrings;
3046
3281
  exports.setDebugMode = setDebugMode;
3047
3282
  exports.setFeatureFlag = setFeatureFlag;
3283
+ exports.setKeyboardDragTrigger = setKeyboardDragTrigger;
3048
3284
 
3049
3285
  Object.defineProperty(exports, '__esModule', { value: true });
3050
3286
 
package/dist/index.mjs CHANGED
@@ -287,6 +287,24 @@ var SOURCES = {
287
287
  POINTER: "pointer",
288
288
  KEYBOARD: "keyboard"
289
289
  };
290
+
291
+ // Maps each valid global keyboardDragTrigger value (see setKeyboardDragTrigger) to the
292
+ // KeyboardEvent.key values it covers. Keys that are not part of the active trigger are left
293
+ // completely untouched by the library.
294
+ var KEYBOARD_DRAG_TRIGGER_KEYS = {
295
+ space: [" "],
296
+ enter: ["Enter"],
297
+ space_or_enter: [" ", "Enter"]
298
+ };
299
+ var DEFAULT_KEYBOARD_DRAG_TRIGGER = "space_or_enter";
300
+
301
+ // Human-readable phrase for each trigger, used by the default screen-reader instruction (see
302
+ // src/helpers/aria.js). Must have exactly the same keys as KEYBOARD_DRAG_TRIGGER_KEYS above.
303
+ var KEYBOARD_DRAG_TRIGGER_PHRASES = {
304
+ space: "space-bar",
305
+ enter: "enter",
306
+ space_or_enter: "space-bar or enter"
307
+ };
290
308
  var SHADOW_ITEM_MARKER_PROPERTY_NAME = "isDndShadowItem";
291
309
  var SHADOW_ELEMENT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-internal";
292
310
  var SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-hint";
@@ -2201,12 +2219,54 @@ function dndzone$2(node, options) {
2201
2219
  };
2202
2220
  }
2203
2221
 
2204
- var _ID_TO_INSTRUCTION;
2222
+ var _INSTRUCTION_ID_TO_ST;
2205
2223
  var INSTRUCTION_IDs$1 = {
2206
2224
  DND_ZONE_ACTIVE: "dnd-zone-active",
2207
2225
  DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled"
2208
2226
  };
2209
- var ID_TO_INSTRUCTION = (_ID_TO_INSTRUCTION = {}, _defineProperty(_ID_TO_INSTRUCTION, INSTRUCTION_IDs$1.DND_ZONE_ACTIVE, "Tab to one the items and press space-bar or enter to start dragging it"), _defineProperty(_ID_TO_INSTRUCTION, INSTRUCTION_IDs$1.DND_ZONE_DRAG_DISABLED, "This is a disabled drag and drop list"), _ID_TO_INSTRUCTION);
2227
+ var INSTRUCTION_ID_TO_STRING_KEY = (_INSTRUCTION_ID_TO_ST = {}, _defineProperty(_INSTRUCTION_ID_TO_ST, INSTRUCTION_IDs$1.DND_ZONE_ACTIVE, "zoneActiveInstruction"), _defineProperty(_INSTRUCTION_ID_TO_ST, INSTRUCTION_IDs$1.DND_ZONE_DRAG_DISABLED, "zoneDragDisabledInstruction"), _INSTRUCTION_ID_TO_ST);
2228
+ var DEFAULT_ARIA_STRINGS = {
2229
+ dragStarted: function dragStarted(_ref) {
2230
+ var itemLabel = _ref.itemLabel,
2231
+ zoneLabel = _ref.zoneLabel,
2232
+ canMoveBetweenZones = _ref.canMoveBetweenZones;
2233
+ return "Started dragging item ".concat(itemLabel, ". Use the arrow keys to move it within its list ").concat(zoneLabel) + (canMoveBetweenZones ? ", or tab to another list in order to move the item into it" : "");
2234
+ },
2235
+ movedToPosition: function movedToPosition(_ref2) {
2236
+ var itemLabel = _ref2.itemLabel,
2237
+ zoneLabel = _ref2.zoneLabel,
2238
+ position = _ref2.position;
2239
+ return "Moved item ".concat(itemLabel, " to position ").concat(position, " in the list ").concat(zoneLabel);
2240
+ },
2241
+ movedToZoneEnd: function movedToZoneEnd(_ref3) {
2242
+ var itemLabel = _ref3.itemLabel,
2243
+ zoneLabel = _ref3.zoneLabel;
2244
+ return "Moved item ".concat(itemLabel, " to the end of the list ").concat(zoneLabel);
2245
+ },
2246
+ movedToZoneStart: function movedToZoneStart(_ref4) {
2247
+ var itemLabel = _ref4.itemLabel,
2248
+ zoneLabel = _ref4.zoneLabel;
2249
+ return "Moved item ".concat(itemLabel, " to the beginning of the list ").concat(zoneLabel);
2250
+ },
2251
+ dropped: function dropped(_ref5) {
2252
+ var itemLabel = _ref5.itemLabel;
2253
+ return "Stopped dragging item ".concat(itemLabel);
2254
+ },
2255
+ zoneActiveInstruction: function zoneActiveInstruction(_ref6) {
2256
+ var keyboardDragTrigger = _ref6.keyboardDragTrigger;
2257
+ return "Tab to one the items and press ".concat(KEYBOARD_DRAG_TRIGGER_PHRASES[keyboardDragTrigger], " to start dragging it");
2258
+ },
2259
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
2260
+ };
2261
+ var FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
2262
+ // zoneActiveInstruction names the key that starts a drag, so a translation may need to vary it with the trigger.
2263
+ var STRING_OR_FUNCTION_ARIA_STRING_KEYS = ["zoneActiveInstruction"];
2264
+ // zoneDragDisabledInstruction has no drag to start and thus no key to name, so it stays string-only.
2265
+ var STRING_ONLY_ARIA_STRING_KEYS = ["zoneDragDisabledInstruction"];
2266
+ var ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2267
+ var instructionCtx = {
2268
+ keyboardDragTrigger: DEFAULT_KEYBOARD_DRAG_TRIGGER
2269
+ };
2210
2270
  var ALERT_DIV_ID = "dnd-action-aria-alert";
2211
2271
  var alertsDiv;
2212
2272
  function initAriaOnBrowser() {
@@ -2232,11 +2292,11 @@ function initAriaOnBrowser() {
2232
2292
  document.body.prepend(alertsDiv);
2233
2293
 
2234
2294
  // setting the instructions
2235
- Object.entries(ID_TO_INSTRUCTION).forEach(function (_ref) {
2236
- var _ref2 = _slicedToArray(_ref, 2),
2237
- id = _ref2[0],
2238
- txt = _ref2[1];
2239
- return document.body.prepend(instructionToHiddenDiv(id, txt));
2295
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref7) {
2296
+ var _ref8 = _slicedToArray(_ref7, 2),
2297
+ id = _ref8[0],
2298
+ key = _ref8[1];
2299
+ return document.body.prepend(instructionToHiddenDiv(id, formatWithFallback(key, instructionCtx)));
2240
2300
  });
2241
2301
  }
2242
2302
 
@@ -2259,7 +2319,7 @@ function initAria() {
2259
2319
  */
2260
2320
  function destroyAria() {
2261
2321
  if (isOnServer || !alertsDiv) return;
2262
- Object.keys(ID_TO_INSTRUCTION).forEach(function (id) {
2322
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (id) {
2263
2323
  var _document$getElementB;
2264
2324
  return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.remove();
2265
2325
  });
@@ -2269,12 +2329,18 @@ function destroyAria() {
2269
2329
  function instructionToHiddenDiv(id, txt) {
2270
2330
  var div = document.createElement("div");
2271
2331
  div.id = id;
2272
- div.innerHTML = "<p>".concat(txt, "</p>");
2332
+ renderInstruction(div, txt);
2273
2333
  div.style.display = "none";
2274
2334
  div.style.position = "fixed";
2275
2335
  div.style.zIndex = "-5";
2276
2336
  return div;
2277
2337
  }
2338
+ function renderInstruction(div, txt) {
2339
+ div.replaceChildren();
2340
+ var paragraph = document.createElement("p");
2341
+ paragraph.textContent = txt;
2342
+ div.appendChild(paragraph);
2343
+ }
2278
2344
 
2279
2345
  /**
2280
2346
  * Will make the screen reader alert the provided text to the user
@@ -2293,6 +2359,133 @@ function alertToScreenReader(txt) {
2293
2359
  alertsDiv.style.display = "inline";
2294
2360
  }
2295
2361
 
2362
+ /**
2363
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
2364
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
2365
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
2366
+ * Pass null to restore the built-in English strings.
2367
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
2368
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
2369
+ * zoneActiveInstruction (a string, or a function taking {keyboardDragTrigger} and returning a string -
2370
+ * useful because a hard-coded translation can't name the right key if the app varies the trigger);
2371
+ * zoneDragDisabledInstruction (a string only - a disabled zone has no drag to start, so there is no key
2372
+ * for a formatter to name)
2373
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
2374
+ * the wrong type. Validation completes before the active strings change.
2375
+ */
2376
+ function setAriaStrings(overrides) {
2377
+ if (overrides === null || overrides === undefined) {
2378
+ ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2379
+ } else {
2380
+ if (_typeof(overrides) !== "object" || Array.isArray(overrides)) {
2381
+ throw new Error("setAriaStrings expects an object or null but instead got a ".concat(_typeof(overrides), ", ").concat(toString(overrides)));
2382
+ }
2383
+ Object.keys(overrides).forEach(function (key) {
2384
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
2385
+ throw new Error("Can't set non existing aria string ".concat(key, "! Supported strings: ").concat(Object.keys(DEFAULT_ARIA_STRINGS)));
2386
+ }
2387
+ var value = overrides[key];
2388
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
2389
+ throw new Error("".concat(key, " should be a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2390
+ }
2391
+ if (STRING_OR_FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "string" && typeof value !== "function") {
2392
+ throw new Error("".concat(key, " should be a string or a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2393
+ }
2394
+ if (STRING_ONLY_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
2395
+ throw new Error("".concat(key, " should be a string but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2396
+ }
2397
+ });
2398
+ ariaStrings = _objectSpread2(_objectSpread2({}, DEFAULT_ARIA_STRINGS), overrides);
2399
+ }
2400
+ refreshInstructions();
2401
+ }
2402
+ function formatAriaString(strings, key, ctx) {
2403
+ var ariaString = strings[key];
2404
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
2405
+ }
2406
+ function formatWithFallback(key, ctx) {
2407
+ try {
2408
+ return formatAriaString(ariaStrings, key, ctx);
2409
+ } catch (err) {
2410
+ printDebug(function () {
2411
+ return ["aria string formatter for \"".concat(key, "\" threw, falling back to the default"), err];
2412
+ });
2413
+ try {
2414
+ return formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
2415
+ } catch (defaultErr) {
2416
+ printDebug(function () {
2417
+ return ["default aria string formatter for \"".concat(key, "\" also threw"), defaultErr];
2418
+ });
2419
+ return "";
2420
+ }
2421
+ }
2422
+ }
2423
+ function refreshInstructions() {
2424
+ if (isOnServer) return;
2425
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref9) {
2426
+ var _ref10 = _slicedToArray(_ref9, 2),
2427
+ id = _ref10[0],
2428
+ key = _ref10[1];
2429
+ var div = document.getElementById(id);
2430
+ if (div) renderInstruction(div, formatWithFallback(key, instructionCtx));
2431
+ });
2432
+ }
2433
+
2434
+ /**
2435
+ * Sets the context the static instruction formatters receive, and re-renders them. Internal - the
2436
+ * public entry point is setKeyboardDragTrigger.
2437
+ * @param {{keyboardDragTrigger: "space"|"enter"|"space_or_enter"}} ctx
2438
+ */
2439
+ function setInstructionContext(ctx) {
2440
+ instructionCtx = _objectSpread2({}, ctx);
2441
+ refreshInstructions();
2442
+ }
2443
+
2444
+ /**
2445
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
2446
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
2447
+ * escape because this function runs during the drag lifecycle.
2448
+ * @param {string} key - one of the keys accepted by setAriaStrings
2449
+ * @param {Object} [ctx] - the interpolation context for that key
2450
+ */
2451
+ function announceToScreenReader(key, ctx) {
2452
+ alertToScreenReader(formatWithFallback(key, ctx));
2453
+ }
2454
+
2455
+ var activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
2456
+
2457
+ /**
2458
+ * Chooses which key(s) start and stop a keyboard drag on a focused item. Keys outside the trigger are
2459
+ * left completely untouched by the library (no preventDefault, no stopPropagation), so the app can use
2460
+ * them - ex: "space" keeps Enter free to activate the focused item.
2461
+ * This setting is global to the document and applies to every dndzone. It can be called at any time.
2462
+ * Pass null or undefined (or call with no argument) to restore the default, "space_or_enter".
2463
+ * @param {"space"|"enter"|"space_or_enter"|null|undefined} [trigger]
2464
+ * @throws {Error} if given anything other than the documented values, null, or undefined
2465
+ */
2466
+ function setKeyboardDragTrigger(trigger) {
2467
+ if (trigger === null || trigger === undefined) {
2468
+ activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
2469
+ } else if (typeof trigger !== "string" || !Object.prototype.hasOwnProperty.call(KEYBOARD_DRAG_TRIGGER_KEYS, trigger)) {
2470
+ throw new Error("setKeyboardDragTrigger expects one of ".concat(Object.keys(KEYBOARD_DRAG_TRIGGER_KEYS).map(function (key) {
2471
+ return "\"".concat(key, "\"");
2472
+ }).join(", "), " or null but instead got a ").concat(_typeof(trigger), ", ").concat(toString(trigger)));
2473
+ } else {
2474
+ activeTrigger = trigger;
2475
+ }
2476
+ setInstructionContext({
2477
+ keyboardDragTrigger: activeTrigger
2478
+ });
2479
+ }
2480
+
2481
+ /**
2482
+ * @param {string} key - a KeyboardEvent.key value
2483
+ * @return {boolean} whether this key starts/stops a drag under the active trigger
2484
+ */
2485
+ function isKeyboardDragTriggerKey(key) {
2486
+ return KEYBOARD_DRAG_TRIGGER_KEYS[activeTrigger].includes(key);
2487
+ }
2488
+
2296
2489
  var DEFAULT_DROP_ZONE_TYPE = "--any--";
2297
2490
  var DEFAULT_DROP_TARGET_STYLE = {
2298
2491
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
@@ -2422,12 +2615,22 @@ function handleZoneFocus(e) {
2422
2615
  if (newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left) {
2423
2616
  targetItems.push(itemToMove);
2424
2617
  if (!autoAriaDisabled) {
2425
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the end of the list ").concat(focusedDzLabel));
2618
+ announceToScreenReader("movedToZoneEnd", {
2619
+ itemLabel: focusedItemLabel,
2620
+ zoneLabel: focusedDzLabel,
2621
+ position: targetItems.length,
2622
+ count: targetItems.length
2623
+ });
2426
2624
  }
2427
2625
  } else {
2428
2626
  targetItems.unshift(itemToMove);
2429
2627
  if (!autoAriaDisabled) {
2430
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the beginning of the list ").concat(focusedDzLabel));
2628
+ announceToScreenReader("movedToZoneStart", {
2629
+ itemLabel: focusedItemLabel,
2630
+ zoneLabel: focusedDzLabel,
2631
+ position: 1,
2632
+ count: targetItems.length
2633
+ });
2431
2634
  }
2432
2635
  }
2433
2636
  var dzFrom = focusedDz;
@@ -2464,7 +2667,17 @@ function handleDrop() {
2464
2667
  var droppedItemType = draggedItemType;
2465
2668
  if (!droppedConfig) return;
2466
2669
  if (!droppedConfig.autoAriaDisabled) {
2467
- alertToScreenReader("Stopped dragging item ".concat(focusedItemLabel));
2670
+ // Include the destination and final position so localized messages can describe the completed drop.
2671
+ var droppedItems = droppedConfig.items;
2672
+ var droppedIdx = droppedItems.findIndex(function (item) {
2673
+ return item[ITEM_ID_KEY] === droppedItemId;
2674
+ });
2675
+ announceToScreenReader("dropped", {
2676
+ itemLabel: focusedItemLabel,
2677
+ zoneLabel: focusedDzLabel,
2678
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
2679
+ count: droppedItems.length
2680
+ });
2468
2681
  }
2469
2682
  if (allDragTargets.has(document.activeElement)) {
2470
2683
  document.activeElement.blur();
@@ -2521,6 +2734,10 @@ function dndzone$1(node, options) {
2521
2734
  case "Enter":
2522
2735
  case " ":
2523
2736
  {
2737
+ // keys outside the configured trigger belong to the consumer - don't claim them in any way
2738
+ if (!isKeyboardDragTriggerKey(e.key)) {
2739
+ return;
2740
+ }
2524
2741
  // we don't want to affect nested input elements or clickable elements
2525
2742
  if ((e.target.disabled !== undefined || e.target.href || e.target.isContentEditable) && !allDragTargets.has(e.target)) {
2526
2743
  return;
@@ -2551,7 +2768,12 @@ function dndzone$1(node, options) {
2551
2768
  });
2552
2769
  if (idx < children.length - 1) {
2553
2770
  if (!config.autoAriaDisabled) {
2554
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(idx + 2, " in the list ").concat(focusedDzLabel));
2771
+ announceToScreenReader("movedToPosition", {
2772
+ itemLabel: focusedItemLabel,
2773
+ zoneLabel: focusedDzLabel,
2774
+ position: idx + 2,
2775
+ count: items.length
2776
+ });
2555
2777
  }
2556
2778
  swap(items, idx, idx + 1);
2557
2779
  dispatchFinalizeEvent(node, items, {
@@ -2577,7 +2799,12 @@ function dndzone$1(node, options) {
2577
2799
  });
2578
2800
  if (_idx > 0) {
2579
2801
  if (!config.autoAriaDisabled) {
2580
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(_idx, " in the list ").concat(focusedDzLabel));
2802
+ announceToScreenReader("movedToPosition", {
2803
+ itemLabel: focusedItemLabel,
2804
+ zoneLabel: focusedDzLabel,
2805
+ position: _idx,
2806
+ count: _items.length
2807
+ });
2581
2808
  }
2582
2809
  swap(_items, _idx, _idx - 1);
2583
2810
  dispatchFinalizeEvent(node, _items, {
@@ -2608,11 +2835,18 @@ function dndzone$1(node, options) {
2608
2835
  return dzToConfig.get(dz).dropTargetClasses;
2609
2836
  });
2610
2837
  if (!config.autoAriaDisabled) {
2611
- var msg = "Started dragging item ".concat(focusedItemLabel, ". Use the arrow keys to move it within its list ").concat(focusedDzLabel);
2612
- if (dropTargets.length > 1) {
2613
- msg += ", or tab to another list in order to move the item into it";
2614
- }
2615
- alertToScreenReader(msg);
2838
+ // Include the starting position so localized messages can describe where the item was picked up.
2839
+ var startItems = dzToConfig.get(node).items;
2840
+ var startIdx = startItems.findIndex(function (item) {
2841
+ return item[ITEM_ID_KEY] === focusedItemId;
2842
+ });
2843
+ announceToScreenReader("dragStarted", {
2844
+ itemLabel: focusedItemLabel,
2845
+ zoneLabel: focusedDzLabel,
2846
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
2847
+ count: startItems.length,
2848
+ canMoveBetweenZones: dropTargets.length > 1
2849
+ });
2616
2850
  }
2617
2851
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {
2618
2852
  trigger: TRIGGERS.DRAG_STARTED,
@@ -2989,7 +3223,7 @@ function dragHandle(handle) {
2989
3223
  window.addEventListener("touchend", resetStartDrag);
2990
3224
  }
2991
3225
  function handleKeyDown(e) {
2992
- if (e.key === "Enter" || e.key === " ") isItemsDragDisabled.set(false);
3226
+ if (isKeyboardDragTriggerKey(e.key)) isItemsDragDisabled.set(false);
2993
3227
  }
2994
3228
  function resetStartDrag() {
2995
3229
  isItemsDragDisabled.set(true);
@@ -3026,4 +3260,4 @@ function dragHandle(handle) {
3026
3260
  };
3027
3261
  }
3028
3262
 
3029
- export { DRAGGED_ELEMENT_ID, FEATURE_FLAG_NAMES, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID, SOURCES, TRIGGERS, alertToScreenReader, dndzone, dragHandle, dragHandleZone, overrideItemIdKeyNameBeforeInitialisingDndZones, setDebugMode, setFeatureFlag };
3263
+ export { DRAGGED_ELEMENT_ID, FEATURE_FLAG_NAMES, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID, SOURCES, TRIGGERS, alertToScreenReader, dndzone, dragHandle, dragHandleZone, overrideItemIdKeyNameBeforeInitialisingDndZones, setAriaStrings, setDebugMode, setFeatureFlag, setKeyboardDragTrigger };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-dnd-action",
3
3
  "description": "*An awesome drag and drop library for Svelte 3 and 4 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *",
4
- "version": "0.9.76",
4
+ "version": "0.9.78",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/isaacHagoel/svelte-dnd-action.git"
package/src/constants.js CHANGED
@@ -18,6 +18,24 @@ export const SOURCES = {
18
18
  KEYBOARD: "keyboard"
19
19
  };
20
20
 
21
+ // Maps each valid global keyboardDragTrigger value (see setKeyboardDragTrigger) to the
22
+ // KeyboardEvent.key values it covers. Keys that are not part of the active trigger are left
23
+ // completely untouched by the library.
24
+ export const KEYBOARD_DRAG_TRIGGER_KEYS = {
25
+ space: [" "],
26
+ enter: ["Enter"],
27
+ space_or_enter: [" ", "Enter"]
28
+ };
29
+ export const DEFAULT_KEYBOARD_DRAG_TRIGGER = "space_or_enter";
30
+
31
+ // Human-readable phrase for each trigger, used by the default screen-reader instruction (see
32
+ // src/helpers/aria.js). Must have exactly the same keys as KEYBOARD_DRAG_TRIGGER_KEYS above.
33
+ export const KEYBOARD_DRAG_TRIGGER_PHRASES = {
34
+ space: "space-bar",
35
+ enter: "enter",
36
+ space_or_enter: "space-bar or enter"
37
+ };
38
+
21
39
  export const SHADOW_ITEM_MARKER_PROPERTY_NAME = "isDndShadowItem";
22
40
  export const SHADOW_ELEMENT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-internal";
23
41
  export const SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-hint";
@@ -1,14 +1,37 @@
1
- import {isOnServer} from "../constants";
1
+ import {DEFAULT_KEYBOARD_DRAG_TRIGGER, KEYBOARD_DRAG_TRIGGER_PHRASES, isOnServer, printDebug} from "../constants";
2
+ import {toString} from "./util";
2
3
 
3
4
  const INSTRUCTION_IDs = {
4
5
  DND_ZONE_ACTIVE: "dnd-zone-active",
5
6
  DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled"
6
7
  };
7
- const ID_TO_INSTRUCTION = {
8
- [INSTRUCTION_IDs.DND_ZONE_ACTIVE]: "Tab to one the items and press space-bar or enter to start dragging it",
9
- [INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED]: "This is a disabled drag and drop list"
8
+ const INSTRUCTION_ID_TO_STRING_KEY = {
9
+ [INSTRUCTION_IDs.DND_ZONE_ACTIVE]: "zoneActiveInstruction",
10
+ [INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED]: "zoneDragDisabledInstruction"
10
11
  };
11
12
 
13
+ const DEFAULT_ARIA_STRINGS = {
14
+ dragStarted: ({itemLabel, zoneLabel, canMoveBetweenZones}) =>
15
+ `Started dragging item ${itemLabel}. Use the arrow keys to move it within its list ${zoneLabel}` +
16
+ (canMoveBetweenZones ? ", or tab to another list in order to move the item into it" : ""),
17
+ movedToPosition: ({itemLabel, zoneLabel, position}) => `Moved item ${itemLabel} to position ${position} in the list ${zoneLabel}`,
18
+ movedToZoneEnd: ({itemLabel, zoneLabel}) => `Moved item ${itemLabel} to the end of the list ${zoneLabel}`,
19
+ movedToZoneStart: ({itemLabel, zoneLabel}) => `Moved item ${itemLabel} to the beginning of the list ${zoneLabel}`,
20
+ dropped: ({itemLabel}) => `Stopped dragging item ${itemLabel}`,
21
+ zoneActiveInstruction: ({keyboardDragTrigger}) =>
22
+ `Tab to one the items and press ${KEYBOARD_DRAG_TRIGGER_PHRASES[keyboardDragTrigger]} to start dragging it`,
23
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
24
+ };
25
+
26
+ const FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
27
+ // zoneActiveInstruction names the key that starts a drag, so a translation may need to vary it with the trigger.
28
+ const STRING_OR_FUNCTION_ARIA_STRING_KEYS = ["zoneActiveInstruction"];
29
+ // zoneDragDisabledInstruction has no drag to start and thus no key to name, so it stays string-only.
30
+ const STRING_ONLY_ARIA_STRING_KEYS = ["zoneDragDisabledInstruction"];
31
+
32
+ let ariaStrings = {...DEFAULT_ARIA_STRINGS};
33
+ let instructionCtx = {keyboardDragTrigger: DEFAULT_KEYBOARD_DRAG_TRIGGER};
34
+
12
35
  const ALERT_DIV_ID = "dnd-action-aria-alert";
13
36
  let alertsDiv;
14
37
 
@@ -35,7 +58,9 @@ function initAriaOnBrowser() {
35
58
  document.body.prepend(alertsDiv);
36
59
 
37
60
  // setting the instructions
38
- Object.entries(ID_TO_INSTRUCTION).forEach(([id, txt]) => document.body.prepend(instructionToHiddenDiv(id, txt)));
61
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(([id, key]) =>
62
+ document.body.prepend(instructionToHiddenDiv(id, formatWithFallback(key, instructionCtx)))
63
+ );
39
64
  }
40
65
 
41
66
  /**
@@ -57,7 +82,7 @@ export function initAria() {
57
82
  */
58
83
  export function destroyAria() {
59
84
  if (isOnServer || !alertsDiv) return;
60
- Object.keys(ID_TO_INSTRUCTION).forEach(id => document.getElementById(id)?.remove());
85
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(id => document.getElementById(id)?.remove());
61
86
  alertsDiv.remove();
62
87
  alertsDiv = undefined;
63
88
  }
@@ -65,13 +90,20 @@ export function destroyAria() {
65
90
  function instructionToHiddenDiv(id, txt) {
66
91
  const div = document.createElement("div");
67
92
  div.id = id;
68
- div.innerHTML = `<p>${txt}</p>`;
93
+ renderInstruction(div, txt);
69
94
  div.style.display = "none";
70
95
  div.style.position = "fixed";
71
96
  div.style.zIndex = "-5";
72
97
  return div;
73
98
  }
74
99
 
100
+ function renderInstruction(div, txt) {
101
+ div.replaceChildren();
102
+ const paragraph = document.createElement("p");
103
+ paragraph.textContent = txt;
104
+ div.appendChild(paragraph);
105
+ }
106
+
75
107
  /**
76
108
  * Will make the screen reader alert the provided text to the user
77
109
  * @param {string} txt
@@ -88,3 +120,92 @@ export function alertToScreenReader(txt) {
88
120
  alertsDiv.style.display = "none";
89
121
  alertsDiv.style.display = "inline";
90
122
  }
123
+
124
+ /**
125
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
126
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
127
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
128
+ * Pass null to restore the built-in English strings.
129
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
130
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
131
+ * zoneActiveInstruction (a string, or a function taking {keyboardDragTrigger} and returning a string -
132
+ * useful because a hard-coded translation can't name the right key if the app varies the trigger);
133
+ * zoneDragDisabledInstruction (a string only - a disabled zone has no drag to start, so there is no key
134
+ * for a formatter to name)
135
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
136
+ * the wrong type. Validation completes before the active strings change.
137
+ */
138
+ export function setAriaStrings(overrides) {
139
+ if (overrides === null || overrides === undefined) {
140
+ ariaStrings = {...DEFAULT_ARIA_STRINGS};
141
+ } else {
142
+ if (typeof overrides !== "object" || Array.isArray(overrides)) {
143
+ throw new Error(`setAriaStrings expects an object or null but instead got a ${typeof overrides}, ${toString(overrides)}`);
144
+ }
145
+ Object.keys(overrides).forEach(key => {
146
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
147
+ throw new Error(`Can't set non existing aria string ${key}! Supported strings: ${Object.keys(DEFAULT_ARIA_STRINGS)}`);
148
+ }
149
+ const value = overrides[key];
150
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
151
+ throw new Error(`${key} should be a function but instead it is a ${typeof value}, ${toString(value)}`);
152
+ }
153
+ if (STRING_OR_FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "string" && typeof value !== "function") {
154
+ throw new Error(`${key} should be a string or a function but instead it is a ${typeof value}, ${toString(value)}`);
155
+ }
156
+ if (STRING_ONLY_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
157
+ throw new Error(`${key} should be a string but instead it is a ${typeof value}, ${toString(value)}`);
158
+ }
159
+ });
160
+ ariaStrings = {...DEFAULT_ARIA_STRINGS, ...overrides};
161
+ }
162
+ refreshInstructions();
163
+ }
164
+
165
+ function formatAriaString(strings, key, ctx) {
166
+ const ariaString = strings[key];
167
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
168
+ }
169
+
170
+ function formatWithFallback(key, ctx) {
171
+ try {
172
+ return formatAriaString(ariaStrings, key, ctx);
173
+ } catch (err) {
174
+ printDebug(() => [`aria string formatter for "${key}" threw, falling back to the default`, err]);
175
+ try {
176
+ return formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
177
+ } catch (defaultErr) {
178
+ printDebug(() => [`default aria string formatter for "${key}" also threw`, defaultErr]);
179
+ return "";
180
+ }
181
+ }
182
+ }
183
+
184
+ function refreshInstructions() {
185
+ if (isOnServer) return;
186
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(([id, key]) => {
187
+ const div = document.getElementById(id);
188
+ if (div) renderInstruction(div, formatWithFallback(key, instructionCtx));
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Sets the context the static instruction formatters receive, and re-renders them. Internal - the
194
+ * public entry point is setKeyboardDragTrigger.
195
+ * @param {{keyboardDragTrigger: "space"|"enter"|"space_or_enter"}} ctx
196
+ */
197
+ export function setInstructionContext(ctx) {
198
+ instructionCtx = {...ctx};
199
+ refreshInstructions();
200
+ }
201
+
202
+ /**
203
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
204
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
205
+ * escape because this function runs during the drag lifecycle.
206
+ * @param {string} key - one of the keys accepted by setAriaStrings
207
+ * @param {Object} [ctx] - the interpolation context for that key
208
+ */
209
+ export function announceToScreenReader(key, ctx) {
210
+ alertToScreenReader(formatWithFallback(key, ctx));
211
+ }
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export {dndzone} from "./action.js";
2
2
  export {dragHandleZone, dragHandle} from "./wrappers/withDragHandles";
3
- export {alertToScreenReader} from "./helpers/aria";
3
+ export {alertToScreenReader, setAriaStrings} from "./helpers/aria";
4
+ export {setKeyboardDragTrigger} from "./keyboardDragTrigger";
4
5
  export {
5
6
  TRIGGERS,
6
7
  SOURCES,
@@ -1,7 +1,8 @@
1
1
  import {decrementActiveDropZoneCount, incrementActiveDropZoneCount, ITEM_ID_KEY, SOURCES, TRIGGERS} from "./constants";
2
+ import {isKeyboardDragTriggerKey} from "./keyboardDragTrigger";
2
3
  import {styleActiveDropZones, styleInactiveDropZones} from "./helpers/styler";
3
4
  import {dispatchConsiderEvent, dispatchFinalizeEvent} from "./helpers/dispatcher";
4
- import {initAria, alertToScreenReader, destroyAria} from "./helpers/aria";
5
+ import {initAria, announceToScreenReader, destroyAria} from "./helpers/aria";
5
6
  import {toString} from "./helpers/util";
6
7
  import {printDebug} from "./constants";
7
8
 
@@ -124,12 +125,22 @@ function handleZoneFocus(e) {
124
125
  ) {
125
126
  targetItems.push(itemToMove);
126
127
  if (!autoAriaDisabled) {
127
- alertToScreenReader(`Moved item ${focusedItemLabel} to the end of the list ${focusedDzLabel}`);
128
+ announceToScreenReader("movedToZoneEnd", {
129
+ itemLabel: focusedItemLabel,
130
+ zoneLabel: focusedDzLabel,
131
+ position: targetItems.length,
132
+ count: targetItems.length
133
+ });
128
134
  }
129
135
  } else {
130
136
  targetItems.unshift(itemToMove);
131
137
  if (!autoAriaDisabled) {
132
- alertToScreenReader(`Moved item ${focusedItemLabel} to the beginning of the list ${focusedDzLabel}`);
138
+ announceToScreenReader("movedToZoneStart", {
139
+ itemLabel: focusedItemLabel,
140
+ zoneLabel: focusedDzLabel,
141
+ position: 1,
142
+ count: targetItems.length
143
+ });
133
144
  }
134
145
  }
135
146
  const dzFrom = focusedDz;
@@ -155,7 +166,15 @@ function handleDrop(dispatchConsider = true) {
155
166
  if (!droppedConfig) return;
156
167
 
157
168
  if (!droppedConfig.autoAriaDisabled) {
158
- alertToScreenReader(`Stopped dragging item ${focusedItemLabel}`);
169
+ // Include the destination and final position so localized messages can describe the completed drop.
170
+ const droppedItems = droppedConfig.items;
171
+ const droppedIdx = droppedItems.findIndex(item => item[ITEM_ID_KEY] === droppedItemId);
172
+ announceToScreenReader("dropped", {
173
+ itemLabel: focusedItemLabel,
174
+ zoneLabel: focusedDzLabel,
175
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
176
+ count: droppedItems.length
177
+ });
159
178
  }
160
179
  if (allDragTargets.has(document.activeElement)) {
161
180
  document.activeElement.blur();
@@ -212,6 +231,10 @@ export function dndzone(node, options) {
212
231
  switch (e.key) {
213
232
  case "Enter":
214
233
  case " ": {
234
+ // keys outside the configured trigger belong to the consumer - don't claim them in any way
235
+ if (!isKeyboardDragTriggerKey(e.key)) {
236
+ return;
237
+ }
215
238
  // we don't want to affect nested input elements or clickable elements
216
239
  if ((e.target.disabled !== undefined || e.target.href || e.target.isContentEditable) && !allDragTargets.has(e.target)) {
217
240
  return;
@@ -238,7 +261,12 @@ export function dndzone(node, options) {
238
261
  printDebug(() => ["arrow down", idx]);
239
262
  if (idx < children.length - 1) {
240
263
  if (!config.autoAriaDisabled) {
241
- alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx + 2} in the list ${focusedDzLabel}`);
264
+ announceToScreenReader("movedToPosition", {
265
+ itemLabel: focusedItemLabel,
266
+ zoneLabel: focusedDzLabel,
267
+ position: idx + 2,
268
+ count: items.length
269
+ });
242
270
  }
243
271
  swap(items, idx, idx + 1);
244
272
  dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
@@ -256,7 +284,12 @@ export function dndzone(node, options) {
256
284
  printDebug(() => ["arrow up", idx]);
257
285
  if (idx > 0) {
258
286
  if (!config.autoAriaDisabled) {
259
- alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx} in the list ${focusedDzLabel}`);
287
+ announceToScreenReader("movedToPosition", {
288
+ itemLabel: focusedItemLabel,
289
+ zoneLabel: focusedDzLabel,
290
+ position: idx,
291
+ count: items.length
292
+ });
260
293
  }
261
294
  swap(items, idx, idx - 1);
262
295
  dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
@@ -279,11 +312,16 @@ export function dndzone(node, options) {
279
312
  dz => dzToConfig.get(dz).dropTargetClasses
280
313
  );
281
314
  if (!config.autoAriaDisabled) {
282
- let msg = `Started dragging item ${focusedItemLabel}. Use the arrow keys to move it within its list ${focusedDzLabel}`;
283
- if (dropTargets.length > 1) {
284
- msg += `, or tab to another list in order to move the item into it`;
285
- }
286
- alertToScreenReader(msg);
315
+ // Include the starting position so localized messages can describe where the item was picked up.
316
+ const startItems = dzToConfig.get(node).items;
317
+ const startIdx = startItems.findIndex(item => item[ITEM_ID_KEY] === focusedItemId);
318
+ announceToScreenReader("dragStarted", {
319
+ itemLabel: focusedItemLabel,
320
+ zoneLabel: focusedDzLabel,
321
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
322
+ count: startItems.length,
323
+ canMoveBetweenZones: dropTargets.length > 1
324
+ });
287
325
  }
288
326
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {trigger: TRIGGERS.DRAG_STARTED, id: focusedItemId, source: SOURCES.KEYBOARD});
289
327
  triggerAllDzsUpdate();
@@ -0,0 +1,44 @@
1
+ import {DEFAULT_KEYBOARD_DRAG_TRIGGER, KEYBOARD_DRAG_TRIGGER_KEYS} from "./constants";
2
+ import {toString} from "./helpers/util";
3
+ import {setInstructionContext} from "./helpers/aria";
4
+
5
+ let activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
6
+
7
+ /**
8
+ * Chooses which key(s) start and stop a keyboard drag on a focused item. Keys outside the trigger are
9
+ * left completely untouched by the library (no preventDefault, no stopPropagation), so the app can use
10
+ * them - ex: "space" keeps Enter free to activate the focused item.
11
+ * This setting is global to the document and applies to every dndzone. It can be called at any time.
12
+ * Pass null or undefined (or call with no argument) to restore the default, "space_or_enter".
13
+ * @param {"space"|"enter"|"space_or_enter"|null|undefined} [trigger]
14
+ * @throws {Error} if given anything other than the documented values, null, or undefined
15
+ */
16
+ export function setKeyboardDragTrigger(trigger) {
17
+ if (trigger === null || trigger === undefined) {
18
+ activeTrigger = DEFAULT_KEYBOARD_DRAG_TRIGGER;
19
+ } else if (typeof trigger !== "string" || !Object.prototype.hasOwnProperty.call(KEYBOARD_DRAG_TRIGGER_KEYS, trigger)) {
20
+ throw new Error(
21
+ `setKeyboardDragTrigger expects one of ${Object.keys(KEYBOARD_DRAG_TRIGGER_KEYS)
22
+ .map(key => `"${key}"`)
23
+ .join(", ")} or null but instead got a ${typeof trigger}, ${toString(trigger)}`
24
+ );
25
+ } else {
26
+ activeTrigger = trigger;
27
+ }
28
+ setInstructionContext({keyboardDragTrigger: activeTrigger});
29
+ }
30
+
31
+ /**
32
+ * @return {"space"|"enter"|"space_or_enter"} the active trigger
33
+ */
34
+ export function getKeyboardDragTrigger() {
35
+ return activeTrigger;
36
+ }
37
+
38
+ /**
39
+ * @param {string} key - a KeyboardEvent.key value
40
+ * @return {boolean} whether this key starts/stops a drag under the active trigger
41
+ */
42
+ export function isKeyboardDragTriggerKey(key) {
43
+ return KEYBOARD_DRAG_TRIGGER_KEYS[activeTrigger].includes(key);
44
+ }
@@ -1,4 +1,5 @@
1
1
  import {SOURCES, TRIGGERS} from "../constants";
2
+ import {isKeyboardDragTriggerKey} from "../keyboardDragTrigger";
2
3
  import {dndzone} from "../action";
3
4
  import {createStore} from "./simpleStore";
4
5
 
@@ -104,7 +105,7 @@ export function dragHandle(handle) {
104
105
  }
105
106
 
106
107
  function handleKeyDown(e) {
107
- if (e.key === "Enter" || e.key === " ") isItemsDragDisabled.set(false);
108
+ if (isKeyboardDragTriggerKey(e.key)) isItemsDragDisabled.set(false);
108
109
  }
109
110
 
110
111
  function resetStartDrag() {