@jupyterlab/filebrowser 4.0.0-beta.1 → 4.0.0-rc.0

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/lib/listing.js CHANGED
@@ -180,6 +180,8 @@ export class DirListing extends Widget {
180
180
  this._inRename = false;
181
181
  this._isDirty = false;
182
182
  this._hiddenColumns = new Set();
183
+ // _focusIndex should never be set outside the range [0, this._items.length - 1]
184
+ this._focusIndex = 0;
183
185
  this.addClass(DIR_LISTING_CLASS);
184
186
  this.translator = options.translator || nullTranslator;
185
187
  this._trans = this.translator.load('jupyterlab');
@@ -365,6 +367,15 @@ export class DirListing extends Widget {
365
367
  if (!this.isDisposed && result.button.accept) {
366
368
  await this._delete(items.map(item => item.path));
367
369
  }
370
+ // Re-focus
371
+ let focusIndex = this._focusIndex;
372
+ const lastIndexAfterDelete = this._sortedItems.length - items.length - 1;
373
+ if (focusIndex > lastIndexAfterDelete) {
374
+ // If the focus index after deleting items is out of bounds, set it to the
375
+ // last item.
376
+ focusIndex = Math.max(0, lastIndexAfterDelete);
377
+ }
378
+ this._focusItem(focusIndex);
368
379
  }
369
380
  /**
370
381
  * Duplicate the currently selected item(s).
@@ -532,7 +543,7 @@ export class DirListing extends Widget {
532
543
  * Select an item by name.
533
544
  *
534
545
  * @param name - The name of the item to select.
535
- * @param focus - Whether to move focus the selected item.
546
+ * @param focus - Whether to move focus to the selected item.
536
547
  *
537
548
  * @returns A promise that resolves when the name is selected.
538
549
  */
@@ -693,16 +704,23 @@ export class DirListing extends Widget {
693
704
  nodes.push(node);
694
705
  content.appendChild(node);
695
706
  }
696
- // Remove extra classes from the nodes.
697
- nodes.forEach(node => {
707
+ nodes.forEach((node, i) => {
708
+ // Remove extra classes from the nodes.
698
709
  node.classList.remove(SELECTED_CLASS);
699
710
  node.classList.remove(RUNNING_CLASS);
700
711
  node.classList.remove(CUT_CLASS);
712
+ // Uncheck each file checkbox
701
713
  const checkbox = renderer.getCheckboxNode(node);
702
714
  if (checkbox) {
703
- // Uncheck each file checkbox
704
715
  checkbox.checked = false;
705
716
  }
717
+ // Handle `tabIndex`
718
+ const nameNode = renderer.getNameNode(node);
719
+ if (nameNode) {
720
+ // Must check if the name node is there because it gets replaced by the
721
+ // edit node when editing the name of the file or directory.
722
+ nameNode.tabIndex = i === this._focusIndex ? 0 : -1;
723
+ }
706
724
  });
707
725
  // Put the check-all checkbox in the header into the correct state
708
726
  const checkAllCheckbox = renderer.getCheckboxNode(this.headerNode);
@@ -723,7 +741,7 @@ export class DirListing extends Widget {
723
741
  ? trans.__('Deselect all files and directories')
724
742
  : trans.__('Select all files and directories'));
725
743
  }
726
- // Add extra classes to item nodes based on widget state.
744
+ // Update item nodes based on widget state.
727
745
  items.forEach((item, i) => {
728
746
  const node = nodes[i];
729
747
  const ft = this._manager.registry.getFileTypeForModel(item);
@@ -824,6 +842,13 @@ export class DirListing extends Widget {
824
842
  }
825
843
  return;
826
844
  }
845
+ else {
846
+ // Focus the selected file on click to ensure a couple of things:
847
+ // 1. If a user clicks on the item node, its name node will receive focus.
848
+ // 2. If a user clicks on blank space in the directory listing, the
849
+ // previously focussed item will be focussed.
850
+ this._focusItem(this._focusIndex);
851
+ }
827
852
  }
828
853
  /**
829
854
  * Handle the `'scroll'` event for the widget.
@@ -889,11 +914,11 @@ export class DirListing extends Widget {
889
914
  }
890
915
  this._softSelection = '';
891
916
  }
892
- // Re-focus the selected file. This is needed because nodes corresponding
893
- // to files selected in mousedown handler will not retain the focus
894
- // as mousedown event is always followed by a blur/focus event.
917
+ // Re-focus. This is needed because nodes corresponding to files selected in
918
+ // mousedown handler will not retain the focus as mousedown event is always
919
+ // followed by a blur/focus event.
895
920
  if (event.button === 0) {
896
- this._focusSelectedFile();
921
+ this._focusItem(this._focusIndex);
897
922
  }
898
923
  // Remove the drag listeners if necessary.
899
924
  if (event.button !== 0 || !this._drag) {
@@ -939,10 +964,102 @@ export class DirListing extends Widget {
939
964
  this._manager.openOrReveal(path);
940
965
  }
941
966
  }
967
+ /**
968
+ * Calculate the next focus index, given the current focus index and a
969
+ * direction, keeping within the bounds of the directory listing.
970
+ *
971
+ * @param index Current focus index
972
+ * @param direction -1 (up) or 1 (down)
973
+ * @returns The next focus index, which could be the same as the current focus
974
+ * index if at the boundary.
975
+ */
976
+ _getNextFocusIndex(index, direction) {
977
+ const nextIndex = index + direction;
978
+ if (nextIndex === -1 || nextIndex === this._items.length) {
979
+ // keep focus index within bounds
980
+ return index;
981
+ }
982
+ else {
983
+ return nextIndex;
984
+ }
985
+ }
986
+ /**
987
+ * Handle the up or down arrow key.
988
+ *
989
+ * @param event The keyboard event
990
+ * @param direction -1 (up) or 1 (down)
991
+ */
992
+ _handleArrowY(event, direction) {
993
+ // We only handle the `ctrl` and `shift` modifiers. If other modifiers are
994
+ // present, then do nothing.
995
+ if (event.altKey || event.metaKey) {
996
+ return;
997
+ }
998
+ // If folder is empty, there's nothing to do with the up/down key.
999
+ if (!this._items.length) {
1000
+ return;
1001
+ }
1002
+ // Don't handle the arrow key press if it's not on directory item. This
1003
+ // avoids a confusing user experience that can result from when the user
1004
+ // moves the selection and focus index apart (via ctrl + up/down). The last
1005
+ // selected item remains highlighted but the last focussed item loses its
1006
+ // focus ring if it's not actively focussed. This forces the user to
1007
+ // visibly reveal the last focussed item before moving the focus.
1008
+ if (!event.target.classList.contains(ITEM_TEXT_CLASS)) {
1009
+ return;
1010
+ }
1011
+ event.stopPropagation();
1012
+ event.preventDefault();
1013
+ const focusIndex = this._focusIndex;
1014
+ let nextFocusIndex = this._getNextFocusIndex(focusIndex, direction);
1015
+ // The following if-block allows the first press of the down arrow to select
1016
+ // the first (rather than the second) file/directory in the list. This is
1017
+ // the situation when the page first loads or when a user changes directory.
1018
+ if (direction > 0 &&
1019
+ focusIndex === 0 &&
1020
+ !event.ctrlKey &&
1021
+ Object.keys(this.selection).length === 0) {
1022
+ nextFocusIndex = 0;
1023
+ }
1024
+ // Shift key indicates multi-selection. Either the user is trying to grow
1025
+ // the selection, or shrink it.
1026
+ if (event.shiftKey) {
1027
+ this._handleMultiSelect(nextFocusIndex);
1028
+ }
1029
+ else if (!event.ctrlKey) {
1030
+ // If neither the shift nor ctrl keys were used with the up/down arrow,
1031
+ // then we treat it as a normal, unmodified key press and select the
1032
+ // next item.
1033
+ this._selectItem(nextFocusIndex, event.shiftKey, false /* focus = false because we call focus method directly following this */);
1034
+ }
1035
+ this._focusItem(nextFocusIndex);
1036
+ this.update();
1037
+ }
1038
+ /**
1039
+ * cd ..
1040
+ *
1041
+ * Go up one level in the directory tree.
1042
+ */
1043
+ async goUp() {
1044
+ const model = this.model;
1045
+ if (model.path === model.rootPath) {
1046
+ return;
1047
+ }
1048
+ try {
1049
+ await model.cd('..');
1050
+ }
1051
+ catch (reason) {
1052
+ console.warn(`Failed to go to parent directory of ${model.path}`, reason);
1053
+ }
1054
+ }
942
1055
  /**
943
1056
  * Handle the `'keydown'` event for the widget.
944
1057
  */
945
1058
  evtKeydown(event) {
1059
+ // Do not handle any keydown events here if in the middle of a file rename.
1060
+ if (this._inRename) {
1061
+ return;
1062
+ }
946
1063
  switch (event.keyCode) {
947
1064
  case 13: {
948
1065
  // Enter
@@ -952,34 +1069,64 @@ export class DirListing extends Widget {
952
1069
  }
953
1070
  event.preventDefault();
954
1071
  event.stopPropagation();
955
- const selected = Object.keys(this.selection);
956
- const path = selected[0];
957
- const items = this._sortedItems;
958
- const i = ArrayExt.findFirstIndex(items, value => value.path === path);
959
- if (i === -1) {
1072
+ for (const item of this.selectedItems()) {
1073
+ this.handleOpen(item);
1074
+ }
1075
+ return;
1076
+ }
1077
+ case 38:
1078
+ // Up arrow
1079
+ this._handleArrowY(event, -1);
1080
+ return;
1081
+ case 40:
1082
+ // Down arrow
1083
+ this._handleArrowY(event, 1);
1084
+ return;
1085
+ case 32: {
1086
+ // Space
1087
+ if (event.ctrlKey) {
1088
+ // Follow the Windows and Ubuntu convention: you must press `ctrl` +
1089
+ // `space` in order to toggle whether an item is selected.
1090
+ // However, do not handle if any other modifiers were pressed.
1091
+ if (event.metaKey || event.shiftKey || event.altKey) {
1092
+ return;
1093
+ }
1094
+ // Make sure the ctrl+space key stroke was on a valid, focussed target.
1095
+ const node = this._items[this._focusIndex];
1096
+ if (!(
1097
+ // Event must have occurred within a node whose item can be toggled.
1098
+ (node.contains(event.target) &&
1099
+ // That node must also contain the currently focussed element.
1100
+ node.contains(document.activeElement)))) {
1101
+ return;
1102
+ }
1103
+ event.stopPropagation();
1104
+ // Prevent default, otherwise the container will scroll.
1105
+ event.preventDefault();
1106
+ // Toggle item selected
1107
+ const { path } = this._sortedItems[this._focusIndex];
1108
+ if (this.selection[path]) {
1109
+ delete this.selection[path];
1110
+ }
1111
+ else {
1112
+ this.selection[path] = true;
1113
+ }
1114
+ this.update();
1115
+ // Key was handled, so return.
960
1116
  return;
961
1117
  }
962
- const item = this._sortedItems[i];
963
- this.handleOpen(item);
964
1118
  break;
965
1119
  }
966
- case 38: // Up arrow
967
- this.selectPrevious(event.shiftKey);
968
- event.stopPropagation();
969
- event.preventDefault();
970
- break;
971
- case 40: // Down arrow
972
- this.selectNext(event.shiftKey);
973
- event.stopPropagation();
974
- event.preventDefault();
975
- break;
976
- default:
977
- break;
978
1120
  }
979
1121
  // Detects printable characters typed by the user.
980
1122
  // Not all browsers support .key, but it discharges us from reconstructing
981
1123
  // characters from key codes.
982
- if (!this._inRename && event.key !== undefined && event.key.length === 1) {
1124
+ if (event.key !== undefined &&
1125
+ event.key.length === 1 &&
1126
+ // Don't gobble up the space key on the check-all checkbox (which the
1127
+ // browser treats as a click event).
1128
+ !((event.key === ' ' || event.keyCode === 32) &&
1129
+ event.target.type === 'checkbox')) {
983
1130
  if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
984
1131
  return;
985
1132
  }
@@ -1270,10 +1417,12 @@ export class DirListing extends Widget {
1270
1417
  else {
1271
1418
  this.selection[path] = true;
1272
1419
  }
1420
+ this._focusItem(index);
1273
1421
  // Handle multiple select.
1274
1422
  }
1275
1423
  else if (event.shiftKey) {
1276
- this._handleMultiSelect(selected, index);
1424
+ this._handleMultiSelect(index);
1425
+ this._focusItem(index);
1277
1426
  // Handle a 'soft' selection
1278
1427
  }
1279
1428
  else if (path in this.selection && selected.length > 1) {
@@ -1282,68 +1431,121 @@ export class DirListing extends Widget {
1282
1431
  }
1283
1432
  else {
1284
1433
  // Select only the given item.
1285
- return this._selectItem(index, false);
1434
+ return this._selectItem(index, false, true);
1286
1435
  }
1287
1436
  this.update();
1288
1437
  }
1289
1438
  /**
1290
- * (Re-)focus on the selected file.
1439
+ * (Re-)focus an item in the directory listing.
1291
1440
  *
1292
- * If index is not given, it will be inferred from the current selection;
1293
- * providing index saves on the iteration time.
1294
- */
1295
- _focusSelectedFile(index) {
1296
- if (typeof index === 'undefined') {
1297
- const selected = Object.keys(this.selection);
1298
- if (selected.length > 1) {
1299
- // Multiselect - do not focus on any single file
1300
- return;
1301
- }
1302
- index = ArrayExt.findFirstIndex(this._sortedItems, value => value.path === selected[0]);
1303
- }
1304
- if (index === -1) {
1441
+ * @param index The index of the item node to focus
1442
+ */
1443
+ _focusItem(index) {
1444
+ const items = this._items;
1445
+ if (items.length === 0) {
1446
+ // Focus the top node if the folder is empty and therefore there are no
1447
+ // items inside the folder to focus.
1448
+ this._focusIndex = 0;
1449
+ this.node.focus();
1305
1450
  return;
1306
1451
  }
1307
- // Focus on text to make shortcuts works
1308
- const node = this._items[index];
1309
- const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
1310
- if (text) {
1311
- text.focus();
1452
+ this._focusIndex = index;
1453
+ const node = items[index];
1454
+ const nameNode = this.renderer.getNameNode(node);
1455
+ if (nameNode) {
1456
+ // Make the filename text node focusable so that it receives keyboard
1457
+ // events; text node was specifically chosen to receive shortcuts because
1458
+ // it gets substituted with input element during file name edits which
1459
+ // conveniently deactivates irrelevant shortcuts.
1460
+ nameNode.tabIndex = 0;
1461
+ nameNode.focus();
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Are all of the items between two provided indices selected?
1466
+ *
1467
+ * The items at the indices are not considered.
1468
+ *
1469
+ * @param j Index of one item.
1470
+ * @param k Index of another item. Note: may be less or greater than first
1471
+ * index.
1472
+ * @returns True if and only if all items between the j and k are selected.
1473
+ * Returns undefined if j and k are the same.
1474
+ */
1475
+ _allSelectedBetween(j, k) {
1476
+ if (j === k) {
1477
+ return;
1312
1478
  }
1479
+ const [start, end] = j < k ? [j + 1, k] : [k + 1, j];
1480
+ return this._sortedItems
1481
+ .slice(start, end)
1482
+ .reduce((result, item) => result && this.selection[item.path], true);
1313
1483
  }
1314
1484
  /**
1315
1485
  * Handle a multiple select on a file item node.
1316
1486
  */
1317
- _handleMultiSelect(selected, index) {
1318
- // Find the "nearest selected".
1487
+ _handleMultiSelect(index) {
1319
1488
  const items = this._sortedItems;
1320
- let nearestIndex = -1;
1321
- for (let i = 0; i < this._items.length; i++) {
1322
- if (i === index) {
1323
- continue;
1324
- }
1325
- const path = items[i].path;
1326
- if (selected.indexOf(path) !== -1) {
1327
- if (nearestIndex === -1) {
1328
- nearestIndex = i;
1329
- }
1330
- else {
1331
- if (Math.abs(index - i) < Math.abs(nearestIndex - i)) {
1332
- nearestIndex = i;
1333
- }
1489
+ const fromIndex = this._focusIndex;
1490
+ const target = items[index];
1491
+ let shouldAdd = true;
1492
+ if (index === fromIndex) {
1493
+ // This follows the convention in Ubuntu and Windows, which is to allow
1494
+ // the focussed item to gain but not lose selected status on shift-click.
1495
+ // (MacOS is irrelevant here because MacOS Finder has no notion of a
1496
+ // focused-but-not-selected state.)
1497
+ this.selection[target.path] = true;
1498
+ return;
1499
+ }
1500
+ // If the target and all items in-between are selected, then we assume that
1501
+ // the user is trying to shrink rather than grow the group of selected
1502
+ // items.
1503
+ if (this.selection[target.path]) {
1504
+ // However, there is a special case when the distance between the from-
1505
+ // and to- index is just one (for example, when the user is pressing the
1506
+ // shift key plus arrow-up/down). If and only if the situation looks like
1507
+ // the following when going down (or reverse when going up) ...
1508
+ //
1509
+ // - [ante-anchor / previous item] unselected (or boundary)
1510
+ // - [anchor / currently focussed item / item at from-index] selected
1511
+ // - [target / next item / item at to-index] selected
1512
+ //
1513
+ // ... then we shrink the selection / unselect the currently focussed
1514
+ // item.
1515
+ if (Math.abs(index - fromIndex) === 1) {
1516
+ const anchor = items[fromIndex];
1517
+ const anteAnchor = items[fromIndex + (index < fromIndex ? 1 : -1)];
1518
+ if (
1519
+ // Currently focussed item is selected
1520
+ this.selection[anchor.path] &&
1521
+ // Item on other side of focussed item (away from target) is either a
1522
+ // boundary or unselected
1523
+ (!anteAnchor || !this.selection[anteAnchor.path])) {
1524
+ delete this.selection[anchor.path];
1334
1525
  }
1335
1526
  }
1527
+ else if (this._allSelectedBetween(fromIndex, index)) {
1528
+ shouldAdd = false;
1529
+ }
1336
1530
  }
1337
- // Default to the first element (and fill down).
1338
- if (nearestIndex === -1) {
1339
- nearestIndex = 0;
1340
- }
1341
- // Select the rows between the current and the nearest selected.
1342
- for (let i = 0; i < this._items.length; i++) {
1343
- if ((nearestIndex >= i && index <= i) ||
1344
- (nearestIndex <= i && index >= i)) {
1531
+ // Select (or unselect) the rows between chosen index (target) and the last
1532
+ // focussed.
1533
+ const step = fromIndex < index ? 1 : -1;
1534
+ for (let i = fromIndex; i !== index + step; i += step) {
1535
+ if (shouldAdd) {
1536
+ if (i === fromIndex) {
1537
+ // Do not change the selection state of the starting (fromIndex) item.
1538
+ continue;
1539
+ }
1345
1540
  this.selection[items[i].path] = true;
1346
1541
  }
1542
+ else {
1543
+ if (i === index) {
1544
+ // Do not unselect the target item.
1545
+ continue;
1546
+ }
1547
+ delete this.selection[items[i].path];
1548
+ }
1347
1549
  }
1348
1550
  }
1349
1551
  /**
@@ -1366,57 +1568,85 @@ export class DirListing extends Widget {
1366
1568
  /**
1367
1569
  * Allow the user to rename item on a given row.
1368
1570
  */
1369
- _doRename() {
1571
+ async _doRename() {
1370
1572
  this._inRename = true;
1573
+ const selectedPaths = Object.keys(this.selection);
1574
+ // Bail out if nothing has been selected.
1575
+ if (selectedPaths.length === 0) {
1576
+ this._inRename = false;
1577
+ return Promise.resolve('');
1578
+ }
1579
+ // Figure out which selected path to use for the rename.
1371
1580
  const items = this._sortedItems;
1372
- const path = Object.keys(this.selection)[0];
1581
+ let { path } = items[this._focusIndex];
1582
+ if (!this.selection[path]) {
1583
+ // If the currently focused item is not selected, then choose the last
1584
+ // selected item.
1585
+ path = selectedPaths.slice(-1)[0];
1586
+ }
1587
+ // Get the corresponding model, nodes, and file name.
1373
1588
  const index = ArrayExt.findFirstIndex(items, value => value.path === path);
1374
1589
  const row = this._items[index];
1375
1590
  const item = items[index];
1376
1591
  const nameNode = this.renderer.getNameNode(row);
1377
1592
  const original = item.name;
1593
+ // Seed the text input with current file name, and select and focus it.
1378
1594
  this._editNode.value = original;
1379
- this._selectItem(index, false);
1380
- return Private.doRename(nameNode, this._editNode, original).then(newName => {
1381
- this.node.focus();
1382
- if (!newName || newName === original) {
1383
- this._inRename = false;
1384
- return original;
1385
- }
1386
- if (!isValidFileName(newName)) {
1387
- void showErrorMessage(this._trans.__('Rename Error'), Error(this._trans._p('showErrorMessage', '"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"', newName)));
1388
- this._inRename = false;
1389
- return original;
1390
- }
1391
- if (this.isDisposed) {
1392
- this._inRename = false;
1393
- throw new Error('File browser is disposed.');
1394
- }
1595
+ this._selectItem(index, false, true);
1596
+ // Wait for user input
1597
+ const newName = await Private.userInputForRename(nameNode, this._editNode, original);
1598
+ // Check if the widget was disposed during the `await`.
1599
+ if (this.isDisposed) {
1600
+ this._inRename = false;
1601
+ throw new Error('File browser is disposed.');
1602
+ }
1603
+ let finalFilename = newName;
1604
+ if (!newName || newName === original) {
1605
+ finalFilename = original;
1606
+ }
1607
+ else if (!isValidFileName(newName)) {
1608
+ void showErrorMessage(this._trans.__('Rename Error'), Error(this._trans._p('showErrorMessage', '"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"', newName)));
1609
+ finalFilename = original;
1610
+ }
1611
+ else {
1612
+ // Attempt rename at the file system level.
1395
1613
  const manager = this._manager;
1396
1614
  const oldPath = PathExt.join(this._model.path, original);
1397
1615
  const newPath = PathExt.join(this._model.path, newName);
1398
- const promise = renameFile(manager, oldPath, newPath);
1399
- return promise
1400
- .catch(error => {
1616
+ try {
1617
+ await renameFile(manager, oldPath, newPath);
1618
+ }
1619
+ catch (error) {
1401
1620
  if (error !== 'File not renamed') {
1402
1621
  void showErrorMessage(this._trans._p('showErrorMessage', 'Rename Error'), error);
1403
1622
  }
1623
+ finalFilename = original;
1624
+ }
1625
+ // Check if the widget was disposed during the `await`.
1626
+ if (this.isDisposed) {
1404
1627
  this._inRename = false;
1405
- return original;
1406
- })
1407
- .then(() => {
1408
- if (this.isDisposed) {
1409
- this._inRename = false;
1410
- throw new Error('File browser is disposed.');
1411
- }
1412
- if (this._inRename) {
1413
- // No need to catch because `newName` will always exit.
1414
- void this.selectItemByName(newName);
1415
- }
1416
- this._inRename = false;
1417
- return newName;
1418
- });
1419
- });
1628
+ throw new Error('File browser is disposed.');
1629
+ }
1630
+ }
1631
+ // If nothing else has been selected, then select the renamed file. In
1632
+ // other words, don't select the renamed file if the user has clicked
1633
+ // away to some other file.
1634
+ if (!this.isDisposed &&
1635
+ Object.keys(this.selection).length === 1 &&
1636
+ // We haven't updated the instance yet to reflect the rename, so unless
1637
+ // the user or something else has updated the selection, the original file
1638
+ // path and not the new file path will be in `this.selection`.
1639
+ this.selection[item.path]) {
1640
+ try {
1641
+ await this.selectItemByName(finalFilename, true);
1642
+ }
1643
+ catch (_a) {
1644
+ // do nothing
1645
+ console.warn('After rename, failed to select file', finalFilename);
1646
+ }
1647
+ }
1648
+ this._inRename = false;
1649
+ return finalFilename;
1420
1650
  }
1421
1651
  /**
1422
1652
  * Select a given item.
@@ -1429,8 +1659,8 @@ export class DirListing extends Widget {
1429
1659
  }
1430
1660
  const path = items[index].path;
1431
1661
  this.selection[path] = true;
1432
- if (!keepExisting && focus) {
1433
- this._focusSelectedFile(index);
1662
+ if (focus) {
1663
+ this._focusItem(index);
1434
1664
  }
1435
1665
  this.update();
1436
1666
  }
@@ -1463,6 +1693,11 @@ export class DirListing extends Widget {
1463
1693
  this.clearSelectedItems();
1464
1694
  // Update the sorted items.
1465
1695
  this.sort(this.sortState);
1696
+ // Reset focus. But wait until the DOM has been updated (hence
1697
+ // `requestAnimationFrame`).
1698
+ requestAnimationFrame(() => {
1699
+ this._focusItem(0);
1700
+ });
1466
1701
  }
1467
1702
  /**
1468
1703
  * Handle a `fileChanged` signal from the model.
@@ -1513,7 +1748,8 @@ export class DirListing extends Widget {
1513
1748
  header.className = HEADER_CLASS;
1514
1749
  node.appendChild(header);
1515
1750
  node.appendChild(content);
1516
- node.tabIndex = 0;
1751
+ // Set to -1 to allow calling this.node.focus().
1752
+ node.tabIndex = -1;
1517
1753
  return node;
1518
1754
  }
1519
1755
  /**
@@ -1679,11 +1915,6 @@ export class DirListing extends Widget {
1679
1915
  node.appendChild(text);
1680
1916
  node.appendChild(modified);
1681
1917
  node.appendChild(fileSize);
1682
- // Make the text note focusable so that it receives keyboard events;
1683
- // text node was specifically chosen to receive shortcuts because
1684
- // text element gets substituted with input area during file name edits
1685
- // which conveniently deactivate irrelevant shortcuts.
1686
- text.tabIndex = 0;
1687
1918
  if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1688
1919
  modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1689
1920
  }
@@ -1716,11 +1947,6 @@ export class DirListing extends Widget {
1716
1947
  // Wrap the checkbox in a label element in order to increase its hit area.
1717
1948
  const labelWrapper = document.createElement('label');
1718
1949
  labelWrapper.classList.add(CHECKBOX_WRAPPER_CLASS);
1719
- // The individual file checkboxes are visible on hover, but the header
1720
- // check-all checkbox is always visible.
1721
- if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
1722
- labelWrapper.classList.add('jp-mod-visible');
1723
- }
1724
1950
  const checkbox = document.createElement('input');
1725
1951
  checkbox.type = 'checkbox';
1726
1952
  // Prevent the user from clicking (via mouse, keyboard, or touch) the
@@ -1729,6 +1955,15 @@ export class DirListing extends Widget {
1729
1955
  checkbox.addEventListener('click', event => {
1730
1956
  event.preventDefault();
1731
1957
  });
1958
+ // The individual file checkboxes are visible on hover, but the header
1959
+ // check-all checkbox is always visible.
1960
+ if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
1961
+ labelWrapper.classList.add('jp-mod-visible');
1962
+ }
1963
+ else {
1964
+ // Disable tabbing to all other checkboxes.
1965
+ checkbox.tabIndex = -1;
1966
+ }
1732
1967
  labelWrapper.appendChild(checkbox);
1733
1968
  return labelWrapper;
1734
1969
  }
@@ -1934,7 +2169,7 @@ var Private;
1934
2169
  *
1935
2170
  * @returns Boolean indicating whether the name changed.
1936
2171
  */
1937
- function doRename(text, edit, original) {
2172
+ function userInputForRename(text, edit, original) {
1938
2173
  const parent = text.parentElement;
1939
2174
  parent.replaceChild(edit, text);
1940
2175
  edit.focus();
@@ -1945,7 +2180,7 @@ var Private;
1945
2180
  else {
1946
2181
  edit.setSelectionRange(0, index);
1947
2182
  }
1948
- return new Promise((resolve, reject) => {
2183
+ return new Promise(resolve => {
1949
2184
  edit.onblur = () => {
1950
2185
  parent.replaceChild(text, edit);
1951
2186
  resolve(edit.value);
@@ -1962,20 +2197,10 @@ var Private;
1962
2197
  event.preventDefault();
1963
2198
  edit.value = original;
1964
2199
  edit.blur();
1965
- break;
1966
- case 38: // Up arrow
1967
- event.stopPropagation();
1968
- event.preventDefault();
1969
- if (edit.selectionStart !== edit.selectionEnd) {
1970
- edit.selectionStart = edit.selectionEnd = 0;
1971
- }
1972
- break;
1973
- case 40: // Down arrow
1974
- event.stopPropagation();
1975
- event.preventDefault();
1976
- if (edit.selectionStart !== edit.selectionEnd) {
1977
- edit.selectionStart = edit.selectionEnd = edit.value.length;
1978
- }
2200
+ // Put focus back on the text node. That way the user can, for
2201
+ // example, press the keyboard shortcut to go back into edit mode,
2202
+ // and it will work.
2203
+ text.focus();
1979
2204
  break;
1980
2205
  default:
1981
2206
  break;
@@ -1983,7 +2208,7 @@ var Private;
1983
2208
  };
1984
2209
  });
1985
2210
  }
1986
- Private.doRename = doRename;
2211
+ Private.userInputForRename = userInputForRename;
1987
2212
  /**
1988
2213
  * Sort a list of items by sort state as a new array.
1989
2214
  */