svelte-dnd-action 0.9.76 → 0.9.77

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
@@ -118,7 +118,7 @@ An options-object with the following attributes:
118
118
  | `dropTargetStyle` | Object<String> | 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
119
  | `dropTargetClasses`| Array<String> | 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
120
  | `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. |
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. <br />**If you only want to change the wording (ex: to translate it), use `setAriaStrings` instead and leave this off.** |
122
122
  | `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
123
  | `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
124
  | `dropAnimationDisabled` | Boolean | No | `false` | Setting it to true will disable the animation of the dropped element to its final place. |
@@ -167,6 +167,33 @@ If you don't provide the aria-labels everything will still work, but the message
167
167
  _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
168
  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
169
 
170
+ #### Translating the screen-reader messages
171
+
172
+ `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:
173
+
174
+ ```javascript
175
+ import {setAriaStrings} from "svelte-dnd-action";
176
+
177
+ setAriaStrings({
178
+ dragStarted: ({itemLabel, zoneLabel, canMoveBetweenZones}) =>
179
+ `Déplacement de ${itemLabel} commencé. Utilisez les flèches pour le déplacer dans la liste ${zoneLabel}` +
180
+ (canMoveBetweenZones ? `, ou tabulez vers une autre liste` : ``),
181
+ movedToPosition: ({itemLabel, zoneLabel, position}) => `${itemLabel} déplacé en position ${position} dans la liste ${zoneLabel}`,
182
+ movedToZoneEnd: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé à la fin de la liste ${zoneLabel}`,
183
+ movedToZoneStart: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé au début de la liste ${zoneLabel}`,
184
+ // The default message omits the destination, but custom messages can include it
185
+ dropped: ({itemLabel, zoneLabel, position, count}) => `${itemLabel} déposé dans ${zoneLabel}, ${position} sur ${count}`,
186
+ zoneActiveInstruction: `Tabulez jusqu'à un élément et appuyez sur espace ou entrée pour le déplacer`,
187
+ zoneDragDisabledInstruction: `Cette liste de glisser-déposer est désactivée`
188
+ });
189
+ ```
190
+
191
+ Every key is optional. Omitted keys use their English defaults. The five announcement keys are formatter functions, so translations can control word order and pluralisation. Each formatter receives `itemLabel` and `zoneLabel` from the consumer-provided `aria-label` attributes, plus `position` and `count` for the item's 1-based position and the number of items in the relevant zone. `dragStarted` also receives `canMoveBetweenZones`. Destructure only the fields your wording needs; the built-in English messages do not use every field, but custom messages can.
192
+
193
+ 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.
194
+
195
+ The setting is global to the document and applies to every dndzone. Different zones cannot use different strings at the same time.
196
+
170
197
  ##### Keyboard support
171
198
 
172
199
  - Tab into a dnd container to get a description and instructions
package/dist/index.d.ts CHANGED
@@ -82,6 +82,27 @@ 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;
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
+
85
106
  /**
86
107
  * Allows using another key instead of "id" in the items data. This is global and applies to all dndzones.
87
108
  * Has to be called when there are no rendered dndzones whatsoever.
package/dist/index.js CHANGED
@@ -2207,12 +2207,45 @@
2207
2207
  };
2208
2208
  }
2209
2209
 
2210
- var _ID_TO_INSTRUCTION;
2210
+ var _INSTRUCTION_ID_TO_ST;
2211
2211
  var INSTRUCTION_IDs$1 = {
2212
2212
  DND_ZONE_ACTIVE: "dnd-zone-active",
2213
2213
  DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled"
2214
2214
  };
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);
2215
+ 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);
2216
+ var DEFAULT_ARIA_STRINGS = {
2217
+ dragStarted: function dragStarted(_ref) {
2218
+ var itemLabel = _ref.itemLabel,
2219
+ zoneLabel = _ref.zoneLabel,
2220
+ canMoveBetweenZones = _ref.canMoveBetweenZones;
2221
+ 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" : "");
2222
+ },
2223
+ movedToPosition: function movedToPosition(_ref2) {
2224
+ var itemLabel = _ref2.itemLabel,
2225
+ zoneLabel = _ref2.zoneLabel,
2226
+ position = _ref2.position;
2227
+ return "Moved item ".concat(itemLabel, " to position ").concat(position, " in the list ").concat(zoneLabel);
2228
+ },
2229
+ movedToZoneEnd: function movedToZoneEnd(_ref3) {
2230
+ var itemLabel = _ref3.itemLabel,
2231
+ zoneLabel = _ref3.zoneLabel;
2232
+ return "Moved item ".concat(itemLabel, " to the end of the list ").concat(zoneLabel);
2233
+ },
2234
+ movedToZoneStart: function movedToZoneStart(_ref4) {
2235
+ var itemLabel = _ref4.itemLabel,
2236
+ zoneLabel = _ref4.zoneLabel;
2237
+ return "Moved item ".concat(itemLabel, " to the beginning of the list ").concat(zoneLabel);
2238
+ },
2239
+ dropped: function dropped(_ref5) {
2240
+ var itemLabel = _ref5.itemLabel;
2241
+ return "Stopped dragging item ".concat(itemLabel);
2242
+ },
2243
+ zoneActiveInstruction: "Tab to one the items and press space-bar or enter to start dragging it",
2244
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
2245
+ };
2246
+ var FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
2247
+ var STRING_ARIA_STRING_KEYS = ["zoneActiveInstruction", "zoneDragDisabledInstruction"];
2248
+ var ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2216
2249
  var ALERT_DIV_ID = "dnd-action-aria-alert";
2217
2250
  var alertsDiv;
2218
2251
  function initAriaOnBrowser() {
@@ -2238,11 +2271,11 @@
2238
2271
  document.body.prepend(alertsDiv);
2239
2272
 
2240
2273
  // 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));
2274
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref6) {
2275
+ var _ref7 = _slicedToArray(_ref6, 2),
2276
+ id = _ref7[0],
2277
+ key = _ref7[1];
2278
+ return document.body.prepend(instructionToHiddenDiv(id, ariaStrings[key]));
2246
2279
  });
2247
2280
  }
2248
2281
 
@@ -2265,7 +2298,7 @@
2265
2298
  */
2266
2299
  function destroyAria() {
2267
2300
  if (isOnServer || !alertsDiv) return;
2268
- Object.keys(ID_TO_INSTRUCTION).forEach(function (id) {
2301
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (id) {
2269
2302
  var _document$getElementB;
2270
2303
  return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.remove();
2271
2304
  });
@@ -2275,12 +2308,18 @@
2275
2308
  function instructionToHiddenDiv(id, txt) {
2276
2309
  var div = document.createElement("div");
2277
2310
  div.id = id;
2278
- div.innerHTML = "<p>".concat(txt, "</p>");
2311
+ renderInstruction(div, txt);
2279
2312
  div.style.display = "none";
2280
2313
  div.style.position = "fixed";
2281
2314
  div.style.zIndex = "-5";
2282
2315
  return div;
2283
2316
  }
2317
+ function renderInstruction(div, txt) {
2318
+ div.replaceChildren();
2319
+ var paragraph = document.createElement("p");
2320
+ paragraph.textContent = txt;
2321
+ div.appendChild(paragraph);
2322
+ }
2284
2323
 
2285
2324
  /**
2286
2325
  * Will make the screen reader alert the provided text to the user
@@ -2299,6 +2338,79 @@
2299
2338
  alertsDiv.style.display = "inline";
2300
2339
  }
2301
2340
 
2341
+ /**
2342
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
2343
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
2344
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
2345
+ * Pass null to restore the built-in English strings.
2346
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
2347
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
2348
+ * zoneActiveInstruction, zoneDragDisabledInstruction (strings)
2349
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
2350
+ * the wrong type. Validation completes before the active strings change.
2351
+ */
2352
+ function setAriaStrings(overrides) {
2353
+ if (overrides === null || overrides === undefined) {
2354
+ ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2355
+ } else {
2356
+ if (_typeof(overrides) !== "object" || Array.isArray(overrides)) {
2357
+ throw new Error("setAriaStrings expects an object or null but instead got a ".concat(_typeof(overrides), ", ").concat(toString(overrides)));
2358
+ }
2359
+ Object.keys(overrides).forEach(function (key) {
2360
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
2361
+ throw new Error("Can't set non existing aria string ".concat(key, "! Supported strings: ").concat(Object.keys(DEFAULT_ARIA_STRINGS)));
2362
+ }
2363
+ var value = overrides[key];
2364
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
2365
+ throw new Error("".concat(key, " should be a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2366
+ }
2367
+ if (STRING_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
2368
+ throw new Error("".concat(key, " should be a string but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2369
+ }
2370
+ });
2371
+ ariaStrings = _objectSpread2(_objectSpread2({}, DEFAULT_ARIA_STRINGS), overrides);
2372
+ }
2373
+ if (isOnServer) return;
2374
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref8) {
2375
+ var _ref9 = _slicedToArray(_ref8, 2),
2376
+ id = _ref9[0],
2377
+ key = _ref9[1];
2378
+ var div = document.getElementById(id);
2379
+ if (div) renderInstruction(div, ariaStrings[key]);
2380
+ });
2381
+ }
2382
+ function formatAriaString(strings, key, ctx) {
2383
+ var ariaString = strings[key];
2384
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
2385
+ }
2386
+
2387
+ /**
2388
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
2389
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
2390
+ * escape because this function runs during the drag lifecycle.
2391
+ * @param {string} key - one of the keys accepted by setAriaStrings
2392
+ * @param {Object} [ctx] - the interpolation context for that key
2393
+ */
2394
+ function announceToScreenReader(key, ctx) {
2395
+ var text;
2396
+ try {
2397
+ text = formatAriaString(ariaStrings, key, ctx);
2398
+ } catch (err) {
2399
+ printDebug(function () {
2400
+ return ["aria string formatter for \"".concat(key, "\" threw, falling back to the default"), err];
2401
+ });
2402
+ try {
2403
+ text = formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
2404
+ } catch (defaultErr) {
2405
+ printDebug(function () {
2406
+ return ["default aria string formatter for \"".concat(key, "\" also threw"), defaultErr];
2407
+ });
2408
+ text = "";
2409
+ }
2410
+ }
2411
+ alertToScreenReader(text);
2412
+ }
2413
+
2302
2414
  var DEFAULT_DROP_ZONE_TYPE = "--any--";
2303
2415
  var DEFAULT_DROP_TARGET_STYLE = {
2304
2416
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
@@ -2428,12 +2540,22 @@
2428
2540
  if (newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left) {
2429
2541
  targetItems.push(itemToMove);
2430
2542
  if (!autoAriaDisabled) {
2431
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the end of the list ").concat(focusedDzLabel));
2543
+ announceToScreenReader("movedToZoneEnd", {
2544
+ itemLabel: focusedItemLabel,
2545
+ zoneLabel: focusedDzLabel,
2546
+ position: targetItems.length,
2547
+ count: targetItems.length
2548
+ });
2432
2549
  }
2433
2550
  } else {
2434
2551
  targetItems.unshift(itemToMove);
2435
2552
  if (!autoAriaDisabled) {
2436
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the beginning of the list ").concat(focusedDzLabel));
2553
+ announceToScreenReader("movedToZoneStart", {
2554
+ itemLabel: focusedItemLabel,
2555
+ zoneLabel: focusedDzLabel,
2556
+ position: 1,
2557
+ count: targetItems.length
2558
+ });
2437
2559
  }
2438
2560
  }
2439
2561
  var dzFrom = focusedDz;
@@ -2470,7 +2592,17 @@
2470
2592
  var droppedItemType = draggedItemType;
2471
2593
  if (!droppedConfig) return;
2472
2594
  if (!droppedConfig.autoAriaDisabled) {
2473
- alertToScreenReader("Stopped dragging item ".concat(focusedItemLabel));
2595
+ // Include the destination and final position so localized messages can describe the completed drop.
2596
+ var droppedItems = droppedConfig.items;
2597
+ var droppedIdx = droppedItems.findIndex(function (item) {
2598
+ return item[ITEM_ID_KEY] === droppedItemId;
2599
+ });
2600
+ announceToScreenReader("dropped", {
2601
+ itemLabel: focusedItemLabel,
2602
+ zoneLabel: focusedDzLabel,
2603
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
2604
+ count: droppedItems.length
2605
+ });
2474
2606
  }
2475
2607
  if (allDragTargets.has(document.activeElement)) {
2476
2608
  document.activeElement.blur();
@@ -2557,7 +2689,12 @@
2557
2689
  });
2558
2690
  if (idx < children.length - 1) {
2559
2691
  if (!config.autoAriaDisabled) {
2560
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(idx + 2, " in the list ").concat(focusedDzLabel));
2692
+ announceToScreenReader("movedToPosition", {
2693
+ itemLabel: focusedItemLabel,
2694
+ zoneLabel: focusedDzLabel,
2695
+ position: idx + 2,
2696
+ count: items.length
2697
+ });
2561
2698
  }
2562
2699
  swap(items, idx, idx + 1);
2563
2700
  dispatchFinalizeEvent(node, items, {
@@ -2583,7 +2720,12 @@
2583
2720
  });
2584
2721
  if (_idx > 0) {
2585
2722
  if (!config.autoAriaDisabled) {
2586
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(_idx, " in the list ").concat(focusedDzLabel));
2723
+ announceToScreenReader("movedToPosition", {
2724
+ itemLabel: focusedItemLabel,
2725
+ zoneLabel: focusedDzLabel,
2726
+ position: _idx,
2727
+ count: _items.length
2728
+ });
2587
2729
  }
2588
2730
  swap(_items, _idx, _idx - 1);
2589
2731
  dispatchFinalizeEvent(node, _items, {
@@ -2614,11 +2756,18 @@
2614
2756
  return dzToConfig.get(dz).dropTargetClasses;
2615
2757
  });
2616
2758
  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);
2759
+ // Include the starting position so localized messages can describe where the item was picked up.
2760
+ var startItems = dzToConfig.get(node).items;
2761
+ var startIdx = startItems.findIndex(function (item) {
2762
+ return item[ITEM_ID_KEY] === focusedItemId;
2763
+ });
2764
+ announceToScreenReader("dragStarted", {
2765
+ itemLabel: focusedItemLabel,
2766
+ zoneLabel: focusedDzLabel,
2767
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
2768
+ count: startItems.length,
2769
+ canMoveBetweenZones: dropTargets.length > 1
2770
+ });
2622
2771
  }
2623
2772
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {
2624
2773
  trigger: TRIGGERS.DRAG_STARTED,
@@ -3043,6 +3192,7 @@
3043
3192
  exports.dragHandle = dragHandle;
3044
3193
  exports.dragHandleZone = dragHandleZone;
3045
3194
  exports.overrideItemIdKeyNameBeforeInitialisingDndZones = overrideItemIdKeyNameBeforeInitialisingDndZones;
3195
+ exports.setAriaStrings = setAriaStrings;
3046
3196
  exports.setDebugMode = setDebugMode;
3047
3197
  exports.setFeatureFlag = setFeatureFlag;
3048
3198
 
package/dist/index.mjs CHANGED
@@ -2201,12 +2201,45 @@ function dndzone$2(node, options) {
2201
2201
  };
2202
2202
  }
2203
2203
 
2204
- var _ID_TO_INSTRUCTION;
2204
+ var _INSTRUCTION_ID_TO_ST;
2205
2205
  var INSTRUCTION_IDs$1 = {
2206
2206
  DND_ZONE_ACTIVE: "dnd-zone-active",
2207
2207
  DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled"
2208
2208
  };
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);
2209
+ 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);
2210
+ var DEFAULT_ARIA_STRINGS = {
2211
+ dragStarted: function dragStarted(_ref) {
2212
+ var itemLabel = _ref.itemLabel,
2213
+ zoneLabel = _ref.zoneLabel,
2214
+ canMoveBetweenZones = _ref.canMoveBetweenZones;
2215
+ 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" : "");
2216
+ },
2217
+ movedToPosition: function movedToPosition(_ref2) {
2218
+ var itemLabel = _ref2.itemLabel,
2219
+ zoneLabel = _ref2.zoneLabel,
2220
+ position = _ref2.position;
2221
+ return "Moved item ".concat(itemLabel, " to position ").concat(position, " in the list ").concat(zoneLabel);
2222
+ },
2223
+ movedToZoneEnd: function movedToZoneEnd(_ref3) {
2224
+ var itemLabel = _ref3.itemLabel,
2225
+ zoneLabel = _ref3.zoneLabel;
2226
+ return "Moved item ".concat(itemLabel, " to the end of the list ").concat(zoneLabel);
2227
+ },
2228
+ movedToZoneStart: function movedToZoneStart(_ref4) {
2229
+ var itemLabel = _ref4.itemLabel,
2230
+ zoneLabel = _ref4.zoneLabel;
2231
+ return "Moved item ".concat(itemLabel, " to the beginning of the list ").concat(zoneLabel);
2232
+ },
2233
+ dropped: function dropped(_ref5) {
2234
+ var itemLabel = _ref5.itemLabel;
2235
+ return "Stopped dragging item ".concat(itemLabel);
2236
+ },
2237
+ zoneActiveInstruction: "Tab to one the items and press space-bar or enter to start dragging it",
2238
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
2239
+ };
2240
+ var FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
2241
+ var STRING_ARIA_STRING_KEYS = ["zoneActiveInstruction", "zoneDragDisabledInstruction"];
2242
+ var ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2210
2243
  var ALERT_DIV_ID = "dnd-action-aria-alert";
2211
2244
  var alertsDiv;
2212
2245
  function initAriaOnBrowser() {
@@ -2232,11 +2265,11 @@ function initAriaOnBrowser() {
2232
2265
  document.body.prepend(alertsDiv);
2233
2266
 
2234
2267
  // 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));
2268
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref6) {
2269
+ var _ref7 = _slicedToArray(_ref6, 2),
2270
+ id = _ref7[0],
2271
+ key = _ref7[1];
2272
+ return document.body.prepend(instructionToHiddenDiv(id, ariaStrings[key]));
2240
2273
  });
2241
2274
  }
2242
2275
 
@@ -2259,7 +2292,7 @@ function initAria() {
2259
2292
  */
2260
2293
  function destroyAria() {
2261
2294
  if (isOnServer || !alertsDiv) return;
2262
- Object.keys(ID_TO_INSTRUCTION).forEach(function (id) {
2295
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (id) {
2263
2296
  var _document$getElementB;
2264
2297
  return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.remove();
2265
2298
  });
@@ -2269,12 +2302,18 @@ function destroyAria() {
2269
2302
  function instructionToHiddenDiv(id, txt) {
2270
2303
  var div = document.createElement("div");
2271
2304
  div.id = id;
2272
- div.innerHTML = "<p>".concat(txt, "</p>");
2305
+ renderInstruction(div, txt);
2273
2306
  div.style.display = "none";
2274
2307
  div.style.position = "fixed";
2275
2308
  div.style.zIndex = "-5";
2276
2309
  return div;
2277
2310
  }
2311
+ function renderInstruction(div, txt) {
2312
+ div.replaceChildren();
2313
+ var paragraph = document.createElement("p");
2314
+ paragraph.textContent = txt;
2315
+ div.appendChild(paragraph);
2316
+ }
2278
2317
 
2279
2318
  /**
2280
2319
  * Will make the screen reader alert the provided text to the user
@@ -2293,6 +2332,79 @@ function alertToScreenReader(txt) {
2293
2332
  alertsDiv.style.display = "inline";
2294
2333
  }
2295
2334
 
2335
+ /**
2336
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
2337
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
2338
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
2339
+ * Pass null to restore the built-in English strings.
2340
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
2341
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
2342
+ * zoneActiveInstruction, zoneDragDisabledInstruction (strings)
2343
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
2344
+ * the wrong type. Validation completes before the active strings change.
2345
+ */
2346
+ function setAriaStrings(overrides) {
2347
+ if (overrides === null || overrides === undefined) {
2348
+ ariaStrings = _objectSpread2({}, DEFAULT_ARIA_STRINGS);
2349
+ } else {
2350
+ if (_typeof(overrides) !== "object" || Array.isArray(overrides)) {
2351
+ throw new Error("setAriaStrings expects an object or null but instead got a ".concat(_typeof(overrides), ", ").concat(toString(overrides)));
2352
+ }
2353
+ Object.keys(overrides).forEach(function (key) {
2354
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
2355
+ throw new Error("Can't set non existing aria string ".concat(key, "! Supported strings: ").concat(Object.keys(DEFAULT_ARIA_STRINGS)));
2356
+ }
2357
+ var value = overrides[key];
2358
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
2359
+ throw new Error("".concat(key, " should be a function but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2360
+ }
2361
+ if (STRING_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
2362
+ throw new Error("".concat(key, " should be a string but instead it is a ").concat(_typeof(value), ", ").concat(toString(value)));
2363
+ }
2364
+ });
2365
+ ariaStrings = _objectSpread2(_objectSpread2({}, DEFAULT_ARIA_STRINGS), overrides);
2366
+ }
2367
+ if (isOnServer) return;
2368
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(function (_ref8) {
2369
+ var _ref9 = _slicedToArray(_ref8, 2),
2370
+ id = _ref9[0],
2371
+ key = _ref9[1];
2372
+ var div = document.getElementById(id);
2373
+ if (div) renderInstruction(div, ariaStrings[key]);
2374
+ });
2375
+ }
2376
+ function formatAriaString(strings, key, ctx) {
2377
+ var ariaString = strings[key];
2378
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
2379
+ }
2380
+
2381
+ /**
2382
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
2383
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
2384
+ * escape because this function runs during the drag lifecycle.
2385
+ * @param {string} key - one of the keys accepted by setAriaStrings
2386
+ * @param {Object} [ctx] - the interpolation context for that key
2387
+ */
2388
+ function announceToScreenReader(key, ctx) {
2389
+ var text;
2390
+ try {
2391
+ text = formatAriaString(ariaStrings, key, ctx);
2392
+ } catch (err) {
2393
+ printDebug(function () {
2394
+ return ["aria string formatter for \"".concat(key, "\" threw, falling back to the default"), err];
2395
+ });
2396
+ try {
2397
+ text = formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
2398
+ } catch (defaultErr) {
2399
+ printDebug(function () {
2400
+ return ["default aria string formatter for \"".concat(key, "\" also threw"), defaultErr];
2401
+ });
2402
+ text = "";
2403
+ }
2404
+ }
2405
+ alertToScreenReader(text);
2406
+ }
2407
+
2296
2408
  var DEFAULT_DROP_ZONE_TYPE = "--any--";
2297
2409
  var DEFAULT_DROP_TARGET_STYLE = {
2298
2410
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
@@ -2422,12 +2534,22 @@ function handleZoneFocus(e) {
2422
2534
  if (newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left) {
2423
2535
  targetItems.push(itemToMove);
2424
2536
  if (!autoAriaDisabled) {
2425
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the end of the list ").concat(focusedDzLabel));
2537
+ announceToScreenReader("movedToZoneEnd", {
2538
+ itemLabel: focusedItemLabel,
2539
+ zoneLabel: focusedDzLabel,
2540
+ position: targetItems.length,
2541
+ count: targetItems.length
2542
+ });
2426
2543
  }
2427
2544
  } else {
2428
2545
  targetItems.unshift(itemToMove);
2429
2546
  if (!autoAriaDisabled) {
2430
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to the beginning of the list ").concat(focusedDzLabel));
2547
+ announceToScreenReader("movedToZoneStart", {
2548
+ itemLabel: focusedItemLabel,
2549
+ zoneLabel: focusedDzLabel,
2550
+ position: 1,
2551
+ count: targetItems.length
2552
+ });
2431
2553
  }
2432
2554
  }
2433
2555
  var dzFrom = focusedDz;
@@ -2464,7 +2586,17 @@ function handleDrop() {
2464
2586
  var droppedItemType = draggedItemType;
2465
2587
  if (!droppedConfig) return;
2466
2588
  if (!droppedConfig.autoAriaDisabled) {
2467
- alertToScreenReader("Stopped dragging item ".concat(focusedItemLabel));
2589
+ // Include the destination and final position so localized messages can describe the completed drop.
2590
+ var droppedItems = droppedConfig.items;
2591
+ var droppedIdx = droppedItems.findIndex(function (item) {
2592
+ return item[ITEM_ID_KEY] === droppedItemId;
2593
+ });
2594
+ announceToScreenReader("dropped", {
2595
+ itemLabel: focusedItemLabel,
2596
+ zoneLabel: focusedDzLabel,
2597
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
2598
+ count: droppedItems.length
2599
+ });
2468
2600
  }
2469
2601
  if (allDragTargets.has(document.activeElement)) {
2470
2602
  document.activeElement.blur();
@@ -2551,7 +2683,12 @@ function dndzone$1(node, options) {
2551
2683
  });
2552
2684
  if (idx < children.length - 1) {
2553
2685
  if (!config.autoAriaDisabled) {
2554
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(idx + 2, " in the list ").concat(focusedDzLabel));
2686
+ announceToScreenReader("movedToPosition", {
2687
+ itemLabel: focusedItemLabel,
2688
+ zoneLabel: focusedDzLabel,
2689
+ position: idx + 2,
2690
+ count: items.length
2691
+ });
2555
2692
  }
2556
2693
  swap(items, idx, idx + 1);
2557
2694
  dispatchFinalizeEvent(node, items, {
@@ -2577,7 +2714,12 @@ function dndzone$1(node, options) {
2577
2714
  });
2578
2715
  if (_idx > 0) {
2579
2716
  if (!config.autoAriaDisabled) {
2580
- alertToScreenReader("Moved item ".concat(focusedItemLabel, " to position ").concat(_idx, " in the list ").concat(focusedDzLabel));
2717
+ announceToScreenReader("movedToPosition", {
2718
+ itemLabel: focusedItemLabel,
2719
+ zoneLabel: focusedDzLabel,
2720
+ position: _idx,
2721
+ count: _items.length
2722
+ });
2581
2723
  }
2582
2724
  swap(_items, _idx, _idx - 1);
2583
2725
  dispatchFinalizeEvent(node, _items, {
@@ -2608,11 +2750,18 @@ function dndzone$1(node, options) {
2608
2750
  return dzToConfig.get(dz).dropTargetClasses;
2609
2751
  });
2610
2752
  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);
2753
+ // Include the starting position so localized messages can describe where the item was picked up.
2754
+ var startItems = dzToConfig.get(node).items;
2755
+ var startIdx = startItems.findIndex(function (item) {
2756
+ return item[ITEM_ID_KEY] === focusedItemId;
2757
+ });
2758
+ announceToScreenReader("dragStarted", {
2759
+ itemLabel: focusedItemLabel,
2760
+ zoneLabel: focusedDzLabel,
2761
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
2762
+ count: startItems.length,
2763
+ canMoveBetweenZones: dropTargets.length > 1
2764
+ });
2616
2765
  }
2617
2766
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {
2618
2767
  trigger: TRIGGERS.DRAG_STARTED,
@@ -3026,4 +3175,4 @@ function dragHandle(handle) {
3026
3175
  };
3027
3176
  }
3028
3177
 
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 };
3178
+ 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 };
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.77",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/isaacHagoel/svelte-dnd-action.git"
@@ -1,14 +1,32 @@
1
- import {isOnServer} from "../constants";
1
+ import {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: "Tab to one the items and press space-bar or enter to start dragging it",
22
+ zoneDragDisabledInstruction: "This is a disabled drag and drop list"
23
+ };
24
+
25
+ const FUNCTION_ARIA_STRING_KEYS = ["dragStarted", "movedToPosition", "movedToZoneEnd", "movedToZoneStart", "dropped"];
26
+ const STRING_ARIA_STRING_KEYS = ["zoneActiveInstruction", "zoneDragDisabledInstruction"];
27
+
28
+ let ariaStrings = {...DEFAULT_ARIA_STRINGS};
29
+
12
30
  const ALERT_DIV_ID = "dnd-action-aria-alert";
13
31
  let alertsDiv;
14
32
 
@@ -35,7 +53,7 @@ function initAriaOnBrowser() {
35
53
  document.body.prepend(alertsDiv);
36
54
 
37
55
  // setting the instructions
38
- Object.entries(ID_TO_INSTRUCTION).forEach(([id, txt]) => document.body.prepend(instructionToHiddenDiv(id, txt)));
56
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(([id, key]) => document.body.prepend(instructionToHiddenDiv(id, ariaStrings[key])));
39
57
  }
40
58
 
41
59
  /**
@@ -57,7 +75,7 @@ export function initAria() {
57
75
  */
58
76
  export function destroyAria() {
59
77
  if (isOnServer || !alertsDiv) return;
60
- Object.keys(ID_TO_INSTRUCTION).forEach(id => document.getElementById(id)?.remove());
78
+ Object.keys(INSTRUCTION_ID_TO_STRING_KEY).forEach(id => document.getElementById(id)?.remove());
61
79
  alertsDiv.remove();
62
80
  alertsDiv = undefined;
63
81
  }
@@ -65,13 +83,20 @@ export function destroyAria() {
65
83
  function instructionToHiddenDiv(id, txt) {
66
84
  const div = document.createElement("div");
67
85
  div.id = id;
68
- div.innerHTML = `<p>${txt}</p>`;
86
+ renderInstruction(div, txt);
69
87
  div.style.display = "none";
70
88
  div.style.position = "fixed";
71
89
  div.style.zIndex = "-5";
72
90
  return div;
73
91
  }
74
92
 
93
+ function renderInstruction(div, txt) {
94
+ div.replaceChildren();
95
+ const paragraph = document.createElement("p");
96
+ paragraph.textContent = txt;
97
+ div.appendChild(paragraph);
98
+ }
99
+
75
100
  /**
76
101
  * Will make the screen reader alert the provided text to the user
77
102
  * @param {string} txt
@@ -88,3 +113,70 @@ export function alertToScreenReader(txt) {
88
113
  alertsDiv.style.display = "none";
89
114
  alertsDiv.style.display = "inline";
90
115
  }
116
+
117
+ /**
118
+ * Overrides the strings the library announces to screen readers. Each call starts with the built-in
119
+ * English defaults and applies the supplied overrides, so omitted keys return to English when the locale
120
+ * changes. Existing instruction elements update immediately. This setting is global to all dndzones.
121
+ * Pass null to restore the built-in English strings.
122
+ * @param {Object | null} overrides - any subset of: dragStarted, movedToPosition, movedToZoneEnd,
123
+ * movedToZoneStart, dropped (functions taking a context object and returning a string);
124
+ * zoneActiveInstruction, zoneDragDisabledInstruction (strings)
125
+ * @throws {Error} if overrides is not an object or null, contains an unknown key, or contains a value of
126
+ * the wrong type. Validation completes before the active strings change.
127
+ */
128
+ export function setAriaStrings(overrides) {
129
+ if (overrides === null || overrides === undefined) {
130
+ ariaStrings = {...DEFAULT_ARIA_STRINGS};
131
+ } else {
132
+ if (typeof overrides !== "object" || Array.isArray(overrides)) {
133
+ throw new Error(`setAriaStrings expects an object or null but instead got a ${typeof overrides}, ${toString(overrides)}`);
134
+ }
135
+ Object.keys(overrides).forEach(key => {
136
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_ARIA_STRINGS, key)) {
137
+ throw new Error(`Can't set non existing aria string ${key}! Supported strings: ${Object.keys(DEFAULT_ARIA_STRINGS)}`);
138
+ }
139
+ const value = overrides[key];
140
+ if (FUNCTION_ARIA_STRING_KEYS.includes(key) && typeof value !== "function") {
141
+ throw new Error(`${key} should be a function but instead it is a ${typeof value}, ${toString(value)}`);
142
+ }
143
+ if (STRING_ARIA_STRING_KEYS.includes(key) && typeof value !== "string") {
144
+ throw new Error(`${key} should be a string but instead it is a ${typeof value}, ${toString(value)}`);
145
+ }
146
+ });
147
+ ariaStrings = {...DEFAULT_ARIA_STRINGS, ...overrides};
148
+ }
149
+ if (isOnServer) return;
150
+ Object.entries(INSTRUCTION_ID_TO_STRING_KEY).forEach(([id, key]) => {
151
+ const div = document.getElementById(id);
152
+ if (div) renderInstruction(div, ariaStrings[key]);
153
+ });
154
+ }
155
+
156
+ function formatAriaString(strings, key, ctx) {
157
+ const ariaString = strings[key];
158
+ return typeof ariaString === "function" ? ariaString(ctx) : ariaString;
159
+ }
160
+
161
+ /**
162
+ * Formats and announces one configured ARIA message. If a consumer formatter throws, the built-in
163
+ * English formatter is used and the error is reported through printDebug. Formatter errors must not
164
+ * escape because this function runs during the drag lifecycle.
165
+ * @param {string} key - one of the keys accepted by setAriaStrings
166
+ * @param {Object} [ctx] - the interpolation context for that key
167
+ */
168
+ export function announceToScreenReader(key, ctx) {
169
+ let text;
170
+ try {
171
+ text = formatAriaString(ariaStrings, key, ctx);
172
+ } catch (err) {
173
+ printDebug(() => [`aria string formatter for "${key}" threw, falling back to the default`, err]);
174
+ try {
175
+ text = formatAriaString(DEFAULT_ARIA_STRINGS, key, ctx);
176
+ } catch (defaultErr) {
177
+ printDebug(() => [`default aria string formatter for "${key}" also threw`, defaultErr]);
178
+ text = "";
179
+ }
180
+ }
181
+ alertToScreenReader(text);
182
+ }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
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
4
  export {
5
5
  TRIGGERS,
6
6
  SOURCES,
@@ -1,7 +1,7 @@
1
1
  import {decrementActiveDropZoneCount, incrementActiveDropZoneCount, ITEM_ID_KEY, SOURCES, TRIGGERS} from "./constants";
2
2
  import {styleActiveDropZones, styleInactiveDropZones} from "./helpers/styler";
3
3
  import {dispatchConsiderEvent, dispatchFinalizeEvent} from "./helpers/dispatcher";
4
- import {initAria, alertToScreenReader, destroyAria} from "./helpers/aria";
4
+ import {initAria, announceToScreenReader, destroyAria} from "./helpers/aria";
5
5
  import {toString} from "./helpers/util";
6
6
  import {printDebug} from "./constants";
7
7
 
@@ -124,12 +124,22 @@ function handleZoneFocus(e) {
124
124
  ) {
125
125
  targetItems.push(itemToMove);
126
126
  if (!autoAriaDisabled) {
127
- alertToScreenReader(`Moved item ${focusedItemLabel} to the end of the list ${focusedDzLabel}`);
127
+ announceToScreenReader("movedToZoneEnd", {
128
+ itemLabel: focusedItemLabel,
129
+ zoneLabel: focusedDzLabel,
130
+ position: targetItems.length,
131
+ count: targetItems.length
132
+ });
128
133
  }
129
134
  } else {
130
135
  targetItems.unshift(itemToMove);
131
136
  if (!autoAriaDisabled) {
132
- alertToScreenReader(`Moved item ${focusedItemLabel} to the beginning of the list ${focusedDzLabel}`);
137
+ announceToScreenReader("movedToZoneStart", {
138
+ itemLabel: focusedItemLabel,
139
+ zoneLabel: focusedDzLabel,
140
+ position: 1,
141
+ count: targetItems.length
142
+ });
133
143
  }
134
144
  }
135
145
  const dzFrom = focusedDz;
@@ -155,7 +165,15 @@ function handleDrop(dispatchConsider = true) {
155
165
  if (!droppedConfig) return;
156
166
 
157
167
  if (!droppedConfig.autoAriaDisabled) {
158
- alertToScreenReader(`Stopped dragging item ${focusedItemLabel}`);
168
+ // Include the destination and final position so localized messages can describe the completed drop.
169
+ const droppedItems = droppedConfig.items;
170
+ const droppedIdx = droppedItems.findIndex(item => item[ITEM_ID_KEY] === droppedItemId);
171
+ announceToScreenReader("dropped", {
172
+ itemLabel: focusedItemLabel,
173
+ zoneLabel: focusedDzLabel,
174
+ position: (droppedIdx < 0 ? 0 : droppedIdx) + 1,
175
+ count: droppedItems.length
176
+ });
159
177
  }
160
178
  if (allDragTargets.has(document.activeElement)) {
161
179
  document.activeElement.blur();
@@ -238,7 +256,12 @@ export function dndzone(node, options) {
238
256
  printDebug(() => ["arrow down", idx]);
239
257
  if (idx < children.length - 1) {
240
258
  if (!config.autoAriaDisabled) {
241
- alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx + 2} in the list ${focusedDzLabel}`);
259
+ announceToScreenReader("movedToPosition", {
260
+ itemLabel: focusedItemLabel,
261
+ zoneLabel: focusedDzLabel,
262
+ position: idx + 2,
263
+ count: items.length
264
+ });
242
265
  }
243
266
  swap(items, idx, idx + 1);
244
267
  dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
@@ -256,7 +279,12 @@ export function dndzone(node, options) {
256
279
  printDebug(() => ["arrow up", idx]);
257
280
  if (idx > 0) {
258
281
  if (!config.autoAriaDisabled) {
259
- alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx} in the list ${focusedDzLabel}`);
282
+ announceToScreenReader("movedToPosition", {
283
+ itemLabel: focusedItemLabel,
284
+ zoneLabel: focusedDzLabel,
285
+ position: idx,
286
+ count: items.length
287
+ });
260
288
  }
261
289
  swap(items, idx, idx - 1);
262
290
  dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
@@ -279,11 +307,16 @@ export function dndzone(node, options) {
279
307
  dz => dzToConfig.get(dz).dropTargetClasses
280
308
  );
281
309
  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);
310
+ // Include the starting position so localized messages can describe where the item was picked up.
311
+ const startItems = dzToConfig.get(node).items;
312
+ const startIdx = startItems.findIndex(item => item[ITEM_ID_KEY] === focusedItemId);
313
+ announceToScreenReader("dragStarted", {
314
+ itemLabel: focusedItemLabel,
315
+ zoneLabel: focusedDzLabel,
316
+ position: (startIdx < 0 ? 0 : startIdx) + 1,
317
+ count: startItems.length,
318
+ canMoveBetweenZones: dropTargets.length > 1
319
+ });
287
320
  }
288
321
  dispatchConsiderEvent(node, dzToConfig.get(node).items, {trigger: TRIGGERS.DRAG_STARTED, id: focusedItemId, source: SOURCES.KEYBOARD});
289
322
  triggerAllDzsUpdate();