@react-aria/dnd 3.9.3 → 3.10.1

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 (40) hide show
  1. package/dist/DragManager.main.js +79 -12
  2. package/dist/DragManager.main.js.map +1 -1
  3. package/dist/DragManager.mjs +79 -12
  4. package/dist/DragManager.module.js +79 -12
  5. package/dist/DragManager.module.js.map +1 -1
  6. package/dist/DropTargetKeyboardNavigation.main.js +201 -0
  7. package/dist/DropTargetKeyboardNavigation.main.js.map +1 -0
  8. package/dist/DropTargetKeyboardNavigation.mjs +196 -0
  9. package/dist/DropTargetKeyboardNavigation.module.js +196 -0
  10. package/dist/DropTargetKeyboardNavigation.module.js.map +1 -0
  11. package/dist/ListDropTargetDelegate.main.js.map +1 -1
  12. package/dist/ListDropTargetDelegate.module.js.map +1 -1
  13. package/dist/types.d.ts +7 -1
  14. package/dist/types.d.ts.map +1 -1
  15. package/dist/useDrop.main.js.map +1 -1
  16. package/dist/useDrop.module.js.map +1 -1
  17. package/dist/useDropIndicator.main.js +15 -7
  18. package/dist/useDropIndicator.main.js.map +1 -1
  19. package/dist/useDropIndicator.mjs +15 -7
  20. package/dist/useDropIndicator.module.js +15 -7
  21. package/dist/useDropIndicator.module.js.map +1 -1
  22. package/dist/useDroppableCollection.main.js +23 -98
  23. package/dist/useDroppableCollection.main.js.map +1 -1
  24. package/dist/useDroppableCollection.mjs +23 -98
  25. package/dist/useDroppableCollection.module.js +23 -98
  26. package/dist/useDroppableCollection.module.js.map +1 -1
  27. package/dist/useDroppableItem.main.js +4 -2
  28. package/dist/useDroppableItem.main.js.map +1 -1
  29. package/dist/useDroppableItem.mjs +4 -2
  30. package/dist/useDroppableItem.module.js +4 -2
  31. package/dist/useDroppableItem.module.js.map +1 -1
  32. package/package.json +17 -12
  33. package/src/.DS_Store +0 -0
  34. package/src/DragManager.ts +66 -17
  35. package/src/DropTargetKeyboardNavigation.ts +273 -0
  36. package/src/ListDropTargetDelegate.ts +1 -1
  37. package/src/useDrop.ts +0 -1
  38. package/src/useDropIndicator.ts +17 -11
  39. package/src/useDroppableCollection.ts +23 -123
  40. package/src/useDroppableItem.ts +7 -4
