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 +64 -5
- package/dist/index.d.ts +30 -0
- package/dist/index.js +256 -20
- package/dist/index.mjs +255 -21
- package/package.json +1 -1
- package/src/constants.js +18 -0
- package/src/helpers/aria.js +128 -7
- package/src/index.js +2 -1
- package/src/keyboardAction.js +49 -11
- package/src/keyboardDragTrigger.js +44 -0
- package/src/wrappers/withDragHandles.js +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# SVELTE DND ACTION [](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<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
117
|
| `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
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
|
|
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
|
|
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
|
|
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(
|
|
2242
|
-
var
|
|
2243
|
-
id =
|
|
2244
|
-
|
|
2245
|
-
return document.body.prepend(instructionToHiddenDiv(id,
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
2236
|
-
var
|
|
2237
|
-
id =
|
|
2238
|
-
|
|
2239
|
-
return document.body.prepend(instructionToHiddenDiv(id,
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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
|
|
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.
|
|
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";
|
package/src/helpers/aria.js
CHANGED
|
@@ -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
|
|
8
|
-
[INSTRUCTION_IDs.DND_ZONE_ACTIVE]: "
|
|
9
|
-
[INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED]: "
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
package/src/keyboardAction.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
108
|
+
if (isKeyboardDragTriggerKey(e.key)) isItemsDragDisabled.set(false);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
function resetStartDrag() {
|