@headless-tree/core 1.5.1 → 1.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @headless-tree/core
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 297b575: The anchor for shift-selecting (`item.selectUpTo()`) is now the last item that was clicked while not holding shift, or alternatively the focused item if that didn't exist. The previous behavior was always using the focused item as anchor, which doesn't match common multi-select behaviors in similar applications (#176)
8
+
9
+ ### Patch Changes
10
+
11
+ - 287fe40: Added missing `aria-expanded` attribute to tree items (thanks to @gordey4doronin for the contribution) (#171)
12
+ - 7158afe: Improve rerendering behavior by skipping changes to the item loading state when items are already loading, and skipping changes to loading state in case of checkbox click propagation if items are loaded immediately (#173)
13
+
3
14
  ## 1.5.1
4
15
 
5
16
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -199,6 +199,9 @@ type MainFeatureDef<T = any> = {
199
199
  hotkeys: never;
200
200
  };
201
201
 
202
+ interface SelectionDataRef {
203
+ selectUpToAnchorId?: string | null;
204
+ }
202
205
  type SelectionFeatureDef<T> = {
203
206
  state: {
204
207
  selectedItems: string[];
@@ -276,7 +279,10 @@ type SyncDataLoaderFeatureDef<T> = {
276
279
  retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
277
280
  };
278
281
  itemInstance: {
282
+ /** Returns false. Provided for consistency with async data loader */
279
283
  isLoading: () => boolean;
284
+ /** Returns true. Provided for consistency with async data loader */
285
+ hasLoadedData: () => boolean;
280
286
  };
281
287
  hotkeys: never;
282
288
  };
@@ -284,6 +290,8 @@ type SyncDataLoaderFeatureDef<T> = {
284
290
  interface AsyncDataLoaderDataRef<T = any> {
285
291
  itemData: Record<string, T>;
286
292
  childrenIds: Record<string, string[]>;
293
+ loadingDataSubs: Record<string, (() => void)[]>;
294
+ loadingChildrenSubs: Record<string, (() => void)[]>;
287
295
  }
288
296
  /**
289
297
  * @category Async Data Loader/General
@@ -324,6 +332,7 @@ type AsyncDataLoaderFeatureDef<T> = {
324
332
  /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
325
333
  updateCachedData: (data: T | undefined) => void;
326
334
  updateCachedChildrenIds: (childrenIds: string[]) => void;
335
+ hasLoadedData: () => boolean;
327
336
  isLoading: () => boolean;
328
337
  };
329
338
  hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
@@ -601,4 +610,4 @@ declare const isOrderedDragTarget: <T>(dragTarget: DragTarget<T>) => dragTarget
601
610
  dragLineLevel: number;
602
611
  };
603
612
 
604
- export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
613
+ export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionDataRef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
package/dist/index.d.ts CHANGED
@@ -199,6 +199,9 @@ type MainFeatureDef<T = any> = {
199
199
  hotkeys: never;
200
200
  };
201
201
 
202
+ interface SelectionDataRef {
203
+ selectUpToAnchorId?: string | null;
204
+ }
202
205
  type SelectionFeatureDef<T> = {
203
206
  state: {
204
207
  selectedItems: string[];
@@ -276,7 +279,10 @@ type SyncDataLoaderFeatureDef<T> = {
276
279
  retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
277
280
  };
278
281
  itemInstance: {
282
+ /** Returns false. Provided for consistency with async data loader */
279
283
  isLoading: () => boolean;
284
+ /** Returns true. Provided for consistency with async data loader */
285
+ hasLoadedData: () => boolean;
280
286
  };
281
287
  hotkeys: never;
282
288
  };
@@ -284,6 +290,8 @@ type SyncDataLoaderFeatureDef<T> = {
284
290
  interface AsyncDataLoaderDataRef<T = any> {
285
291
  itemData: Record<string, T>;
286
292
  childrenIds: Record<string, string[]>;
293
+ loadingDataSubs: Record<string, (() => void)[]>;
294
+ loadingChildrenSubs: Record<string, (() => void)[]>;
287
295
  }
288
296
  /**
289
297
  * @category Async Data Loader/General
@@ -324,6 +332,7 @@ type AsyncDataLoaderFeatureDef<T> = {
324
332
  /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
325
333
  updateCachedData: (data: T | undefined) => void;
326
334
  updateCachedChildrenIds: (childrenIds: string[]) => void;
335
+ hasLoadedData: () => boolean;
327
336
  isLoading: () => boolean;
328
337
  };
329
338
  hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
@@ -601,4 +610,4 @@ declare const isOrderedDragTarget: <T>(dragTarget: DragTarget<T>) => dragTarget
601
610
  dragLineLevel: number;
602
611
  };
603
612
 
604
- export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
613
+ export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionDataRef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
package/dist/index.js CHANGED
@@ -251,6 +251,7 @@ var treeFeature = {
251
251
  "aria-selected": "false",
252
252
  "aria-label": item.getItemName(),
253
253
  "aria-level": itemMeta.level + 1,
254
+ "aria-expanded": item.isFolder() ? item.isExpanded() : void 0,
254
255
  tabIndex: item.isFocused() ? 0 : -1,
255
256
  onClick: (e) => {
256
257
  item.setFocused();
@@ -760,7 +761,9 @@ var selectionFeature = {
760
761
  },
761
762
  selectUpTo: ({ tree, item }, ctrl) => {
762
763
  const indexA = item.getItemMeta().index;
763
- const indexB = tree.getFocusedItem().getItemMeta().index;
764
+ const { selectUpToAnchorId } = tree.getDataRef().current;
765
+ const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
766
+ const indexB = itemB.getItemMeta().index;
764
767
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
765
768
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
766
769
  if (!ctrl) {
@@ -791,6 +794,9 @@ var selectionFeature = {
791
794
  } else {
792
795
  tree.setSelectedItems([item.getItemMeta().itemId]);
793
796
  }
797
+ if (!e.shiftKey) {
798
+ tree.getDataRef().current.selectUpToAnchorId = item.getId();
799
+ }
794
800
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
795
801
  }
796
802
  })
@@ -858,8 +864,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
858
864
  return includeFolders ? [itemId, ...descendants] : descendants;
859
865
  };
860
866
  var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
861
- yield tree.loadItemData(itemId);
862
- if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
867
+ const item = tree.getItemInstance(itemId);
868
+ if (!item.hasLoadedData()) {
869
+ yield tree.loadItemData(itemId);
870
+ }
871
+ if (!tree.getConfig().isItemFolder(item)) {
863
872
  return [itemId];
864
873
  }
865
874
  const childrenIds = yield tree.loadChildrenIds(itemId);
@@ -871,17 +880,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
871
880
  return includeFolders ? [itemId, ...descendants] : descendants;
872
881
  });
873
882
  var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
874
- tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
875
- ...items,
876
- itemId
877
- ]);
878
- try {
879
- yield callback();
880
- } finally {
881
- tree.applySubStateUpdate(
882
- "loadingCheckPropagationItems",
883
- (items) => items.filter((id) => id !== itemId)
884
- );
883
+ const prom = callback();
884
+ const immediate = {};
885
+ const firstCompleted = yield Promise.race([prom, immediate]);
886
+ if (firstCompleted !== immediate) {
887
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
888
+ ...items,
889
+ itemId
890
+ ]);
891
+ try {
892
+ yield prom;
893
+ } finally {
894
+ tree.applySubStateUpdate(
895
+ "loadingCheckPropagationItems",
896
+ (items) => items.filter((id) => id !== itemId)
897
+ );
898
+ }
899
+ } else {
900
+ yield prom;
885
901
  }
886
902
  });
887
903
  var checkboxesFeature = {
@@ -1103,16 +1119,27 @@ var hotkeysCoreFeature = {
1103
1119
 
1104
1120
  // src/features/async-data-loader/feature.ts
1105
1121
  var getDataRef = (tree) => {
1106
- var _a, _b, _c, _d;
1122
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1107
1123
  const dataRef = tree.getDataRef();
1108
1124
  (_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
1109
1125
  (_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
1126
+ (_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
1127
+ (_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
1110
1128
  return dataRef;
1111
1129
  };
1112
1130
  var loadItemData = (tree, itemId) => __async(null, null, function* () {
1113
- var _a;
1131
+ var _a, _b;
1114
1132
  const config = tree.getConfig();
1115
1133
  const dataRef = getDataRef(tree);
1134
+ if (tree.getState().loadingItemData.includes(itemId)) {
1135
+ return new Promise((resolve) => {
1136
+ var _a2, _b2;
1137
+ (_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1138
+ dataRef.current.loadingDataSubs[itemId].push(() => {
1139
+ resolve(dataRef.current.itemData[itemId]);
1140
+ });
1141
+ });
1142
+ }
1116
1143
  if (!dataRef.current.itemData[itemId]) {
1117
1144
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1118
1145
  ...loadingItemData,
@@ -1126,13 +1153,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
1126
1153
  "loadingItemData",
1127
1154
  (loadingItemData) => loadingItemData.filter((id) => id !== itemId)
1128
1155
  );
1156
+ (_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
1129
1157
  return item;
1130
1158
  });
1131
1159
  var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1132
- var _a, _b;
1160
+ var _a, _b, _c;
1133
1161
  const config = tree.getConfig();
1134
1162
  const dataRef = getDataRef(tree);
1135
1163
  let childrenIds;
1164
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
1165
+ return new Promise((resolve) => {
1166
+ var _a2, _b2;
1167
+ (_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1168
+ dataRef.current.loadingChildrenSubs[itemId].push(() => {
1169
+ resolve(dataRef.current.childrenIds[itemId]);
1170
+ });
1171
+ });
1172
+ }
1136
1173
  if (!dataRef.current.childrenIds[itemId]) {
1137
1174
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1138
1175
  ...loadingItemChildrens,
@@ -1164,6 +1201,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1164
1201
  "loadingItemChildrens",
1165
1202
  (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
1166
1203
  );
1204
+ (_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
1167
1205
  return childrenIds;
1168
1206
  });
1169
1207
  var asyncDataLoaderFeature = {
@@ -1240,6 +1278,10 @@ var asyncDataLoaderFeature = {
1240
1278
  const dataRef = tree.getDataRef();
1241
1279
  dataRef.current.itemData[itemId] = data;
1242
1280
  tree.rebuildTree();
1281
+ },
1282
+ hasLoadedData: ({ tree, itemId }) => {
1283
+ const dataRef = tree.getDataRef();
1284
+ return dataRef.current.itemData[itemId] !== void 0;
1243
1285
  }
1244
1286
  }
1245
1287
  };
@@ -1291,7 +1333,8 @@ var syncDataLoaderFeature = {
1291
1333
  loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
1292
1334
  },
1293
1335
  itemInstance: {
1294
- isLoading: () => false
1336
+ isLoading: () => false,
1337
+ hasLoadedData: () => true
1295
1338
  }
1296
1339
  };
1297
1340
 
package/dist/index.mjs CHANGED
@@ -207,6 +207,7 @@ var treeFeature = {
207
207
  "aria-selected": "false",
208
208
  "aria-label": item.getItemName(),
209
209
  "aria-level": itemMeta.level + 1,
210
+ "aria-expanded": item.isFolder() ? item.isExpanded() : void 0,
210
211
  tabIndex: item.isFocused() ? 0 : -1,
211
212
  onClick: (e) => {
212
213
  item.setFocused();
@@ -716,7 +717,9 @@ var selectionFeature = {
716
717
  },
717
718
  selectUpTo: ({ tree, item }, ctrl) => {
718
719
  const indexA = item.getItemMeta().index;
719
- const indexB = tree.getFocusedItem().getItemMeta().index;
720
+ const { selectUpToAnchorId } = tree.getDataRef().current;
721
+ const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
722
+ const indexB = itemB.getItemMeta().index;
720
723
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
721
724
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
722
725
  if (!ctrl) {
@@ -747,6 +750,9 @@ var selectionFeature = {
747
750
  } else {
748
751
  tree.setSelectedItems([item.getItemMeta().itemId]);
749
752
  }
753
+ if (!e.shiftKey) {
754
+ tree.getDataRef().current.selectUpToAnchorId = item.getId();
755
+ }
750
756
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
751
757
  }
752
758
  })
@@ -814,8 +820,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
814
820
  return includeFolders ? [itemId, ...descendants] : descendants;
815
821
  };
816
822
  var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
817
- yield tree.loadItemData(itemId);
818
- if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
823
+ const item = tree.getItemInstance(itemId);
824
+ if (!item.hasLoadedData()) {
825
+ yield tree.loadItemData(itemId);
826
+ }
827
+ if (!tree.getConfig().isItemFolder(item)) {
819
828
  return [itemId];
820
829
  }
821
830
  const childrenIds = yield tree.loadChildrenIds(itemId);
@@ -827,17 +836,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
827
836
  return includeFolders ? [itemId, ...descendants] : descendants;
828
837
  });
829
838
  var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
830
- tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
831
- ...items,
832
- itemId
833
- ]);
834
- try {
835
- yield callback();
836
- } finally {
837
- tree.applySubStateUpdate(
838
- "loadingCheckPropagationItems",
839
- (items) => items.filter((id) => id !== itemId)
840
- );
839
+ const prom = callback();
840
+ const immediate = {};
841
+ const firstCompleted = yield Promise.race([prom, immediate]);
842
+ if (firstCompleted !== immediate) {
843
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
844
+ ...items,
845
+ itemId
846
+ ]);
847
+ try {
848
+ yield prom;
849
+ } finally {
850
+ tree.applySubStateUpdate(
851
+ "loadingCheckPropagationItems",
852
+ (items) => items.filter((id) => id !== itemId)
853
+ );
854
+ }
855
+ } else {
856
+ yield prom;
841
857
  }
842
858
  });
843
859
  var checkboxesFeature = {
@@ -1059,16 +1075,27 @@ var hotkeysCoreFeature = {
1059
1075
 
1060
1076
  // src/features/async-data-loader/feature.ts
1061
1077
  var getDataRef = (tree) => {
1062
- var _a, _b, _c, _d;
1078
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1063
1079
  const dataRef = tree.getDataRef();
1064
1080
  (_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
1065
1081
  (_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
1082
+ (_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
1083
+ (_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
1066
1084
  return dataRef;
1067
1085
  };
1068
1086
  var loadItemData = (tree, itemId) => __async(null, null, function* () {
1069
- var _a;
1087
+ var _a, _b;
1070
1088
  const config = tree.getConfig();
1071
1089
  const dataRef = getDataRef(tree);
1090
+ if (tree.getState().loadingItemData.includes(itemId)) {
1091
+ return new Promise((resolve) => {
1092
+ var _a2, _b2;
1093
+ (_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1094
+ dataRef.current.loadingDataSubs[itemId].push(() => {
1095
+ resolve(dataRef.current.itemData[itemId]);
1096
+ });
1097
+ });
1098
+ }
1072
1099
  if (!dataRef.current.itemData[itemId]) {
1073
1100
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1074
1101
  ...loadingItemData,
@@ -1082,13 +1109,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
1082
1109
  "loadingItemData",
1083
1110
  (loadingItemData) => loadingItemData.filter((id) => id !== itemId)
1084
1111
  );
1112
+ (_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
1085
1113
  return item;
1086
1114
  });
1087
1115
  var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1088
- var _a, _b;
1116
+ var _a, _b, _c;
1089
1117
  const config = tree.getConfig();
1090
1118
  const dataRef = getDataRef(tree);
1091
1119
  let childrenIds;
1120
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
1121
+ return new Promise((resolve) => {
1122
+ var _a2, _b2;
1123
+ (_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1124
+ dataRef.current.loadingChildrenSubs[itemId].push(() => {
1125
+ resolve(dataRef.current.childrenIds[itemId]);
1126
+ });
1127
+ });
1128
+ }
1092
1129
  if (!dataRef.current.childrenIds[itemId]) {
1093
1130
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1094
1131
  ...loadingItemChildrens,
@@ -1120,6 +1157,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1120
1157
  "loadingItemChildrens",
1121
1158
  (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
1122
1159
  );
1160
+ (_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
1123
1161
  return childrenIds;
1124
1162
  });
1125
1163
  var asyncDataLoaderFeature = {
@@ -1196,6 +1234,10 @@ var asyncDataLoaderFeature = {
1196
1234
  const dataRef = tree.getDataRef();
1197
1235
  dataRef.current.itemData[itemId] = data;
1198
1236
  tree.rebuildTree();
1237
+ },
1238
+ hasLoadedData: ({ tree, itemId }) => {
1239
+ const dataRef = tree.getDataRef();
1240
+ return dataRef.current.itemData[itemId] !== void 0;
1199
1241
  }
1200
1242
  }
1201
1243
  };
@@ -1247,7 +1289,8 @@ var syncDataLoaderFeature = {
1247
1289
  loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
1248
1290
  },
1249
1291
  itemInstance: {
1250
- isLoading: () => false
1292
+ isLoading: () => false,
1293
+ hasLoadedData: () => true
1251
1294
  }
1252
1295
  };
1253
1296
 
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "checkbox",
14
14
  "hook"
15
15
  ],
16
- "version": "1.5.1",
16
+ "version": "1.6.0",
17
17
  "main": "dist/index.d.ts",
18
18
  "module": "dist/index.mjs",
19
19
  "types": "dist/index.d.mts",
@@ -6,6 +6,8 @@ const getDataRef = <T>(tree: TreeInstance<T>) => {
6
6
  const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
7
7
  dataRef.current.itemData ??= {};
8
8
  dataRef.current.childrenIds ??= {};
9
+ dataRef.current.loadingDataSubs ??= {};
10
+ dataRef.current.loadingChildrenSubs ??= {};
9
11
  return dataRef;
10
12
  };
11
13
 
@@ -13,6 +15,16 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
13
15
  const config = tree.getConfig();
14
16
  const dataRef = getDataRef(tree);
15
17
 
18
+ if (tree.getState().loadingItemData.includes(itemId)) {
19
+ // if currently loading, await existing load
20
+ return new Promise<T>((resolve) => {
21
+ dataRef.current.loadingDataSubs[itemId] ??= [];
22
+ dataRef.current.loadingDataSubs[itemId].push(() => {
23
+ resolve(dataRef.current.itemData[itemId]);
24
+ });
25
+ });
26
+ }
27
+
16
28
  if (!dataRef.current.itemData[itemId]) {
17
29
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
18
30
  ...loadingItemData,
@@ -26,6 +38,7 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
26
38
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
27
39
  loadingItemData.filter((id) => id !== itemId),
28
40
  );
41
+ dataRef.current.loadingDataSubs[itemId]?.forEach((cb) => cb());
29
42
 
30
43
  return item;
31
44
  };
@@ -35,6 +48,16 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
35
48
  const dataRef = getDataRef(tree);
36
49
  let childrenIds: string[];
37
50
 
51
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
52
+ // if currently loading, await existing load
53
+ return new Promise<string[]>((resolve) => {
54
+ dataRef.current.loadingChildrenSubs[itemId] ??= [];
55
+ dataRef.current.loadingChildrenSubs[itemId].push(() => {
56
+ resolve(dataRef.current.childrenIds[itemId]);
57
+ });
58
+ });
59
+ }
60
+
38
61
  if (!dataRef.current.childrenIds[itemId]) {
39
62
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
40
63
  ...loadingItemChildrens,
@@ -66,6 +89,7 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
66
89
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) =>
67
90
  loadingItemChildrens.filter((id) => id !== itemId),
68
91
  );
92
+ dataRef.current.loadingChildrenSubs[itemId]?.forEach((cb) => cb());
69
93
 
70
94
  return childrenIds;
71
95
  };
@@ -166,5 +190,9 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
166
190
  dataRef.current.itemData[itemId] = data;
167
191
  tree.rebuildTree();
168
192
  },
193
+ hasLoadedData: ({ tree, itemId }) => {
194
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
195
+ return dataRef.current.itemData[itemId] !== undefined;
196
+ },
169
197
  },
170
198
  };
@@ -4,6 +4,11 @@ import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
4
4
  export interface AsyncDataLoaderDataRef<T = any> {
5
5
  itemData: Record<string, T>;
6
6
  childrenIds: Record<string, string[]>;
7
+
8
+ // If an item load is requested while it is already loading, we reuse the existing load promise
9
+ // and store callbacks to be called when the load completes
10
+ loadingDataSubs: Record<string, (() => void)[]>;
11
+ loadingChildrenSubs: Record<string, (() => void)[]>;
7
12
  }
8
13
 
9
14
  /**
@@ -50,6 +55,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
50
55
  /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
51
56
  updateCachedData: (data: T | undefined) => void;
52
57
  updateCachedChildrenIds: (childrenIds: string[]) => void;
58
+ hasLoadedData: () => boolean;
53
59
  isLoading: () => boolean;
54
60
  };
55
61
  hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
@@ -22,8 +22,11 @@ const getAllDescendants = async <T>(
22
22
  itemId: string,
23
23
  includeFolders = false,
24
24
  ): Promise<string[]> => {
25
- await tree.loadItemData(itemId);
26
- if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
25
+ const item = tree.getItemInstance(itemId);
26
+ if (!item.hasLoadedData()) {
27
+ await tree.loadItemData(itemId);
28
+ }
29
+ if (!tree.getConfig().isItemFolder(item)) {
27
30
  return [itemId];
28
31
  }
29
32
  const childrenIds = await tree.loadChildrenIds(itemId);
@@ -42,16 +45,25 @@ const withLoadingState = async <T>(
42
45
  itemId: string,
43
46
  callback: () => Promise<void>,
44
47
  ) => {
45
- tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
46
- ...items,
47
- itemId,
48
- ]);
49
- try {
50
- await callback();
51
- } finally {
52
- tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
53
- items.filter((id) => id !== itemId),
54
- );
48
+ const prom = callback();
49
+ const immediate = {};
50
+ const firstCompleted = await Promise.race([prom, immediate]);
51
+ if (firstCompleted !== immediate) {
52
+ // don't update loading state if load is immediate
53
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
54
+ ...items,
55
+ itemId,
56
+ ]);
57
+ try {
58
+ await prom;
59
+ } finally {
60
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
61
+ items.filter((id) => id !== itemId),
62
+ );
63
+ }
64
+ } else {
65
+ // immediately completed, await anyway to keep errors in the same execution flow
66
+ await prom;
55
67
  }
56
68
  };
57
69
 
@@ -1,5 +1,6 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
+ import type { SelectionDataRef } from "./types";
3
4
 
4
5
  export const selectionFeature: FeatureImplementation = {
5
6
  key: "selection",
@@ -50,8 +51,12 @@ export const selectionFeature: FeatureImplementation = {
50
51
 
51
52
  selectUpTo: ({ tree, item }, ctrl: boolean) => {
52
53
  const indexA = item.getItemMeta().index;
53
- // TODO dont use focused item as anchor, but last primary-clicked item
54
- const indexB = tree.getFocusedItem().getItemMeta().index;
54
+ const { selectUpToAnchorId } =
55
+ tree.getDataRef<SelectionDataRef>().current;
56
+ const itemB = selectUpToAnchorId
57
+ ? tree.getItemInstance(selectUpToAnchorId)
58
+ : tree.getFocusedItem();
59
+ const indexB = itemB.getItemMeta().index;
55
60
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
56
61
  const newSelectedItems = tree
57
62
  .getItems()
@@ -90,6 +95,11 @@ export const selectionFeature: FeatureImplementation = {
90
95
  tree.setSelectedItems([item.getItemMeta().itemId]);
91
96
  }
92
97
 
98
+ if (!e.shiftKey) {
99
+ tree.getDataRef<SelectionDataRef>().current.selectUpToAnchorId =
100
+ item.getId();
101
+ }
102
+
93
103
  prev?.()?.onClick?.(e);
94
104
  },
95
105
  }),
@@ -1,5 +1,9 @@
1
1
  import { ItemInstance, SetStateFn } from "../../types/core";
2
2
 
3
+ export interface SelectionDataRef {
4
+ selectUpToAnchorId?: string | null;
5
+ }
6
+
3
7
  export type SelectionFeatureDef<T> = {
4
8
  state: {
5
9
  selectedItems: string[];
@@ -58,5 +58,6 @@ export const syncDataLoaderFeature: FeatureImplementation = {
58
58
 
59
59
  itemInstance: {
60
60
  isLoading: () => false,
61
+ hasLoadedData: () => true,
61
62
  },
62
63
  };
@@ -27,7 +27,10 @@ export type SyncDataLoaderFeatureDef<T> = {
27
27
  retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
28
28
  };
29
29
  itemInstance: {
30
+ /** Returns false. Provided for consistency with async data loader */
30
31
  isLoading: () => boolean;
32
+ /** Returns true. Provided for consistency with async data loader */
33
+ hasLoadedData: () => boolean;
31
34
  };
32
35
  hotkeys: never;
33
36
  };
@@ -144,6 +144,7 @@ export const treeFeature: FeatureImplementation<any> = {
144
144
  "aria-selected": "false",
145
145
  "aria-label": item.getItemName(),
146
146
  "aria-level": itemMeta.level + 1,
147
+ "aria-expanded": item.isFolder() ? item.isExpanded() : undefined,
147
148
  tabIndex: item.isFocused() ? 0 : -1,
148
149
  onClick: (e: MouseEvent) => {
149
150
  item.setFocused();
@@ -250,6 +250,7 @@ describe("core-feature/tree", () => {
250
250
  "aria-posinset": 2,
251
251
  "aria-selected": "false",
252
252
  "aria-setsize": 4,
253
+ "aria-expanded": false,
253
254
  onClick: expect.any(Function),
254
255
  ref: expect.any(Function),
255
256
  role: "treeitem",
@@ -264,12 +265,29 @@ describe("core-feature/tree", () => {
264
265
  "aria-posinset": 1,
265
266
  "aria-selected": "false",
266
267
  "aria-setsize": 4,
268
+ "aria-expanded": true,
267
269
  onClick: expect.any(Function),
268
270
  ref: expect.any(Function),
269
271
  role: "treeitem",
270
272
  tabIndex: 0,
271
273
  });
272
274
  });
275
+
276
+ it("generates aria-expanded for non expanded folder", () => {
277
+ expect(tree.instance.getItemInstance("x3").getProps()).toEqual(
278
+ expect.objectContaining({
279
+ "aria-expanded": false,
280
+ }),
281
+ );
282
+ });
283
+
284
+ it("generates aria-expanded for leaf", () => {
285
+ expect(tree.instance.getItemInstance("x111").getProps()).toEqual(
286
+ expect.objectContaining({
287
+ "aria-expanded": undefined,
288
+ }),
289
+ );
290
+ });
273
291
  });
274
292
 
275
293
  describe("util functions", () => {