@@ -16,7 +16,7 @@ import {DragEndEvent, DragItem, DropActivateEvent, DropEnterEvent, DropEvent, Dr
16
16
  import {getDragModality, getTypes} from './utils';
17
17
  import {isVirtualClick, isVirtualPointerEvent} from '@react-aria/utils';
18
18
  import type {LocalizedStringFormatter} from '@internationalized/string';
19
- import {useEffect, useState} from 'react';
19
+ import {RefObject, useEffect, useState} from 'react';
20
20
 
21
21
  let dropTargets = new Map<Element, DropTarget>();
22
22
  let dropItems = new Map<Element, DroppableItem>();
@@ -32,7 +32,8 @@ interface DropTarget {
32
32
  onDropTargetEnter?: (target: DroppableCollectionTarget | null) => void,
33
33
  onDropActivate?: (e: DropActivateEvent, target: DroppableCollectionTarget | null) => void,
34
34
  onDrop?: (e: DropEvent, target: DroppableCollectionTarget | null) => void,
35
- onKeyDown?: (e: KeyboardEvent, dragTarget: DragTarget) => void
35
+ onKeyDown?: (e: KeyboardEvent, dragTarget: DragTarget) => void,
36
+ activateButtonRef?: RefObject<FocusableElement | null>
36
37
  }
37
38
 
38
39
  export function registerDropTarget(target: DropTarget) {
@@ -47,7 +48,8 @@ export function registerDropTarget(target: DropTarget) {
47
48
  interface DroppableItem {
48
49
  element: FocusableElement,
49
50
  target: DroppableCollectionTarget,
50
- getDropOperation?: (types: Set<string>, allowedOperations: DropOperation[]) => DropOperation
51
+ getDropOperation?: (types: Set<string>, allowedOperations: DropOperation[]) => DropOperation,
52
+ activateButtonRef?: RefObject<FocusableElement | null>
51
53
  }
52
54
 
53
55
  export function registerDropItem(item: DroppableItem) {
@@ -241,15 +243,26 @@ class DragSession {
241
243
  this.cancelEvent(e);
242
244
 
243
245
  if (e.key === 'Enter') {
244
- if (e.altKey) {
245
- this.activate();
246
+ if (e.altKey || this.getCurrentActivateButton()?.contains(e.target as Node)) {
247
+ this.activate(this.currentDropTarget, this.currentDropItem);
246
248
  } else {
247
249
  this.drop();
248
250
  }
249
251
  }
250
252
  }
251
253
 
254
+ getCurrentActivateButton(): FocusableElement | null {
255
+ return this.currentDropItem?.activateButtonRef?.current ?? this.currentDropTarget?.activateButtonRef?.current ?? null;
256
+ }
257
+
252
258
  onFocus(e: FocusEvent) {
259
+ let activateButton = this.getCurrentActivateButton();
260
+ if (e.target === activateButton) {
261
+ // TODO: canceling this breaks the focus ring. Revisit when we support tabbing.
262
+ this.cancelEvent(e);
263
+ return;
264
+ }
265
+
253
266
  // Prevent focus events, except to the original drag target.
254
267
  if (e.target !== this.dragTarget.element) {
255
268
  this.cancelEvent(e);
@@ -265,6 +278,9 @@ class DragSession {
265
278
  this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
266
279
 
267
280
  if (!dropTarget) {
281
+ // if (e.target === activateButton) {
282
+ // activateButton.focus();
283
+ // }
268
284
  if (this.currentDropTarget) {
269
285
  this.currentDropTarget.element.focus();
270
286
  } else {
@@ -274,10 +290,18 @@ class DragSession {
274
290
  }
275
291
 
276
292
  let item = dropItems.get(e.target as HTMLElement);
277
- this.setCurrentDropTarget(dropTarget, item);
293
+ if (dropTarget) {
294
+ this.setCurrentDropTarget(dropTarget, item);
295
+ }
278
296
  }
279
297
 
280
298
  onBlur(e: FocusEvent) {
299
+ let activateButton = this.getCurrentActivateButton();
300
+ if (e.relatedTarget === activateButton) {
301
+ this.cancelEvent(e);
302
+ return;
303
+ }
304
+
281
305
  if (e.target !== this.dragTarget.element) {
282
306
  this.cancelEvent(e);
283
307
  }
@@ -296,14 +320,21 @@ class DragSession {
296
320
  onClick(e: MouseEvent) {
297
321
  this.cancelEvent(e);
298
322
  if (isVirtualClick(e) || this.isVirtualClick) {
323
+ let dropElements = dropItems.values();
324
+ let item = [...dropElements].find(item => item.element === e.target as HTMLElement || item.activateButtonRef?.current?.contains(e.target as HTMLElement));
325
+ let dropTarget = this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
326
+ let activateButton = item?.activateButtonRef?.current ?? dropTarget?.activateButtonRef?.current;
327
+ if (activateButton?.contains(e.target as HTMLElement) && dropTarget) {
328
+ this.activate(dropTarget, item);
329
+ return;
330
+ }
331
+
299
332
  if (e.target === this.dragTarget.element) {
300
333
  this.cancel();
301
334
  return;
302
335
  }
303
336
 
304
- let dropTarget = this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
305
337
  if (dropTarget) {
306
- let item = dropItems.get(e.target as HTMLElement);
307
338
  this.setCurrentDropTarget(dropTarget, item);
308
339
  this.drop(item);
309
340
  }
@@ -319,7 +350,7 @@ class DragSession {
319
350
 
320
351
  cancelEvent(e: Event) {
321
352
  // Allow focusin and focusout on the drag target so focus ring works properly.
322
- if ((e.type === 'focusin' || e.type === 'focusout') && e.target === this.dragTarget?.element) {
353
+ if ((e.type === 'focusin' || e.type === 'focusout') && (e.target === this.dragTarget?.element || e.target === this.getCurrentActivateButton())) {
323
354
  return;
324
355
  }
325
356
 
@@ -375,14 +406,23 @@ class DragSession {
375
406
 
376
407
  this.restoreAriaHidden = ariaHideOutside([
377
408
  this.dragTarget.element,
378
- ...validDropItems.map(item => item.element),
379
- ...visibleDropTargets.map(target => target.element)
409
+ ...validDropItems.flatMap(item => item.activateButtonRef?.current ? [item.element, item.activateButtonRef?.current] : [item.element]),
410
+ ...visibleDropTargets.flatMap(target => target.activateButtonRef?.current ? [target.element, target.activateButtonRef?.current] : [target.element])
380
411
  ]);
381
412
 
382
413
  this.mutationObserver.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['aria-hidden']});
383
414
  }
384
415
 
385
416
  next() {
417
+ // TODO: Allow tabbing to the activate button. Revisit once we fix the focus ring.
418
+ // For now, the activate button is reachable by screen readers and ArrowLeft/ArrowRight
419
+ // is usable specifically by Tree. Will need tabbing for other components.
420
+ // let activateButton = this.getCurrentActivateButton();
421
+ // if (activateButton && document.activeElement !== activateButton) {
422
+ // activateButton.focus();
423
+ // return;
424
+ // }
425
+
386
426
  if (!this.currentDropTarget) {
387
427
  this.setCurrentDropTarget(this.validDropTargets[0]);
388
428
  return;
@@ -409,6 +449,15 @@ class DragSession {
409
449
  }
410
450
 
411
451
  previous() {
452
+ // let activateButton = this.getCurrentActivateButton();
453
+ // if (activateButton && document.activeElement === activateButton) {
454
+ // let target = this.currentDropItem ?? this.currentDropTarget;
455
+ // if (target) {
456
+ // target.element.focus();
457
+ // return;
458
+ // }
459
+ // }
460
+
412
461
  if (!this.currentDropTarget) {
413
462
  this.setCurrentDropTarget(this.validDropTargets[this.validDropTargets.length - 1]);
414
463
  return;
@@ -487,7 +536,6 @@ class DragSession {
487
536
  if (this.currentDropTarget && typeof this.currentDropTarget.onDropTargetEnter === 'function') {
488
537
  this.currentDropTarget.onDropTargetEnter(item.target);
489
538
  }
490
-
491
539
  item.element.focus();
492
540
  this.currentDropItem = item;
493
541
 
@@ -576,14 +624,15 @@ class DragSession {
576
624
  announce(this.stringFormatter.format('dropComplete'));
577
625
  }
578
626
 
579
- activate() {
580
- if (this.currentDropTarget && typeof this.currentDropTarget.onDropActivate === 'function') {
581
- let rect = this.currentDropTarget.element.getBoundingClientRect();
582
- this.currentDropTarget.onDropActivate({
627
+ activate(dropTarget: DropTarget | null, dropItem: DroppableItem | null | undefined) {
628
+ if (dropTarget && typeof dropTarget.onDropActivate === 'function') {
629
+ let target = dropItem?.target ?? null;
630
+ let rect = dropTarget.element.getBoundingClientRect();
631
+ dropTarget.onDropActivate({
583
632
  type: 'dropactivate',
584
633
  x: rect.left + (rect.width / 2),
585
634
  y: rect.top + (rect.height / 2)
586
- }, null);
635
+ }, target);
587
636
  }
588
637
  }
589
638
  }
@@ -0,0 +1,273 @@
1
+ import {Collection, DropTarget, Key, KeyboardDelegate, Node} from '@react-types/shared';
2
+ import {getChildNodes} from '@react-stately/collections';
3
+
4
+ export function navigate(
5
+ keyboardDelegate: KeyboardDelegate,
6
+ collection: Collection<Node<unknown>>,
7
+ target: DropTarget | null | undefined,
8
+ direction: 'left' | 'right' | 'up' | 'down',
9
+ rtl = false,
10
+ wrap = false
11
+ ): DropTarget | null {
12
+ switch (direction) {
13
+ case 'left':
14
+ return rtl
15
+ ? nextDropTarget(keyboardDelegate, collection, target, wrap, 'left')
16
+ : previousDropTarget(keyboardDelegate, collection, target, wrap, 'left');
17
+ case 'right':
18
+ return rtl
19
+ ? previousDropTarget(keyboardDelegate, collection, target, wrap, 'right')
20
+ : nextDropTarget(keyboardDelegate, collection, target, wrap, 'right');
21
+ case 'up':
22
+ return previousDropTarget(keyboardDelegate, collection, target, wrap);
23
+ case 'down':
24
+ return nextDropTarget(keyboardDelegate, collection, target, wrap);
25
+ }
26
+ }
27
+
28
+ function nextDropTarget(
29
+ keyboardDelegate: KeyboardDelegate,
30
+ collection: Collection<Node<unknown>>,
31
+ target: DropTarget | null | undefined,
32
+ wrap = false,
33
+ horizontal: 'left' | 'right' | null = null
34
+ ): DropTarget | null {
35
+ if (!target) {
36
+ return {
37
+ type: 'root'
38
+ };
39
+ }
40
+
41
+ if (target.type === 'root') {
42
+ let nextKey = keyboardDelegate.getFirstKey?.() ?? null;
43
+ if (nextKey != null) {
44
+ return {
45
+ type: 'item',
46
+ key: nextKey,
47
+ dropPosition: 'before'
48
+ };
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ if (target.type === 'item') {
55
+ let nextKey: Key | null | undefined = null;
56
+ if (horizontal) {
57
+ nextKey = horizontal === 'right' ? keyboardDelegate.getKeyRightOf?.(target.key) : keyboardDelegate.getKeyLeftOf?.(target.key);
58
+ } else {
59
+ nextKey = keyboardDelegate.getKeyBelow?.(target.key);
60
+ }
61
+ let nextCollectionKey = collection.getKeyAfter(target.key);
62
+
63
+ // If the keyboard delegate did not move to the next key in the collection,
64
+ // jump to that key with the same drop position. Otherwise, try the other
65
+ // drop positions on the current key first.
66
+ if (nextKey != null && nextKey !== nextCollectionKey) {
67
+ return {
68
+ type: 'item',
69
+ key: nextKey,
70
+ dropPosition: target.dropPosition
71
+ };
72
+ }
73
+
74
+ switch (target.dropPosition) {
75
+ case 'before': {
76
+ return {
77
+ type: 'item',
78
+ key: target.key,
79
+ dropPosition: 'on'
80
+ };
81
+ }
82
+ case 'on': {
83
+ // If there are nested items, traverse to them prior to the "after" position of this target.
84
+ // If the next key is on the same level, then its "before" position is equivalent to this item's "after" position.
85
+ let targetNode = collection.getItem(target.key);
86
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
87
+ if (targetNode && nextNode && nextNode.level >= targetNode.level) {
88
+ return {
89
+ type: 'item',
90
+ key: nextNode.key,
91
+ dropPosition: 'before'
92
+ };
93
+ }
94
+
95
+ return {
96
+ type: 'item',
97
+ key: target.key,
98
+ dropPosition: 'after'
99
+ };
100
+ }
101
+ case 'after': {
102
+ // If this is the last sibling in a level, traverse to the parent.
103
+ let targetNode = collection.getItem(target.key);
104
+ if (targetNode && targetNode.nextKey == null && targetNode.parentKey != null) {
105
+ // If the parent item has an item after it, use the "before" position.
106
+ let parentNode = collection.getItem(targetNode.parentKey);
107
+ if (parentNode?.nextKey != null) {
108
+ return {
109
+ type: 'item',
110
+ key: parentNode.nextKey,
111
+ dropPosition: 'before'
112
+ };
113
+ }
114
+
115
+ if (parentNode) {
116
+ return {
117
+ type: 'item',
118
+ key: parentNode.key,
119
+ dropPosition: 'after'
120
+ };
121
+ }
122
+ }
123
+
124
+ if (targetNode?.nextKey != null) {
125
+ return {
126
+ type: 'item',
127
+ key: targetNode.nextKey,
128
+ dropPosition: 'on'
129
+ };
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (wrap) {
136
+ return {
137
+ type: 'root'
138
+ };
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ function previousDropTarget(
145
+ keyboardDelegate: KeyboardDelegate,
146
+ collection: Collection<Node<unknown>>,
147
+ target: DropTarget | null | undefined,
148
+ wrap = false,
149
+ horizontal: 'left' | 'right' | null = null
150
+ ): DropTarget | null {
151
+ // Start after the last root-level item.
152
+ if (!target || (wrap && target.type === 'root')) {
153
+ // Keyboard delegate gets the deepest item but we want the shallowest.
154
+ let prevKey: Key | null = null;
155
+ let lastKey = keyboardDelegate.getLastKey?.();
156
+ while (lastKey != null) {
157
+ prevKey = lastKey;
158
+ let node = collection.getItem(lastKey);
159
+ lastKey = node?.parentKey;
160
+ }
161
+
162
+ if (prevKey != null) {
163
+ return {
164
+ type: 'item',
165
+ key: prevKey,
166
+ dropPosition: 'after'
167
+ };
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ if (target.type === 'item') {
174
+ let prevKey: Key | null | undefined = null;
175
+ if (horizontal) {
176
+ prevKey = horizontal === 'left' ? keyboardDelegate.getKeyLeftOf?.(target.key) : keyboardDelegate.getKeyRightOf?.(target.key);
177
+ } else {
178
+ prevKey = keyboardDelegate.getKeyAbove?.(target.key);
179
+ }
180
+ let prevCollectionKey = collection.getKeyBefore(target.key);
181
+
182
+ // If the keyboard delegate did not move to the next key in the collection,
183
+ // jump to that key with the same drop position. Otherwise, try the other
184
+ // drop positions on the current key first.
185
+ if (prevKey != null && prevKey !== prevCollectionKey) {
186
+ return {
187
+ type: 'item',
188
+ key: prevKey,
189
+ dropPosition: target.dropPosition
190
+ };
191
+ }
192
+
193
+ switch (target.dropPosition) {
194
+ case 'before': {
195
+ // Move after the last child of the previous item.
196
+ let targetNode = collection.getItem(target.key);
197
+ if (targetNode && targetNode.prevKey != null) {
198
+ let lastChild = getLastChild(collection, targetNode.prevKey);
199
+ if (lastChild) {
200
+ return lastChild;
201
+ }
202
+ }
203
+
204
+ if (prevKey != null) {
205
+ return {
206
+ type: 'item',
207
+ key: prevKey,
208
+ dropPosition: 'on'
209
+ };
210
+ }
211
+
212
+ return {
213
+ type: 'root'
214
+ };
215
+ }
216
+ case 'on': {
217
+ return {
218
+ type: 'item',
219
+ key: target.key,
220
+ dropPosition: 'before'
221
+ };
222
+ }
223
+ case 'after': {
224
+ // Move after the last child of this item.
225
+ let lastChild = getLastChild(collection, target.key);
226
+ if (lastChild) {
227
+ return lastChild;
228
+ }
229
+
230
+ return {
231
+ type: 'item',
232
+ key: target.key,
233
+ dropPosition: 'on'
234
+ };
235
+ }
236
+ }
237
+ }
238
+
239
+ if (target.type !== 'root') {
240
+ return {
241
+ type: 'root'
242
+ };
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ function getLastChild(collection: Collection<Node<unknown>>, key: Key): DropTarget | null {
249
+ // getChildNodes still returns child tree items even when the item is collapsed.
250
+ // Checking if the next item has a greater level is a silly way to determine if the item is expanded.
251
+ let targetNode = collection.getItem(key);
252
+ let nextKey = collection.getKeyAfter(key);
253
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
254
+ if (targetNode && nextNode && nextNode.level > targetNode.level) {
255
+ let children = getChildNodes(targetNode, collection);
256
+ let lastChild: Node<unknown> | null = null;
257
+ for (let child of children) {
258
+ if (child.type === 'item') {
259
+ lastChild = child;
260
+ }
261
+ }
262
+
263
+ if (lastChild) {
264
+ return {
265
+ type: 'item',
266
+ key: lastChild.key,
267
+ dropPosition: 'after'
268
+ };
269
+ }
270
+ }
271
+
272
+ return null;
273
+ }
@@ -33,7 +33,7 @@ export class ListDropTargetDelegate implements DropTargetDelegate {
33
33
  private ref: RefObject<HTMLElement | null>;
34
34
  private layout: 'stack' | 'grid';
35
35
  private orientation: Orientation;
36
- private direction: Direction;
36
+ protected direction: Direction;
37
37
 
38
38
  constructor(collection: Iterable<Node<unknown>>, ref: RefObject<HTMLElement | null>, options?: ListDropTargetDelegateOptions) {
39
39
  this.collection = collection;
package/src/useDrop.ts CHANGED
@@ -36,7 +36,6 @@ export interface DropOptions {
36
36
  /**
37
37
  * Handler that is called after a valid drag is held over the drop target for a period of time.
38
38
  * This typically opens the item so that the user can drop within it.
39
- * @private
40
39
  */
41
40
  onDropActivate?: (e: DropActivateEvent) => void,
42
41
  /** Handler that is called when a valid drag exits the drop target. */
@@ -12,7 +12,7 @@
12
12
 
13
13
  import * as DragManager from './DragManager';
14
14
  import {DroppableCollectionState} from '@react-stately/dnd';
15
- import {DropTarget, Key, RefObject} from '@react-types/shared';
15
+ import {DropTarget, FocusableElement, Key, RefObject} from '@react-types/shared';
16
16
  import {getDroppableCollectionId} from './utils';
17
17
  import {HTMLAttributes} from 'react';
18
18
  // @ts-ignore
@@ -23,7 +23,9 @@ import {useLocalizedStringFormatter} from '@react-aria/i18n';
23
23
 
24
24
  export interface DropIndicatorProps {
25
25
  /** The drop target that the drop indicator represents. */
26
- target: DropTarget
26
+ target: DropTarget,
27
+ /** The ref to the activate button. */
28
+ activateButtonRef?: RefObject<FocusableElement | null>
27
29
  }
28
30
 
29
31
  export interface DropIndicatorAria {
@@ -69,28 +71,32 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
69
71
  } else {
70
72
  let before: Key | null | undefined;
71
73
  let after: Key | null | undefined;
72
- if (collection.getFirstKey() === target.key && target.dropPosition === 'before') {
73
- before = null;
74
+ if (target.dropPosition === 'before') {
75
+ let prevKey = collection.getItem(target.key)?.prevKey;
76
+ let prevNode = prevKey != null ? collection.getItem(prevKey) : null;
77
+ before = prevNode?.type === 'item' ? prevNode.key : null;
74
78
  } else {
75
- before = target.dropPosition === 'before' ? collection.getKeyBefore(target.key) : target.key;
79
+ before = target.key;
76
80
  }
77
81
 
78
- if (collection.getLastKey() === target.key && target.dropPosition === 'after') {
79
- after = null;
82
+ if (target.dropPosition === 'after') {
83
+ let nextKey = collection.getItem(target.key)?.nextKey;
84
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
85
+ after = nextNode?.type === 'item' ? nextNode.key : null;
80
86
  } else {
81
- after = target.dropPosition === 'after' ? collection.getKeyAfter(target.key) : target.key;
87
+ after = target.key;
82
88
  }
83
89
 
84
- if (before && after) {
90
+ if (before != null && after != null) {
85
91
  label = stringFormatter.format('insertBetween', {
86
92
  beforeItemText: getText(before),
87
93
  afterItemText: getText(after)
88
94
  });
89
- } else if (before) {
95
+ } else if (before != null) {
90
96
  label = stringFormatter.format('insertAfter', {
91
97
  itemText: getText(before)
92
98
  });
93
- } else if (after) {
99
+ } else if (after != null) {
94
100
  label = stringFormatter.format('insertBefore', {
95
101
  itemText: getText(after)
96
102
  });