@headless-tree/core 1.5.1 → 1.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @headless-tree/core
2
2
 
3
+ ## 1.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4ddeaf3: Fixed behavior where shift-selecting an item with no previously selected or focused item would multiselect all items from the top to the clicked item. Now, shift-selecting an item with no previously clicked items will only select the clicked item (#176)
8
+
9
+ ## 1.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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)
14
+
15
+ ### Patch Changes
16
+
17
+ - 287fe40: Added missing `aria-expanded` attribute to tree items (thanks to @gordey4doronin for the contribution) (#171)
18
+ - 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)
19
+
3
20
  ## 1.5.1
4
21
 
5
22
  ### 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();
@@ -758,9 +759,16 @@ var selectionFeature = {
758
759
  const { selectedItems } = tree.getState();
759
760
  return selectedItems.includes(itemId);
760
761
  },
761
- selectUpTo: ({ tree, item }, ctrl) => {
762
+ selectUpTo: ({ tree, item, itemId }, ctrl) => {
762
763
  const indexA = item.getItemMeta().index;
763
- const indexB = tree.getFocusedItem().getItemMeta().index;
764
+ const dataRef = tree.getDataRef();
765
+ if (!dataRef.current.selectUpToAnchorId) {
766
+ dataRef.current.selectUpToAnchorId = itemId;
767
+ tree.setSelectedItems([itemId]);
768
+ return;
769
+ }
770
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
771
+ const indexB = itemB.getItemMeta().index;
764
772
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
765
773
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
766
774
  if (!ctrl) {
@@ -780,7 +788,7 @@ var selectionFeature = {
780
788
  item.select();
781
789
  }
782
790
  },
783
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
791
+ getProps: ({ tree, item, itemId, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
784
792
  "aria-selected": item.isSelected() ? "true" : "false",
785
793
  onClick: (e) => {
786
794
  var _a, _b;
@@ -789,7 +797,10 @@ var selectionFeature = {
789
797
  } else if (e.ctrlKey || e.metaKey) {
790
798
  item.toggleSelect();
791
799
  } else {
792
- tree.setSelectedItems([item.getItemMeta().itemId]);
800
+ tree.setSelectedItems([itemId]);
801
+ }
802
+ if (!e.shiftKey) {
803
+ tree.getDataRef().current.selectUpToAnchorId = itemId;
793
804
  }
794
805
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
795
806
  }
@@ -858,8 +869,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
858
869
  return includeFolders ? [itemId, ...descendants] : descendants;
859
870
  };
860
871
  var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
861
- yield tree.loadItemData(itemId);
862
- if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
872
+ const item = tree.getItemInstance(itemId);
873
+ if (!item.hasLoadedData()) {
874
+ yield tree.loadItemData(itemId);
875
+ }
876
+ if (!tree.getConfig().isItemFolder(item)) {
863
877
  return [itemId];
864
878
  }
865
879
  const childrenIds = yield tree.loadChildrenIds(itemId);
@@ -871,17 +885,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
871
885
  return includeFolders ? [itemId, ...descendants] : descendants;
872
886
  });
873
887
  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
- );
888
+ const prom = callback();
889
+ const immediate = {};
890
+ const firstCompleted = yield Promise.race([prom, immediate]);
891
+ if (firstCompleted !== immediate) {
892
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
893
+ ...items,
894
+ itemId
895
+ ]);
896
+ try {
897
+ yield prom;
898
+ } finally {
899
+ tree.applySubStateUpdate(
900
+ "loadingCheckPropagationItems",
901
+ (items) => items.filter((id) => id !== itemId)
902
+ );
903
+ }
904
+ } else {
905
+ yield prom;
885
906
  }
886
907
  });
