@bennerinformatics/ember-fw-table 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.yalc/ember-sortable/.huskyrc +5 -0
  2. package/.yalc/ember-sortable/CHANGELOG.md +755 -0
  3. package/.yalc/ember-sortable/CODE_OF_CONDUCT.md +6 -0
  4. package/.yalc/ember-sortable/LICENSE.md +9 -0
  5. package/.yalc/ember-sortable/MIGRATION_GUIDE_MODIFIERS.md +95 -0
  6. package/.yalc/ember-sortable/MIGRATION_GUIDE_V2.md +120 -0
  7. package/.yalc/ember-sortable/Makefile +24 -0
  8. package/.yalc/ember-sortable/README.md +423 -0
  9. package/.yalc/ember-sortable/RELEASE.md +60 -0
  10. package/.yalc/ember-sortable/V2_MIGRATION_RFC.md +1100 -0
  11. package/.yalc/ember-sortable/addon/modifiers/sortable-group.js +754 -0
  12. package/.yalc/ember-sortable/addon/modifiers/sortable-handle.js +29 -0
  13. package/.yalc/ember-sortable/addon/modifiers/sortable-item.js +869 -0
  14. package/.yalc/ember-sortable/addon/services/ember-sortable.js +92 -0
  15. package/.yalc/ember-sortable/addon/system/scroll-container.js +53 -0
  16. package/.yalc/ember-sortable/addon/system/scroll-parent.js +33 -0
  17. package/.yalc/ember-sortable/addon/utils/constant.js +9 -0
  18. package/.yalc/ember-sortable/addon/utils/coordinate.js +34 -0
  19. package/.yalc/ember-sortable/addon/utils/css-calculation.js +20 -0
  20. package/.yalc/ember-sortable/addon/utils/defaults.js +26 -0
  21. package/.yalc/ember-sortable/addon/utils/keyboard.js +32 -0
  22. package/.yalc/ember-sortable/addon-test-support/helpers/drag.js +111 -0
  23. package/.yalc/ember-sortable/addon-test-support/helpers/index.js +4 -0
  24. package/.yalc/ember-sortable/addon-test-support/helpers/reorder.js +41 -0
  25. package/.yalc/ember-sortable/addon-test-support/utils/keyboard.js +32 -0
  26. package/.yalc/ember-sortable/addon-test-support/utils/offset.js +14 -0
  27. package/.yalc/ember-sortable/app/modifiers/sortable-group.js +1 -0
  28. package/.yalc/ember-sortable/app/modifiers/sortable-handle.js +1 -0
  29. package/.yalc/ember-sortable/app/modifiers/sortable-item.js +1 -0
  30. package/.yalc/ember-sortable/app/services/ember-sortable-internal-state.js +1 -0
  31. package/.yalc/ember-sortable/config/environment.js +5 -0
  32. package/.yalc/ember-sortable/demo.gif +0 -0
  33. package/.yalc/ember-sortable/index.js +5 -0
  34. package/.yalc/ember-sortable/package.json +65 -0
  35. package/.yalc/ember-sortable/yalc.sig +1 -0
  36. package/addon/components/fw-pagination-wrapper.js +2 -2
  37. package/addon/components/fw-table-sortable.js +14 -3
  38. package/addon/templates/components/fw-delete-modal.hbs +1 -1
  39. package/addon/templates/components/fw-table-resort.hbs +4 -4
  40. package/addon/utils/formats.js +6 -3
  41. package/package.json +65 -65
  42. package/yalc.lock +10 -0