887
908
  var checkboxesFeature = {
@@ -1103,16 +1124,27 @@ var hotkeysCoreFeature = {
1103
1124
 
1104
1125
  // src/features/async-data-loader/feature.ts
1105
1126
  var getDataRef = (tree) => {
1106
- var _a, _b, _c, _d;
1127
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1107
1128
  const dataRef = tree.getDataRef();
1108
1129
  (_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
1109
1130
  (_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
1131
+ (_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
1132
+ (_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
1110
1133
  return dataRef;
1111
1134
  };
1112
1135
  var loadItemData = (tree, itemId) => __async(null, null, function* () {
1113
- var _a;
1136
+ var _a, _b;
1114
1137
  const config = tree.getConfig();
1115
1138
  const dataRef = getDataRef(tree);
1139
+ if (tree.getState().loadingItemData.includes(itemId)) {
1140
+ return new Promise((resolve) => {
1141
+ var _a2, _b2;
1142
+ (_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1143
+ dataRef.current.loadingDataSubs[itemId].push(() => {
1144
+ resolve(dataRef.current.itemData[itemId]);
1145
+ });
1146
+ });
1147
+ }
1116
1148
  if (!dataRef.current.itemData[itemId]) {
1117
1149
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1118
1150
  ...loadingItemData,
@@ -1126,13 +1158,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
1126
1158
  "loadingItemData",
1127
1159
  (loadingItemData) => loadingItemData.filter((id) => id !== itemId)
1128
1160
  );
1161
+ (_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
1129
1162
  return item;
1130
1163
  });
1131
1164
  var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1132
- var _a, _b;
1165
+ var _a, _b, _c;
1133
1166
  const config = tree.getConfig();
1134
1167
  const dataRef = getDataRef(tree);
1135
1168
  let childrenIds;
1169
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
1170
+ return new Promise((resolve) => {
1171
+ var _a2, _b2;
1172
+ (_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1173
+ dataRef.current.loadingChildrenSubs[itemId].push(() => {
1174
+ resolve(dataRef.current.childrenIds[itemId]);
1175
+ });
1176
+ });
1177
+ }
1136
1178
  if (!dataRef.current.childrenIds[itemId]) {
1137
1179
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1138
1180
  ...loadingItemChildrens,
@@ -1164,6 +1206,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1164
1206
  "loadingItemChildrens",
1165
1207
  (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
1166
1208
  );
1209
+ (_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
1167
1210
  return childrenIds;
1168
1211
  });
1169
1212
  var asyncDataLoaderFeature = {
@@ -1240,6 +1283,10 @@ var asyncDataLoaderFeature = {
1240
1283
  const dataRef = tree.getDataRef();
1241
1284
  dataRef.current.itemData[itemId] = data;
1242
1285
  tree.rebuildTree();
1286
+ },
1287
+ hasLoadedData: ({ tree, itemId }) => {
1288
+ const dataRef = tree.getDataRef();
1289
+ return dataRef.current.itemData[itemId] !== void 0;
1243
1290
  }
1244
1291
  }
1245
1292
  };
@@ -1291,7 +1338,8 @@ var syncDataLoaderFeature = {
1291
1338
  loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
1292
1339
  },
1293
1340
  itemInstance: {
1294
- isLoading: () => false
1341
+ isLoading: () => false,
1342
+ hasLoadedData: () => true
1295
1343
  }
1296
1344
  };
1297
1345
 
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();
@@ -714,9 +715,16 @@ var selectionFeature = {
714
715
  const { selectedItems } = tree.getState();
715
716
  return selectedItems.includes(itemId);
716
717
  },
717
- selectUpTo: ({ tree, item }, ctrl) => {
718
+ selectUpTo: ({ tree, item, itemId }, ctrl) => {
718
719
  const indexA = item.getItemMeta().index;
719
- const indexB = tree.getFocusedItem().getItemMeta().index;
720
+ const dataRef = tree.getDataRef();
721
+ if (!dataRef.current.selectUpToAnchorId) {
722
+ dataRef.current.selectUpToAnchorId = itemId;
723
+ tree.setSelectedItems([itemId]);
724
+ return;
725
+ }
726
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
727
+ const indexB = itemB.getItemMeta().index;
720
728
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
721
729
  const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
722
730
  if (!ctrl) {
@@ -736,7 +744,7 @@ var selectionFeature = {
736
744
  item.select();
737
745
  }
738
746
  },
739
- getProps: ({ tree, item, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
747
+ getProps: ({ tree, item, itemId, prev }) => __spreadProps(__spreadValues({}, prev == null ? void 0 : prev()), {
740
748
  "aria-selected": item.isSelected() ? "true" : "false",
741
749
  onClick: (e) => {
742
750
  var _a, _b;
@@ -745,7 +753,10 @@ var selectionFeature = {
745
753
  } else if (e.ctrlKey || e.metaKey) {
746
754
  item.toggleSelect();
747
755
  } else {
748
- tree.setSelectedItems([item.getItemMeta().itemId]);
756
+ tree.setSelectedItems([itemId]);
757
+ }
758
+ if (!e.shiftKey) {
759
+ tree.getDataRef().current.selectUpToAnchorId = itemId;
749
760
  }
750
761
  (_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
751
762
  }
@@ -814,8 +825,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
814
825
  return includeFolders ? [itemId, ...descendants] : descendants;
815
826
  };
816
827
  var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
817
- yield tree.loadItemData(itemId);
818
- if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
828
+ const item = tree.getItemInstance(itemId);
829
+ if (!item.hasLoadedData()) {
830
+ yield tree.loadItemData(itemId);
831
+ }
832
+ if (!tree.getConfig().isItemFolder(item)) {
819
833
  return [itemId];
820
834
  }
821
835
  const childrenIds = yield tree.loadChildrenIds(itemId);
@@ -827,17 +841,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
827
841
  return includeFolders ? [itemId, ...descendants] : descendants;
828
842
  });
829
843
  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
- );
844
+ const prom = callback();
845
+ const immediate = {};
846
+ const firstCompleted = yield Promise.race([prom, immediate]);
847
+ if (firstCompleted !== immediate) {
848
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
849
+ ...items,
850
+ itemId
851
+ ]);
852
+ try {
853
+ yield prom;
854
+ } finally {
855
+ tree.applySubStateUpdate(
856
+ "loadingCheckPropagationItems",
857
+ (items) => items.filter((id) => id !== itemId)
858
+ );
859
+ }
860
+ } else {
861
+ yield prom;
841
862
  }
842
863
  });
843
864
  var checkboxesFeature = {
@@ -1059,16 +1080,27 @@ var hotkeysCoreFeature = {
1059
1080
 
1060
1081
  // src/features/async-data-loader/feature.ts
1061
1082
  var getDataRef = (tree) => {
1062
- var _a, _b, _c, _d;
1083
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1063
1084
  const dataRef = tree.getDataRef();
1064
1085
  (_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
1065
1086
  (_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
1087
+ (_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
1088
+ (_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
1066
1089
  return dataRef;
1067
1090
  };
1068
1091
  var loadItemData = (tree, itemId) => __async(null, null, function* () {
1069
- var _a;
1092
+ var _a, _b;
1070
1093
  const config = tree.getConfig();
1071
1094
  const dataRef = getDataRef(tree);
1095
+ if (tree.getState().loadingItemData.includes(itemId)) {
1096
+ return new Promise((resolve) => {
1097
+ var _a2, _b2;
1098
+ (_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1099
+ dataRef.current.loadingDataSubs[itemId].push(() => {
1100
+ resolve(dataRef.current.itemData[itemId]);
1101
+ });
1102
+ });
1103
+ }
1072
1104
  if (!dataRef.current.itemData[itemId]) {
1073
1105
  tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1074
1106
  ...loadingItemData,
@@ -1082,13 +1114,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
1082
1114
  "loadingItemData",
1083
1115
  (loadingItemData) => loadingItemData.filter((id) => id !== itemId)
1084
1116
  );
1117
+ (_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
1085
1118
  return item;
1086
1119
  });
1087
1120
  var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1088
- var _a, _b;
1121
+ var _a, _b, _c;
1089
1122
  const config = tree.getConfig();
1090
1123
  const dataRef = getDataRef(tree);
1091
1124
  let childrenIds;
1125
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
1126
+ return new Promise((resolve) => {
1127
+ var _a2, _b2;
1128
+ (_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
1129
+ dataRef.current.loadingChildrenSubs[itemId].push(() => {
1130
+ resolve(dataRef.current.childrenIds[itemId]);
1131
+ });
1132
+ });
1133
+ }
1092
1134
  if (!dataRef.current.childrenIds[itemId]) {
1093
1135
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1094
1136
  ...loadingItemChildrens,
@@ -1120,6 +1162,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1120
1162
  "loadingItemChildrens",
1121
1163
  (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
1122
1164
  );
1165
+ (_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
1123
1166
  return childrenIds;
1124
1167
  });
1125
1168
  var asyncDataLoaderFeature = {
@@ -1196,6 +1239,10 @@ var asyncDataLoaderFeature = {
1196
1239
  const dataRef = tree.getDataRef();
1197
1240
  dataRef.current.itemData[itemId] = data;
1198
1241
  tree.rebuildTree();
1242
+ },
1243
+ hasLoadedData: ({ tree, itemId }) => {
1244
+ const dataRef = tree.getDataRef();
1245
+ return dataRef.current.itemData[itemId] !== void 0;
1199
1246
  }
1200
1247
  }
1201
1248
  };
@@ -1247,7 +1294,8 @@ var syncDataLoaderFeature = {
1247
1294
  loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
1248
1295
  },
1249
1296
  itemInstance: {
1250
- isLoading: () => false
1297
+ isLoading: () => false,
1298
+ hasLoadedData: () => true
1251
1299
  }
1252
1300
  };
1253
1301
 
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.1",
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",
@@ -48,10 +49,18 @@ export const selectionFeature: FeatureImplementation = {
48
49
  return selectedItems.includes(itemId);
49
50
  },
50
51
 
51
- selectUpTo: ({ tree, item }, ctrl: boolean) => {
52
+ selectUpTo: ({ tree, item, itemId }, 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 dataRef = tree.getDataRef<SelectionDataRef>();
55
+
56
+ if (!dataRef.current.selectUpToAnchorId) {
57
+ dataRef.current.selectUpToAnchorId = itemId;
58
+ tree.setSelectedItems([itemId]);
59
+ return;
60
+ }
61
+
62
+ const itemB = tree.getItemInstance(dataRef.current.selectUpToAnchorId);
63
+ const indexB = itemB.getItemMeta().index;
55
64
  const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
56
65
  const newSelectedItems = tree
57
66
  .getItems()
@@ -78,7 +87,7 @@ export const selectionFeature: FeatureImplementation = {
78
87
  }
79
88
  },
80
89
 
81
- getProps: ({ tree, item, prev }) => ({
90
+ getProps: ({ tree, item, itemId, prev }) => ({
82
91
  ...prev?.(),
83
92
  "aria-selected": item.isSelected() ? "true" : "false",
84
93
  onClick: (e: MouseEvent) => {
@@ -87,7 +96,12 @@ export const selectionFeature: FeatureImplementation = {
87
96
  } else if (e.ctrlKey || e.metaKey) {
88
97
  item.toggleSelect();
89
98
  } else {
90
- tree.setSelectedItems([item.getItemMeta().itemId]);
99
+ tree.setSelectedItems([itemId]);
100
+ }
101
+
102
+ if (!e.shiftKey) {
103
+ tree.getDataRef<SelectionDataRef>().current.selectUpToAnchorId =
104
+ itemId;
91
105
  }
92
106
 
93
107
  prev?.()?.onClick?.(e);
@@ -176,9 +176,8 @@ describe("core-feature/selections", () => {
176
176
 
177
177
  it("should handle selectUpTo without ctrl", () => {
178
178
  const setSelectedItems = tree.mockedHandler("setSelectedItems");
179
- tree.instance.getItemInstance("x111").toggleSelect();
180
- tree.instance.getItemInstance("x112").toggleSelect();
181
- tree.instance.getItemInstance("x112").setFocused();
179
+ tree.do.ctrlSelectItem("x111");
180
+ tree.do.ctrlSelectItem("x112");
182
181
  tree.instance.getItemInstance("x114").selectUpTo(false);
183
182
  expect(setSelectedItems).toHaveBeenCalledWith([
184
183
  "x112",
@@ -190,9 +189,8 @@ describe("core-feature/selections", () => {
190
189
 
191
190
  it("should handle selectUpTo with ctrl", () => {
192
191
  const setSelectedItems = tree.mockedHandler("setSelectedItems");
193
- tree.instance.getItemInstance("x111").toggleSelect();
194
- tree.instance.getItemInstance("x112").toggleSelect();
195
- tree.instance.getItemInstance("x112").setFocused();
192
+ tree.do.ctrlSelectItem("x111");
193
+ tree.do.ctrlSelectItem("x112");
196
194
  tree.instance.getItemInstance("x114").selectUpTo(true);
197
195
  expect(setSelectedItems).toHaveBeenCalledWith([
198
196
  "x111",
@@ -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", () => {