@@ -0,0 +1,1100 @@
1
+ # Ember Sortable V2 Migration RFC
2
+
3
+ ## Author
4
+
5
+ [ygongdev](https://github.com/ygongdev)
6
+
7
+ ## Credits
8
+
9
+ - [jgwhite](https://github.com/jgwhite)
10
+ - [H1D](https://github.com/H1D)
11
+ - [chriskrycho](https://github.com/chriskrycho)
12
+ - [Andrew Lee](https://github.com/drewlee)
13
+
14
+ ## Table of Content
15
+
16
+ 1. [Problem Statement](#problem-statement)
17
+ 2. [Features](#features)
18
+ 3. [Design/Architecture](#designarchitecture)
19
+ 4. [API](#api)
20
+ 5. [Implementation](#implementation)
21
+ 6. [Release Plan](#release-plan)
22
+ 7. [Questions to be addressed](#questions-to-be-addressed)
23
+
24
+ ## Problem Statement
25
+
26
+ `ember-sortable` has been falling behind in the adoption of the on-going `Ember` upgrades. It is currently not in the right condition, in which we can upgrade the `addon` without many blockers.
27
+
28
+ This RFC is meant to describe a high level overview of a new `ember-sortable`, which will help push us adopt many of the new `Ember` features.
29
+
30
+ This RFC is **NOT** meant to show the final implementation details as implementation will be re-iterated and improved over time.
31
+
32
+ ## Features
33
+
34
+ - Baked in accessibility support
35
+ - Up/down and left/right keyboard navigation
36
+ - Screen reader announcement
37
+ - Focus management
38
+ - Visual indicators
39
+ - Direction-agnostic (in terms of mouse drag)
40
+ - Keyboard navigation is limited to up/down/left/right
41
+ - Allows nested sortable elements
42
+ - Adoption of modern Ember testing infrastructure
43
+ - Animation
44
+ - Built with composability and customizability in mind
45
+
46
+ ## Design/Architecture
47
+
48
+ ### 1. Contextual Components
49
+
50
+ The new ember sortable will be designed using contextual components.
51
+ It will be made up of 3 main components:
52
+
53
+ #### Sortable-group
54
+
55
+ Represents the entire sortable component.
56
+
57
+ - requires a group of models to sort. The model will be a shallow copy and will not modify the given group of models.
58
+ - contains all of main logic that makes `ember-sortable` work.
59
+ - yields `sortable-item` and other properties as needed.
60
+
61
+ #### Sortable-item
62
+
63
+ Represents the individual `model` of the group of models.
64
+
65
+ - yields `sortable-handle` and other properties as needed.
66
+
67
+ #### Sortable-handle
68
+
69
+ Represents the handle of each `sortable-item`.
70
+
71
+ This is the bread and butter of the entire component because it is the entrypoint that allows us to start sorting.
72
+
73
+ This component hooks up your custom handle to the mainframe, `sortable-group`.
74
+
75
+ - yields other properties as needed.
76
+
77
+ #### Conceptual Example
78
+
79
+ ```javascript
80
+ {{#ember-sortable::sortable-group
81
+ modelGroup=modelGroup
82
+ onSubmit=(action "onSubmit")
83
+ onDragStart=(action "onDragStart")
84
+ onDragEnd=(action "onDragEnd")
85
+ as |group|
86
+ }}
87
+ {{#each group.modelGroup as |singleModel index|}}
88
+ {{#group.item
89
+ model=singleModel
90
+ index=index
91
+ as |item|
92
+ }}
93
+ {{!-- You can nest another "sortable-group" here --}}
94
+ {{!-- Content goes here --}}
95
+ {{#item.handle}}
96
+ {{!-- Handle goes here --}}
97
+ {{/item.handle}}
98
+ {{/group.item}}
99
+ {{/each}}
100
+ {{/ember-sortable::sortable-group}}
101
+ ```
102
+
103
+ ### 2. Event driven
104
+
105
+ Similar to the current `ember-sortable`, the sorting behavior is going to be mainly based on `events`. `Animation` might be an exception.
106
+ We will utilize the [Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API), [Keyboard Event API](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent), and/or [Mouse Event API](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent).
107
+
108
+ ### 3. Animation
109
+
110
+ For `animation`, we can explore different methods, e.g custom implementation, external animation library. However, the final decision will hopefully satisify the following:
111
+
112
+ - minimize expensive DOM operations, e.g `getComputedStyle`, `Ember runloop scheduling`.
113
+ - If we want to test animations, the `animation` have to be deterministic enough for us to test reliably.
114
+
115
+ ### 4. Accessibility
116
+
117
+ This section provides a high level overview of how we will address the issue of `accessibility`.
118
+
119
+ **The accessibility solutions described are directly referenced from this awesome [codepen demo](https://codepen.io/drewlee/project/full/XWNLeE) made by Andrew Lee.**
120
+
121
+ #### Keyboard Navigation
122
+
123
+ **1. Keyboard Reorder Mode**
124
+
125
+ - To initiate `keyboardReorderMode`, a `sortable-handle` must be `focused` and an `Enter/Space` must be pressed.
126
+ - This operation selects `sortable-item` parent of the `sortable-handle` and enable sorting the `sortable-item` within the `sortable-group` via up/down/left/right arrow keys.
127
+ - We will set `sortable-group` as a dedicated `container` by adding `role` attribute and programmatically set the `focus` onto it.
128
+ - We will create a `visual color indicator` around the selected `sortable-item`. A screen reader announcement will also be made to inform the user that the `sortable-item` has been selected and `sorting` has been enabled.
129
+ - To create `visual indicators`, `sortable-item` will append some `classes`, which by default tries to create `arrow` visual indcators around `sortable-item`. However, consumers are free to override the `class` to customize their own. `Visual indicators` will move as the`sortable-item` moves.
130
+
131
+ **2. Commit**
132
+
133
+ - Every navigation will reorder the components in the UI. However, the reorder will not be `committed` until an `Enter/Space` key has been pressed. If committed, the `focus` will move from the `sortable-group` back to the `sortable-handle`. `role` will be removed.
134
+
135
+ **3. Reset**
136
+
137
+ - The user can also exit `keyboardReorderMode` via `Escape` key or on focus lost. This will reset the reordering back to its initial state and `focus` is retained on the `sortable-handle`. `role` will be removed.
138
+ - We should not need animation for this.
139
+
140
+ #### Drag Drop
141
+
142
+ Drag drop will retain the same behavior as the current `ember-sortable`.
143
+
144
+ #### Screen Reader Announcements
145
+
146
+ - Internally, we can use a `announceActionConfig` object to map an `action` to a `text`. We can have some default texts.
147
+ - To support `i18n`, the consumer can supply their own `announceActionConfig` with their translated strings.
148
+
149
+ ### 5. Utility Classes and Functions
150
+
151
+ We should create a utility class to abstract as much work as we can from the components.
152
+
153
+ For example, a `keyboardManager` class can be used to maintain a history of our operations as well as provide the functionalities to perform the reordering logic during `keyboard navigation`.
154
+
155
+ ### 6. Testing
156
+
157
+ The current `ember-sortable` implements two test helpers
158
+
159
+ - reorder
160
+ - drag
161
+
162
+ Both are heavily DOM calculation driven and can sometimes be hard and unreliable to use.
163
+
164
+ I propose new `event driven` test helpers and perhaps remove `reorder` as it is just a combination of `drag`. Internally, these would be just triggering `events`, which should be determinstic to test.
165
+
166
+ - drag (for mouse)
167
+ - move (for keyboard)
168
+ - reorder?
169
+
170
+ We can implement another `drag` that is dedicated to testing `animation`.
171
+
172
+ ## API
173
+
174
+ TBD
175
+
176
+ ## Implementation
177
+
178
+ **NOTE**
179
+ This is an example to help give an idea of what it could look like.
180
+ This is **NOT** meant to be the final implementation.
181
+ This example does **NOT** contain how we would handle animation and horizontal keyboard sorting.
182
+ While creating this, I used the name `draggable` instead of `ember-sortable`.
183
+
184
+ ### draggable-group
185
+
186
+ #### hbs
187
+
188
+ ```javascript
189
+ {{yield
190
+ (hash
191
+ item=(
192
+ component "drag-drop@-private/draggable-item"
193
+ onSelect=(action "onModelSelect")
194
+ onDragStart=(action "onModelDragStart")
195
+ onDragEnter=(action "onModelDragEnter")
196
+ isKeyboardReorderModeEnabled=isKeyboardReorderModeEnabled
197
+ selectedModelIndex=selectedModelIndex
198
+ maxIndex=maxIndex
199
+ itemName=itemName
200
+ )
201
+ modelGroup=modelGroupCopy
202
+ )
203
+ }}
204
+ ```
205
+
206
+ #### js
207
+
208
+ ```javascript
209
+ /**
210
+ * This component supports re-ordering items in a group via drag-drop and keyboard navigation.
211
+ * The component is built with accessibility in mind. The logic of the component are mostly derived from https://codepen.io/drewlee/project/full/XWNLeE.
212
+ *
213
+ * @param {Ember.Array} modelGroup the group of models to be rearranged.
214
+ * @param {String} itemName A name for the individual models, used for creating more meaningful a11y announcements.
215
+ * @param {Function} [onSubmit] An optional callback for when position rearrangements are confirmed.
216
+ * @param {Function} [onDragStart] An optional callback for when the user starts dragging a model.
217
+ * @param {Funtion} [onDragEnd] An optional callback for when the user has finished dragging a model.
218
+ *
219
+ * @module drag-drop/draggable-group
220
+ * @example
221
+ * {{#drag-drop::draggable-group
222
+ * onSubmit=(action onSubmit)
223
+ * modelGroup=modelGroup
224
+ * onDragStart=(action onDragStart)
225
+ * onDragEnd=(action onDragEnd)
226
+ * itemName=itemName
227
+ * as |group|
228
+ * }}
229
+ * {{#each group.modelGroup as |singleModel index|}}
230
+ * {{#group.item
231
+ * model=singleModel
232
+ * index=index
233
+ * as |item|
234
+ * }}
235
+ * {{singleModel}}
236
+ * {{#item.handle}}
237
+ * Handle
238
+ * {{/item.handle}}
239
+ * {{/group.item}}
240
+ * {{/each}}
241
+ * {{/drag-drop::draggable-group}}
242
+ */
243
+ export default Component.extend({
244
+ layout,
245
+ attributeBindings: ['tabindex', 'role', 'dataTestDragDropDraggableGroup:data-test-drag-drop-draggable-group'],
246
+ // data-test selector
247
+ dataTestDragDropDraggableGroup: true,
248
+ /**
249
+ * @param {Boolean} isKeyboardReorderModeEnabled if the keyboard navigation can be utilized.
250
+ * @param {Object} selectedModel the selected model that the user is repositioning.
251
+ * @param {Integer} selectedModelIndex the position of the selected model in the DOM.
252
+ * @param {Object} targetModel the targeted model that the user is dropping the dragged model onto.
253
+ * @param {Integer} targetModelIndex the index of the targeted model.
254
+ * @param {Boolean} isRetainingFocus if the focus is being managed. This is usually to prevent incorrect focus when the DOM is not ready.
255
+ * @param {Integer} tabindex tabindex attribute.
256
+ * @param {String} role role attribute.
257
+ * @param {Integer} maxIndex the highest possible index within the group.
258
+ */
259
+ isKeyboardReorderModeEnabled: false,
260
+ selectedModel: null,
261
+ selectedModelIndex: -1,
262
+ targetModel: null,
263
+ targetModelIndex: -1,
264
+ isRetainingFocus: false,
265
+ tabindex: undefined,
266
+ role: undefined,
267
+ maxIndex: alias('modelGroup.length'),
268
+
269
+ a11yNotification: service('a11y-notification'),
270
+ i18n: service('i18n'),
271
+
272
+ init() {
273
+ this._super(...arguments);
274
+
275
+ this._assertProperties();
276
+
277
+ // Create a shallow copy of the origina group to prevent mutating the original group.
278
+ const modelGroupCopy = [...get(this, 'modelGroup')];
279
+ setProperties(this, {
280
+ modelGroupCopy,
281
+ keyboardReorderManager: new KeyboardReorderManager(modelGroupCopy),
282
+ });
283
+ },
284
+
285
+ /**
286
+ * Explanation
287
+ * 1. `KeyboardReorderMode` is disabled: users can activate it via ENTER or SPACE.
288
+ * 2. `KeyboardReorderMode` is enabled: users can reorder via UP or DOWN arrow keys. TODO: Expand to more keys, e.g LEFT, RIGHT
289
+ * 3. `KeyboardReorderMode` is enabled: users can finalize/save the reordering via ENTER or SPACE.
290
+ * 4. `KeyboardReorderMode` is enabled: users can discard the reordering via ESC.
291
+ *
292
+ * @param {Event} evt a HTML event
293
+ */
294
+ keyDown(evt) {
295
+ const isKeyboardReorderModeEnabled = get(this, 'isKeyboardReorderModeEnabled');
296
+
297
+ if (!isKeyboardReorderModeEnabled && (isEnterKey(evt) || isSpaceKey(evt))) {
298
+ this._enableKeyboardReorderMode();
299
+ this._setupA11yApplicationContainer();
300
+
301
+ set(this, 'isRetainingFocus', true);
302
+ mutateDOM(() => {
303
+ this.element.focus();
304
+ set(this, 'isRetainingFocus', false);
305
+ });
306
+
307
+ evt.preventDefault();
308
+ // In case of nested groups, prevent the keyDown from bubbling up to the parent.
309
+ evt.stopPropagation();
310
+ return;
311
+ }
312
+
313
+ if (isKeyboardReorderModeEnabled) {
314
+ const { selectedModelIndex, maxIndex } = getProperties(this, 'selectedModelIndex', 'maxIndex');
315
+ if (isDownArrowKey(evt)) {
316
+ const newIndex = Math.min(selectedModelIndex + 1, maxIndex - 1);
317
+ this._moveItem(selectedModelIndex, newIndex);
318
+
319
+ set(this, 'selectedModelIndex', newIndex);
320
+
321
+ this._announceAction(ANNOUNCEMENT_ACTION_TYPES.MOVE, {
322
+ index: newIndex,
323
+ maxIndex,
324
+ });
325
+ evt.preventDefault();
326
+ } else if (isUpArrowKey(evt)) {
327
+ const newIndex = Math.max(selectedModelIndex - 1, 0);
328
+
329
+ this._moveItem(selectedModelIndex, newIndex);
330
+
331
+ set(this, 'selectedModelIndex', newIndex);
332
+
333
+ this._announceAction(ANNOUNCEMENT_ACTION_TYPES.MOVE, {
334
+ index: newIndex,
335
+ maxIndex,
336
+ });
337
+ // prevent mouse scroll
338
+ evt.preventDefault();
339
+ } else if (isEnterKey(evt) || isSpaceKey(evt)) {
340
+ set(this, 'isRetainingFocus', true);
341
+ this._confirmKeyboardSelection();
342
+
343
+ readDOM(() => {
344
+ if (IS_BROWSER) {
345
+ this.element.querySelectorAll(`[${DRAGGABLE_HANDLE_ATTRIBUTE}]`)[selectedModelIndex].focus();
346
+ set(this, 'isRetainingFocus', false);
347
+ }
348
+ });
349
+
350
+ evt.preventDefault();
351
+ } else if (isEscapeKey(evt)) {
352
+ const keyboardReorderManager = get(this, 'keyboardReorderManager');
353
+ const record = keyboardReorderManager.getRecord();
354
+ const lastIndex = record ? record.fromIndex : selectedModelIndex;
355
+
356
+ set(this, 'isRetainingFocus', true);
357
+ this._cancelKeyboardSelection();
358
+
359
+ readDOM(() => {
360
+ if (IS_BROWSER) {
361
+ this.element.querySelectorAll(`[${DRAGGABLE_HANDLE_ATTRIBUTE}]`)[lastIndex].focus();
362
+ set(this, 'isRetainingFocus', false);
363
+ }
364
+ });
365
+
366
+ evt.preventDefault();
367
+ }
368
+ }
369
+ // In case of nested groups, prevent the keyDown from bubbling up to the parent.
370
+ evt.stopPropagation();
371
+ },
372
+
373
+ /**
374
+ * If focus management is not finished and the current focused element is not the handle or descendant of the handle: Cancel
375
+ */
376
+ focusOut(evt) {
377
+ if (IS_BROWSER && !get(this, 'isRetainingFocus') && !this._isElementWithinHandle(document.activeElement)) {
378
+ this._cancelKeyboardSelection();
379
+ }
380
+ evt.stopPropagation();
381
+ },
382
+
383
+ /**
384
+ * Reset any ongoing keyboard selections and disable keyboard navigation because drag is taking over.
385
+ * Invokes any optional `onDragStart` callback.
386
+ *
387
+ * @param {Event} evt a HTML event
388
+ */
389
+ dragStart(evt) {
390
+ const keyboardReorderManager = get(this, 'keyboardReorderManager');
391
+
392
+ keyboardReorderManager.reset();
393
+ this._disableKeyboardReorderMode();
394
+ evt.dataTransfer.setData('text/plain', '');
395
+ tryInvoke(this, 'onDragStart');
396
+ // In case of nested models, prevent the parent from being dragged instead of the child.
397
+ evt.stopPropagation();
398
+ },
399
+
400
+ /**
401
+ * Invoke optional `onDragEnd` callback.
402
+ */
403
+ dragEnd(evt) {
404
+ tryInvoke(this, 'onDragEnd');
405
+ // In case of nested models, prevent invoking parent's handler.
406
+ evt.stopPropagation();
407
+ },
408
+
409
+ /**
410
+ * Need `preventDefault` to allow `drop` to happen.
411
+ *
412
+ * @param {Event} evt a HTML event
413
+ */
414
+ dragOver(evt) {
415
+ evt.preventDefault();
416
+ },
417
+
418
+ /**
419
+ * If target drop area is within the group, execute the `drop` by
420
+ * 1. Moving the `selectedModel` to the `targetModel` position
421
+ * 2. Invoke the `onSubmit` callback since the new order is confirmed.
422
+ * 3. Reset everything.
423
+ *
424
+ * @param {Event} evt a HTML event
425
+ */
426
+ drop(evt) {
427
+ const dropTarget = evt.target;
428
+ evt.preventDefault();
429
+ if (this._isElementWithinDrop(dropTarget)) {
430
+ const { keyboardReorderManager, selectedModel, selectedModelIndex, targetModel, targetModelIndex } =
431
+ getProperties(
432
+ this,
433
+ 'keyboardReorderManager',
434
+ 'selectedModel',
435
+ 'selectedModelIndex',
436
+ 'targetModel',
437
+ 'targetModelIndex'
438
+ );
439
+ this._moveItem(selectedModelIndex, targetModelIndex);
440
+ tryInvoke(this, 'onSubmit', [selectedModel, selectedModelIndex, targetModel, targetModelIndex]);
441
+ keyboardReorderManager.clearRecord();
442
+ }
443
+ this._resetModelSelection();
444
+ // In case of nested models, prevent the parent from being dropped instead of the child.
445
+ evt.stopPropagation();
446
+ },
447
+
448
+ /**
449
+ * Confirms the keyboard selection by:
450
+ * 1. Clearing the tracked record movement.
451
+ * 2. Disabling keyboard navigation.
452
+ * 3. Resets model selections.
453
+ * 4. Tear down a11y container.
454
+ * 5. Invoke `onSubmit` callback.
455
+ * 6. Announce the change.
456
+ */
457
+ _confirmKeyboardSelection() {
458
+ const { keyboardReorderManager, selectedModel, selectedModelIndex, targetModel, targetModelIndex } = getProperties(
459
+ this,
460
+ 'keyboardReorderManager',
461
+ 'selectedModel',
462
+ 'selectedModelIndex',
463
+ 'targetModel',
464
+ 'targetModelIndex'
465
+ );
466
+
467
+ keyboardReorderManager.clearRecord();
468
+ this._disableKeyboardReorderMode();
469
+ this._tearDownA11yApplicationContainer();
470
+
471
+ tryInvoke(this, 'onSubmit', [selectedModel, selectedModelIndex, targetModel, targetModelIndex]);
472
+
473
+ this._resetModelSelection();
474
+ this._announceAction(ANNOUNCEMENT_ACTION_TYPES.CONFIRM);
475
+ },
476
+
477
+ /**
478
+ * Cancels the keyboard selection by:
479
+ * 1. Disabling the keyboard navigation.
480
+ * 2. Reset model selections.
481
+ * 3. Reset any tracked movement by reverting the move and clearing the record.
482
+ * 4. Tear down a11y container.
483
+ * 4. Announce the change.
484
+ */
485
+ _cancelKeyboardSelection() {
486
+ const keyboardReorderManager = get(this, 'keyboardReorderManager');
487
+
488
+ this._disableKeyboardReorderMode();
489
+
490
+ this._resetModelSelection();
491
+
492
+ keyboardReorderManager.reset();
493
+
494
+ this._tearDownA11yApplicationContainer();
495
+
496
+ this._announceAction(ANNOUNCEMENT_ACTION_TYPES.CANCEL);
497
+ },
498
+
499
+ /**
500
+ * Rearranges the order of models inside `modelGroupCopy`.
501
+ *
502
+ * @param {Integer} oldIndex the position of the model to be moved.
503
+ * @param {Integer} newIndex the position that the model is moving to.
504
+ */
505
+ _moveItem(oldIndex, newIndex) {
506
+ const keyboardReorderManager = get(this, 'keyboardReorderManager');
507
+
508
+ keyboardReorderManager.move(oldIndex, newIndex);
509
+ },
510
+
511
+ /**
512
+ * Reset model selection
513
+ */
514
+ _resetModelSelection() {
515
+ setProperties(this, {
516
+ selectedModel: null,
517
+ selectedModelIndex: -1,
518
+ targetModel: null,
519
+ targetedModelIndex: -1,
520
+ });
521
+ },
522
+
523
+ /**
524
+ * Sets up a `role=application` container.
525
+ */
526
+ _setupA11yApplicationContainer() {
527
+ setProperties(this, {
528
+ role: 'application',
529
+ tabindex: -1,
530
+ });
531
+ },
532
+
533
+ /**
534
+ * Tears down the `role=application` container.
535
+ */
536
+ _tearDownA11yApplicationContainer() {
537
+ setProperties(this, {
538
+ role: undefined,
539
+ tabindex: undefined,
540
+ });
541
+ },
542
+
543
+ /**
544
+ * Asserts that required properties are defined correctly.
545
+ */
546
+ _assertProperties() {
547
+ assert('modelGroup is required', get(this, 'modelGroup'));
548
+ assert('itemName is required', get(this, 'itemName'));
549
+ },
550
+
551
+ /**
552
+ * Enables keyboard navigation.
553
+ */
554
+ _enableKeyboardReorderMode() {
555
+ set(this, 'isKeyboardReorderModeEnabled', true);
556
+ },
557
+
558
+ /**
559
+ * Disables keyboard navigation.
560
+ */
561
+ _disableKeyboardReorderMode() {
562
+ set(this, 'isKeyboardReorderModeEnabled', false);
563
+ },
564
+
565
+ /**
566
+ * Checks if the given element is a descedant of a handle.
567
+ *
568
+ * @param {Element} element a DOM element.
569
+ */
570
+ _isElementWithinHandle(element) {
571
+ return element.closest(`#${this.element.id} [${DRAGGABLE_HANDLE_ATTRIBUTE}]`);
572
+ },
573
+
574
+ /**
575
+ * Checks if the given element is a descedant of a droppable region.
576
+ *
577
+ * @param {Element} element a DOM element
578
+ */
579
+ _isElementWithinDrop(element) {
580
+ return element.closest(`#${this.element.id} [${DRAGGABLE_ITEM_ATTRIBUTE}]`);
581
+ },
582
+
583
+ /**
584
+ * Helper method for extracting i18n strings used in JS
585
+ *
586
+ * @method geti18nMessage
587
+ * @param {String} key - Unique key that identifies an i18n string
588
+ * @param {Object} data - Dynamic segments of an i18n string
589
+ * @return {Function}
590
+ */
591
+ _geti18nMessage(key, data) {
592
+ const messageRenderer = get(this, 'i18n').getMessageRenderer(get(this, 'layout'), key);
593
+ return messageRenderer([data]);
594
+ },
595
+
596
+ /**
597
+ * Announce action for screen reader.
598
+ *
599
+ * @param {Enum} announcementType
600
+ * @param {Object} announcementConfig
601
+ */
602
+ _announceAction(announcementType, announcementConfig = {}) {
603
+ const a11yNotification = get(this, 'a11yNotification');
604
+
605
+ let message;
606
+ const itemName = get(this, 'itemName');
607
+ const { index, maxIndex } = announcementConfig;
608
+
609
+ switch (announcementType) {
610
+ case ANNOUNCEMENT_ACTION_TYPES.ACTIVATE:
611
+ message = this._geti18nMessage('i18n_activate', {
612
+ itemName,
613
+ index,
614
+ maxIndex,
615
+ });
616
+ break;
617
+ case ANNOUNCEMENT_ACTION_TYPES.MOVE:
618
+ message = this._geti18nMessage('i18n_move', {
619
+ itemName,
620
+ index,
621
+ maxIndex,
622
+ });
623
+ break;
624
+ case ANNOUNCEMENT_ACTION_TYPES.CONFIRM:
625
+ message = this._geti18nMessage('i18n_confirm', { itemName });
626
+ break;
627
+ case ANNOUNCEMENT_ACTION_TYPES.CANCEL:
628
+ message = this._geti18nMessage('i18n_cancel', { itemName });
629
+ break;
630
+ default:
631
+ break;
632
+ }
633
+ a11yNotification.setTextInLiveRegion(message);
634
+ },
635
+
636
+ actions: {
637
+ /**
638
+ * Enables keyboard navigation.
639
+ */
640
+ enableKeyboardReorderMode() {
641
+ this._enableKeyboardReorderMode();
642
+ },
643
+
644
+ /**
645
+ * Enables keyboard navigation.
646
+ */
647
+ disableKeyboardReorderMode() {
648
+ this._disableKeyboardReorderMode();
649
+ },
650
+
651
+ /**
652
+ * `draggable-item` invokes this when it is selected via keyboard.
653
+ *
654
+ * @param {Object} model the selected model.
655
+ * @param {Integer} index the position of the selected model in the DOM.
656
+ */
657
+ onModelSelect(model, index) {
658
+ setProperties(this, {
659
+ selectedModel: model,
660
+ selectedModelIndex: index,
661
+ });
662
+ },
663
+
664
+ /**
665
+ * `draggable-item` invokes this when it starts being dragged.
666
+ *
667
+ * @param {Object} model the model being dragged.
668
+ * @param {Integer} index the position of the dragged model in the DOM.
669
+ */
670
+ onModelDragStart(model, index) {
671
+ setProperties(this, {
672
+ selectedModel: model,
673
+ selectedModelIndex: index,
674
+ });
675
+ },
676
+
677
+ /**
678
+ * `draggable-item` invokes this when a dragged model enters the target model's region
679
+ *
680
+ * @param {Object} model the target model.
681
+ * @param {Integer} index the position of the target model in the DOM.
682
+ */
683
+ onModelDragEnter(model, index) {
684
+ setProperties(this, {
685
+ targetModel: model,
686
+ targetModelIndex: index,
687
+ });
688
+ },
689
+ },
690
+ });
691
+ ```
692
+
693
+ ### draggable-item
694
+
695
+ #### hbs
696
+
697
+ ```javascript
698
+ {{yield
699
+ (hash
700
+ handle=(component "drag-drop@-private/draggable-handle"
701
+ enableDrag=(action "enableDrag")
702
+ disableDrag=(action "disableDrag")
703
+ isKeyboardReorderModeEnabled=isKeyboardReorderModeEnabled
704
+ selectedIndex=selectedModelIndex
705
+ index=index
706
+ maxIndex=maxIndex
707
+ a11yText=(some-i18n-util itemName=itemName)
708
+ )
709
+ )
710
+ }}
711
+ ```
712
+
713
+ #### js
714
+
715
+ ```javascript
716
+ /**
717
+ * This private component represents the individual model of `draggable-group`.
718
+ *
719
+ * Public API
720
+ * @param {Function} model the model that this component is associated with.
721
+ * @param {Function} index the position of this component in the DOM.
722
+ *
723
+ * Private API
724
+ * @param {Boolean} isKeyboardReorderModeEnabled If the keyboard navigation can be utilized.
725
+ * @param {Integer} selectedModelIndex The position of the selected model in the DOM.
726
+ * @param {Integer} maxIndex The highest possible index within the group.
727
+ * @param {String} itemName A name for the individual models, used for creating more meaningful a11y announcements.
728
+ * @param {Function} onDragStart Callback to notify `draggable-group` that this component is being dragged.
729
+ * @param {Function} onDragEnter Callback to notify `draggable-group` that this component is can be dropped on.
730
+ *
731
+ * @module drag-drop/-private/draggable-item
732
+ */
733
+ export default Component.extend({
734
+ tagName: 'div',
735
+ classNameBindings: ['isSelected:drag-drop__item--active'],
736
+ attributeBindings: [
737
+ 'draggable',
738
+ `dataDragDropItem:${DRAGGABLE_ITEM_ATTRIBUTE}`,
739
+ `dataTestDragDropDraggableItem:data-test-drag-drop-draggable-item`,
740
+ ],
741
+ // data attribute
742
+ dataDragDropItem: true,
743
+ // data-test selector
744
+ dataTestDragDropDraggableItem: true,
745
+ // native attribute allowing element to be draggable.
746
+ draggable: false,
747
+
748
+ /**
749
+ * Toggles a visual state for a11y purpose.
750
+ *
751
+ * @param {Integer} selectedModelIndex the position of the selected model in the DOM.
752
+ * @param {Integer} index the position of this model in the DOM.
753
+ */
754
+ isSelected: computed('selectedModelIndex', 'index', function getIsSelected() {
755
+ const { selectedModelIndex, index } = getProperties(this, 'selectedModelIndex', 'index');
756
+
757
+ return selectedModelIndex === index;
758
+ }),
759
+
760
+ onDragStart() {
761
+ assert('onDragStart is required');
762
+ },
763
+
764
+ onDragEnter() {
765
+ assert('onDragEnter is required');
766
+ },
767
+
768
+ init() {
769
+ this._super(...arguments);
770
+ this._assertProperties();
771
+ },
772
+
773
+ /**
774
+ * Pass the selected model up to the `draggable-group`, so the group knows which model is being selected.
775
+ *
776
+ * @param {Event} evt a HTML event.
777
+ */
778
+ keyDown(evt) {
779
+ const { isKeyboardReorderModeEnabled, model, selectedModelIndex, index } = getProperties(
780
+ this,
781
+ 'isKeyboardReorderModeEnabled',
782
+ 'model',
783
+ 'selectedModelIndex',
784
+ 'index'
785
+ );
786
+
787
+ if (
788
+ selectedModelIndex < 0 &&
789
+ (isKeyboardReorderModeEnabled || (!isKeyboardReorderModeEnabled && (isEnterKey(evt) || isSpaceKey(evt))))
790
+ ) {
791
+ this.onSelect(model, index);
792
+ }
793
+ },
794
+
795
+ /**
796
+ * Invoke `onDragStart` callback from `draggable-group`.
797
+ */
798
+ dragStart() {
799
+ this.onDragStart(get(this, 'model'), get(this, 'index'));
800
+ },
801
+
802
+ /**
803
+ * Invoke `_disableDrag` callback from `draggable-group`.
804
+ */
805
+ dragEnd() {
806
+ this._disableDrag();
807
+ },
808
+
809
+ /**
810
+ * Invoke `onDragEnter` callback from `draggable-group`.
811
+ */
812
+ dragEnter() {
813
+ this.onDragEnter(get(this, 'model'), get(this, 'index'));
814
+ },
815
+
816
+ /**
817
+ * Asserts that required properties are defined correctly.
818
+ */
819
+ _assertProperties() {
820
+ assert('isKeyboardReorderModeEnabled is required', get(this, 'isKeyboardReorderModeEnabled') !== undefined);
821
+ assert('selectedModelIndex is required', get(this, 'selectedModelIndex') !== undefined);
822
+ assert('maxIndex is required', get(this, 'maxIndex') !== undefined);
823
+ assert('itemName is required', get(this, 'itemName') !== undefined);
824
+ },
825
+
826
+ /**
827
+ * Enables this item to be draggable.
828
+ */
829
+ _enableDrag() {
830
+ set(this, 'draggable', true);
831
+ },
832
+
833
+ /**
834
+ * Disables this item from being draggable.
835
+ */
836
+ _disableDrag() {
837
+ set(this, 'draggable', false);
838
+ },
839
+
840
+ actions: {
841
+ /**
842
+ * Callback for `draggable-handle` to enable draggable.
843
+ */
844
+ enableDrag() {
845
+ this._enableDrag();
846
+ },
847
+
848
+ /**
849
+ * Callback for `draggable-handle` to disable draggable.
850
+ */
851
+ disableDrag() {
852
+ this._disableDrag();
853
+ },
854
+ },
855
+ });
856
+ ```
857
+
858
+ ### draggable-handle
859
+
860
+ #### hbs
861
+
862
+ ```javascript
863
+ {
864
+ {
865
+ yield;
866
+ }
867
+ }
868
+ <span class="visually-hidden">{{ a11yText }}</span>;
869
+ ```
870
+
871
+ #### js
872
+
873
+ ```javascript
874
+ /**
875
+ * This private component represents the `handle` for each `draggable-item` of a `draggable-group`.
876
+ *
877
+ * @param {Boolean} isKeyboardReorderModeEnabled If the keyboard navigation can be utilized.
878
+ * @param {Integer} selectedIndex The position of the selected model in the DOM.
879
+ * @param {Integer} index The position of this component in the DOM.
880
+ * @param {Integer} maxIndex The highest possible index within the group.
881
+ * @param {String} a11yText The text for this handle.
882
+ * @param {Function} enableDrag Callback to make `draggable-item` draggable.
883
+ * @param {Function} disableDrag Callback to make `draggable-item` not draggable.
884
+ *
885
+ * @module drag-drop/-private/draggable-handle
886
+ */
887
+ export default Component.extend({
888
+ /**
889
+ * <div> and tabindex: 0 is used intentionally to make the element non-interactive, so it works with Windows screen reader.
890
+ */
891
+ tagName: 'div',
892
+ tabindex: 0,
893
+ classNameBindings: ['showA11yPreviousArrow:drag-drop__handle-previous', 'showA11yNextArrow:drag-drop__handle-next'],
894
+ attributeBindings: [
895
+ `dataDragDropHandle:${DRAGGABLE_HANDLE_ATTRIBUTE}`,
896
+ 'tabindex',
897
+ 'dataTestDragDropDraggableHandle:data-test-drag-drop-draggable-handle',
898
+ ],
899
+ // data attribute
900
+ dataDragDropHandle: true,
901
+ // data-test selector
902
+ dataTestDragDropDraggableHandle: true,
903
+
904
+ /**
905
+ * Shows the previous arrow.
906
+ * 1. keyboard navigation is enabled.
907
+ * 2. This handle is selected.
908
+ * 3. This handle is not the first handle.
909
+ *
910
+ * @param {Integer} index The position of this component in the DOM.
911
+ * @param {Integer} selectedIndex The position of the selected model in the DOM.
912
+ * @param {Boolean} isKeyboardReorderModeEnabled If the keyboard navigation can be utilized.
913
+ */
914
+ showA11yPreviousArrow: computed(
915
+ 'index',
916
+ 'selectedIndex',
917
+ 'isKeyboardReorderModeEnabled',
918
+ function getShowA11yPreviousArrow() {
919
+ const { index, selectedIndex, isKeyboardReorderModeEnabled } = getProperties(
920
+ this,
921
+ 'index',
922
+ 'selectedIndex',
923
+ 'isKeyboardReorderModeEnabled'
924
+ );
925
+ return isKeyboardReorderModeEnabled && selectedIndex === index && index > 0;
926
+ }
927
+ ),
928
+
929
+ /**
930
+ * Shows the next arrow if
931
+ * 1. keyboard navigation is enabled.
932
+ * 2. This handle is selected.
933
+ * 3. This handle is not the last handle.
934
+ *
935
+ * @param {Integer} index The position of this component in the DOM.
936
+ * @param {Integer} selectedIndex The position of the selected model in the DOM.
937
+ * @param {Boolean} isKeyboardReorderModeEnabled If the keyboard navigation can be utilized.
938
+ */
939
+ showA11yNextArrow: computed(
940
+ 'index',
941
+ 'selectedIndex',
942
+ 'maxIndex',
943
+ 'isKeyboardReorderModeEnabled',
944
+ function getShowA11yNextArrow() {
945
+ const { index, selectedIndex, maxIndex, isKeyboardReorderModeEnabled } = getProperties(
946
+ this,
947
+ 'index',
948
+ 'maxIndex',
949
+ 'selectedIndex',
950
+ 'isKeyboardReorderModeEnabled'
951
+ );
952
+ return isKeyboardReorderModeEnabled && selectedIndex === index && index < maxIndex - 1;
953
+ }
954
+ ),
955
+
956
+ enableDrag() {
957
+ assert('enableDrag is required');
958
+ },
959
+
960
+ disableDrag() {
961
+ assert('disableDrag is required');
962
+ },
963
+
964
+ init() {
965
+ this._super(...arguments);
966
+
967
+ this._assertProperties();
968
+ },
969
+
970
+ /**
971
+ * Asserts that required properties are defined correctly.
972
+ */
973
+ _assertProperties() {
974
+ assert(`a11yText is required`, get(this, 'a11yText'));
975
+ assert(`index is required`, get(this, 'index') !== undefined);
976
+ assert(`maxIndex is required`, get(this, 'maxIndex') !== undefined);
977
+ assert(`selectedIndex is required`, get(this, 'selectedIndex') !== undefined);
978
+ assert(`isKeyboardReorderModeEnabled is required`, get(this, 'isKeyboardReorderModeEnabled') !== undefined);
979
+ },
980
+
981
+ /**
982
+ * Enables `draggable-item` to be draggable.
983
+ */
984
+ mouseDown() {
985
+ this.enableDrag();
986
+ },
987
+
988
+ /**
989
+ * Disable `draggable-item` from being draggable.
990
+ */
991
+ mouseUp() {
992
+ this.disableDrag();
993
+ },
994
+ });
995
+ ```
996
+
997
+ ### utils/constants.js
998
+
999
+ ```javascript
1000
+ export const DRAGGABLE_HANDLE_ATTRIBUTE = 'data-drag-drop-draggable-handle';
1001
+ export const DRAGGABLE_ITEM_ATTRIBUTE = 'data-drag-drop-draggable-item';
1002
+ export const ANNOUNCEMENT_ACTION_TYPES = {
1003
+ ACTIVATE: true,
1004
+ MOVE: true,
1005
+ CONFIRM: true,
1006
+ CANCEL: true,
1007
+ };
1008
+ ```
1009
+
1010
+ ### utils/keyboard-reorder-manager.js
1011
+
1012
+ ```javascript
1013
+ class ReorderRecord {
1014
+ constructor(fromIndex, toIndex) {
1015
+ this.fromIndex = fromIndex;
1016
+ this.toIndex = toIndex;
1017
+ }
1018
+ }
1019
+
1020
+ export default class KeyboardReorderManager {
1021
+ constructor(modelGroup) {
1022
+ this._modelGroup = modelGroup;
1023
+ this._record = null;
1024
+ }
1025
+
1026
+ move(fromIndex, toIndex) {
1027
+ if (fromIndex < 0 || toIndex < 0 || fromIndex >= this._modelGroup.length || toIndex >= this._modelGroup.length) {
1028
+ return;
1029
+ }
1030
+
1031
+ const modelToBeMoved = this._modelGroup.objectAt(fromIndex);
1032
+ this._modelGroup.removeAt(fromIndex);
1033
+ this._modelGroup.insertAt(toIndex, modelToBeMoved);
1034
+
1035
+ if (!this._record) {
1036
+ this._record = new ReorderRecord(fromIndex, toIndex);
1037
+ } else {
1038
+ this._record.toIndex = toIndex;
1039
+ }
1040
+ }
1041
+
1042
+ getModel() {
1043
+ return this._modelGroup;
1044
+ }
1045
+
1046
+ getRecord() {
1047
+ return this._record;
1048
+ }
1049
+
1050
+ clearRecord() {
1051
+ this._record = null;
1052
+ }
1053
+
1054
+ reset() {
1055
+ if (this._record) {
1056
+ this.move(this._record.toIndex, this._record.fromIndex);
1057
+ }
1058
+ this.clearRecord();
1059
+ }
1060
+ }
1061
+ ```
1062
+
1063
+ ## Release Plan
1064
+
1065
+ ### 2.0
1066
+
1067
+ - New API with backward incompatible changes
1068
+ - Drag drop
1069
+ - Animation
1070
+ - Test helper
1071
+ - Test Infrastructure Modernization
1072
+ - remove `registerAsyncHelper`
1073
+ - `module` and `setupHooks` syntax
1074
+ - Remove `jQuery` in favor of vanilla.
1075
+ - Migration Guide
1076
+ - 1.x.x -> 2.x.x
1077
+
1078
+ ### 2.1
1079
+
1080
+ - Keyboard support
1081
+ - Keyboard navigation (left/right/up/down)
1082
+ - Commit
1083
+ - Reset
1084
+
1085
+ ### 2.2
1086
+
1087
+ - Accessibility support
1088
+ - Screen reader announcements
1089
+ - Focus management
1090
+ - Semantic markup and attributes
1091
+
1092
+ ### 2.3
1093
+
1094
+ - Nesting support
1095
+
1096
+ ## Questions to be addressed
1097
+
1098
+ 1. With the introduction of Ember Octane and glimmer components, should we use any of their features? How backward compatible should this be?
1099
+
1100
+ 2. Will an external animation library be of high value to us? If so, will the extra overhead be problematic?