@workday/canvas-kit-react 9.0.0-alpha.415-next.17 → 9.0.0-alpha.417-next.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/collection/index.ts +1 -0
  2. package/collection/lib/useCursorListModel.tsx +17 -18
  3. package/collection/lib/useListItemAllowChildStrings.ts +3 -3
  4. package/collection/lib/useListItemRovingFocus.tsx +26 -7
  5. package/collection/lib/useListLoader.ts +313 -0
  6. package/dist/commonjs/action-bar/lib/ActionBar.d.ts +427 -124
  7. package/dist/commonjs/action-bar/lib/ActionBar.d.ts.map +1 -1
  8. package/dist/commonjs/action-bar/lib/ActionBarItem.d.ts +21 -12
  9. package/dist/commonjs/action-bar/lib/ActionBarItem.d.ts.map +1 -1
  10. package/dist/commonjs/action-bar/lib/ActionBarList.d.ts +21 -12
  11. package/dist/commonjs/action-bar/lib/ActionBarList.d.ts.map +1 -1
  12. package/dist/commonjs/action-bar/lib/ActionBarOverflowButton.d.ts +28 -16
  13. package/dist/commonjs/action-bar/lib/ActionBarOverflowButton.d.ts.map +1 -1
  14. package/dist/commonjs/action-bar/lib/useActionBarModel.d.ts +205 -52
  15. package/dist/commonjs/action-bar/lib/useActionBarModel.d.ts.map +1 -1
  16. package/dist/commonjs/breadcrumbs/lib/Breadcrumbs.d.ts +448 -136
  17. package/dist/commonjs/breadcrumbs/lib/Breadcrumbs.d.ts.map +1 -1
  18. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts +28 -16
  19. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts.map +1 -1
  20. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsItem.d.ts +21 -12
  21. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsItem.d.ts.map +1 -1
  22. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsList.d.ts +14 -8
  23. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsList.d.ts.map +1 -1
  24. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsMenu.d.ts +208 -64
  25. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsMenu.d.ts.map +1 -1
  26. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts +28 -16
  27. package/dist/commonjs/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts.map +1 -1
  28. package/dist/commonjs/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts +205 -52
  29. package/dist/commonjs/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts.map +1 -1
  30. package/dist/commonjs/collection/index.d.ts +1 -0
  31. package/dist/commonjs/collection/index.d.ts.map +1 -1
  32. package/dist/commonjs/collection/index.js +1 -0
  33. package/dist/commonjs/collection/lib/ListBox.d.ts +162 -48
  34. package/dist/commonjs/collection/lib/ListBox.d.ts.map +1 -1
  35. package/dist/commonjs/collection/lib/useCursorListModel.d.ts +354 -56
  36. package/dist/commonjs/collection/lib/useCursorListModel.d.ts.map +1 -1
  37. package/dist/commonjs/collection/lib/useCursorListModel.js +12 -15
  38. package/dist/commonjs/collection/lib/useGridModel.d.ts +155 -44
  39. package/dist/commonjs/collection/lib/useGridModel.d.ts.map +1 -1
  40. package/dist/commonjs/collection/lib/useListItemAllowChildStrings.d.ts +10 -7
  41. package/dist/commonjs/collection/lib/useListItemAllowChildStrings.d.ts.map +1 -1
  42. package/dist/commonjs/collection/lib/useListItemAllowChildStrings.js +3 -3
  43. package/dist/commonjs/collection/lib/useListItemRegister.d.ts +7 -4
  44. package/dist/commonjs/collection/lib/useListItemRegister.d.ts.map +1 -1
  45. package/dist/commonjs/collection/lib/useListItemRovingFocus.d.ts +7 -5
  46. package/dist/commonjs/collection/lib/useListItemRovingFocus.d.ts.map +1 -1
  47. package/dist/commonjs/collection/lib/useListItemRovingFocus.js +21 -8
  48. package/dist/commonjs/collection/lib/useListItemSelect.d.ts +7 -4
  49. package/dist/commonjs/collection/lib/useListItemSelect.d.ts.map +1 -1
  50. package/dist/commonjs/collection/lib/useListLoader.d.ts +67 -0
  51. package/dist/commonjs/collection/lib/useListLoader.d.ts.map +1 -0
  52. package/dist/commonjs/collection/lib/useListLoader.js +213 -0
  53. package/dist/commonjs/collection/lib/useListModel.d.ts +155 -44
  54. package/dist/commonjs/collection/lib/useListModel.d.ts.map +1 -1
  55. package/dist/commonjs/collection/lib/useListResetCursorOnBlur.d.ts +7 -4
  56. package/dist/commonjs/collection/lib/useListResetCursorOnBlur.d.ts.map +1 -1
  57. package/dist/commonjs/collection/lib/useOverflowListItemMeasure.d.ts +7 -4
  58. package/dist/commonjs/collection/lib/useOverflowListItemMeasure.d.ts.map +1 -1
  59. package/dist/commonjs/collection/lib/useOverflowListMeasure.d.ts +7 -4
  60. package/dist/commonjs/collection/lib/useOverflowListMeasure.d.ts.map +1 -1
  61. package/dist/commonjs/collection/lib/useOverflowListModel.d.ts +191 -44
  62. package/dist/commonjs/collection/lib/useOverflowListModel.d.ts.map +1 -1
  63. package/dist/commonjs/collection/lib/useOverflowListTarget.d.ts +7 -4
  64. package/dist/commonjs/collection/lib/useOverflowListTarget.d.ts.map +1 -1
  65. package/dist/commonjs/collection/lib/useSelectionListModel.d.ts +155 -44
  66. package/dist/commonjs/collection/lib/useSelectionListModel.d.ts.map +1 -1
  67. package/dist/commonjs/menu/lib/Menu.d.ts +201 -60
  68. package/dist/commonjs/menu/lib/Menu.d.ts.map +1 -1
  69. package/dist/commonjs/menu/lib/MenuCard.d.ts +14 -8
  70. package/dist/commonjs/menu/lib/MenuCard.d.ts.map +1 -1
  71. package/dist/commonjs/menu/lib/MenuContextTarget.d.ts +21 -12
  72. package/dist/commonjs/menu/lib/MenuContextTarget.d.ts.map +1 -1
  73. package/dist/commonjs/menu/lib/MenuItem.d.ts +14 -9
  74. package/dist/commonjs/menu/lib/MenuItem.d.ts.map +1 -1
  75. package/dist/commonjs/menu/lib/MenuList.d.ts +14 -8
  76. package/dist/commonjs/menu/lib/MenuList.d.ts.map +1 -1
  77. package/dist/commonjs/menu/lib/MenuPopper.d.ts +7 -4
  78. package/dist/commonjs/menu/lib/MenuPopper.d.ts.map +1 -1
  79. package/dist/commonjs/menu/lib/MenuTarget.d.ts +35 -20
  80. package/dist/commonjs/menu/lib/MenuTarget.d.ts.map +1 -1
  81. package/dist/commonjs/menu/lib/useMenuModel.d.ts +173 -44
  82. package/dist/commonjs/menu/lib/useMenuModel.d.ts.map +1 -1
  83. package/dist/commonjs/tabs/lib/Tabs.d.ts +626 -176
  84. package/dist/commonjs/tabs/lib/Tabs.d.ts.map +1 -1
  85. package/dist/commonjs/tabs/lib/TabsItem.d.ts +28 -17
  86. package/dist/commonjs/tabs/lib/TabsItem.d.ts.map +1 -1
  87. package/dist/commonjs/tabs/lib/TabsList.d.ts +28 -16
  88. package/dist/commonjs/tabs/lib/TabsList.d.ts.map +1 -1
  89. package/dist/commonjs/tabs/lib/TabsMenuPopper.d.ts +7 -4
  90. package/dist/commonjs/tabs/lib/TabsMenuPopper.d.ts.map +1 -1
  91. package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts +28 -16
  92. package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
  93. package/dist/commonjs/tabs/lib/TabsPanel.d.ts +28 -16
  94. package/dist/commonjs/tabs/lib/TabsPanel.d.ts.map +1 -1
  95. package/dist/commonjs/tabs/lib/TabsPanels.d.ts +14 -8
  96. package/dist/commonjs/tabs/lib/TabsPanels.d.ts.map +1 -1
  97. package/dist/commonjs/tabs/lib/useTabsModel.d.ts +521 -116
  98. package/dist/commonjs/tabs/lib/useTabsModel.d.ts.map +1 -1
  99. package/dist/es6/action-bar/lib/ActionBar.d.ts +427 -124
  100. package/dist/es6/action-bar/lib/ActionBar.d.ts.map +1 -1
  101. package/dist/es6/action-bar/lib/ActionBarItem.d.ts +21 -12
  102. package/dist/es6/action-bar/lib/ActionBarItem.d.ts.map +1 -1
  103. package/dist/es6/action-bar/lib/ActionBarList.d.ts +21 -12
  104. package/dist/es6/action-bar/lib/ActionBarList.d.ts.map +1 -1
  105. package/dist/es6/action-bar/lib/ActionBarOverflowButton.d.ts +28 -16
  106. package/dist/es6/action-bar/lib/ActionBarOverflowButton.d.ts.map +1 -1
  107. package/dist/es6/action-bar/lib/useActionBarModel.d.ts +205 -52
  108. package/dist/es6/action-bar/lib/useActionBarModel.d.ts.map +1 -1
  109. package/dist/es6/breadcrumbs/lib/Breadcrumbs.d.ts +448 -136
  110. package/dist/es6/breadcrumbs/lib/Breadcrumbs.d.ts.map +1 -1
  111. package/dist/es6/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts +28 -16
  112. package/dist/es6/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts.map +1 -1
  113. package/dist/es6/breadcrumbs/lib/BreadcrumbsItem.d.ts +21 -12
  114. package/dist/es6/breadcrumbs/lib/BreadcrumbsItem.d.ts.map +1 -1
  115. package/dist/es6/breadcrumbs/lib/BreadcrumbsList.d.ts +14 -8
  116. package/dist/es6/breadcrumbs/lib/BreadcrumbsList.d.ts.map +1 -1
  117. package/dist/es6/breadcrumbs/lib/BreadcrumbsMenu.d.ts +208 -64
  118. package/dist/es6/breadcrumbs/lib/BreadcrumbsMenu.d.ts.map +1 -1
  119. package/dist/es6/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts +28 -16
  120. package/dist/es6/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts.map +1 -1
  121. package/dist/es6/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts +205 -52
  122. package/dist/es6/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts.map +1 -1
  123. package/dist/es6/collection/index.d.ts +1 -0
  124. package/dist/es6/collection/index.d.ts.map +1 -1
  125. package/dist/es6/collection/index.js +1 -0
  126. package/dist/es6/collection/lib/ListBox.d.ts +162 -48
  127. package/dist/es6/collection/lib/ListBox.d.ts.map +1 -1
  128. package/dist/es6/collection/lib/useCursorListModel.d.ts +354 -56
  129. package/dist/es6/collection/lib/useCursorListModel.d.ts.map +1 -1
  130. package/dist/es6/collection/lib/useCursorListModel.js +12 -15
  131. package/dist/es6/collection/lib/useGridModel.d.ts +155 -44
  132. package/dist/es6/collection/lib/useGridModel.d.ts.map +1 -1
  133. package/dist/es6/collection/lib/useListItemAllowChildStrings.d.ts +10 -7
  134. package/dist/es6/collection/lib/useListItemAllowChildStrings.d.ts.map +1 -1
  135. package/dist/es6/collection/lib/useListItemAllowChildStrings.js +3 -3
  136. package/dist/es6/collection/lib/useListItemRegister.d.ts +7 -4
  137. package/dist/es6/collection/lib/useListItemRegister.d.ts.map +1 -1
  138. package/dist/es6/collection/lib/useListItemRovingFocus.d.ts +7 -5
  139. package/dist/es6/collection/lib/useListItemRovingFocus.d.ts.map +1 -1
  140. package/dist/es6/collection/lib/useListItemRovingFocus.js +21 -8
  141. package/dist/es6/collection/lib/useListItemSelect.d.ts +7 -4
  142. package/dist/es6/collection/lib/useListItemSelect.d.ts.map +1 -1
  143. package/dist/es6/collection/lib/useListLoader.d.ts +67 -0
  144. package/dist/es6/collection/lib/useListLoader.d.ts.map +1 -0
  145. package/dist/es6/collection/lib/useListLoader.js +204 -0
  146. package/dist/es6/collection/lib/useListModel.d.ts +155 -44
  147. package/dist/es6/collection/lib/useListModel.d.ts.map +1 -1
  148. package/dist/es6/collection/lib/useListResetCursorOnBlur.d.ts +7 -4
  149. package/dist/es6/collection/lib/useListResetCursorOnBlur.d.ts.map +1 -1
  150. package/dist/es6/collection/lib/useOverflowListItemMeasure.d.ts +7 -4
  151. package/dist/es6/collection/lib/useOverflowListItemMeasure.d.ts.map +1 -1
  152. package/dist/es6/collection/lib/useOverflowListMeasure.d.ts +7 -4
  153. package/dist/es6/collection/lib/useOverflowListMeasure.d.ts.map +1 -1
  154. package/dist/es6/collection/lib/useOverflowListModel.d.ts +191 -44
  155. package/dist/es6/collection/lib/useOverflowListModel.d.ts.map +1 -1
  156. package/dist/es6/collection/lib/useOverflowListTarget.d.ts +7 -4
  157. package/dist/es6/collection/lib/useOverflowListTarget.d.ts.map +1 -1
  158. package/dist/es6/collection/lib/useSelectionListModel.d.ts +155 -44
  159. package/dist/es6/collection/lib/useSelectionListModel.d.ts.map +1 -1
  160. package/dist/es6/menu/lib/Menu.d.ts +201 -60
  161. package/dist/es6/menu/lib/Menu.d.ts.map +1 -1
  162. package/dist/es6/menu/lib/MenuCard.d.ts +14 -8
  163. package/dist/es6/menu/lib/MenuCard.d.ts.map +1 -1
  164. package/dist/es6/menu/lib/MenuContextTarget.d.ts +21 -12
  165. package/dist/es6/menu/lib/MenuContextTarget.d.ts.map +1 -1
  166. package/dist/es6/menu/lib/MenuItem.d.ts +14 -9
  167. package/dist/es6/menu/lib/MenuItem.d.ts.map +1 -1
  168. package/dist/es6/menu/lib/MenuList.d.ts +14 -8
  169. package/dist/es6/menu/lib/MenuList.d.ts.map +1 -1
  170. package/dist/es6/menu/lib/MenuPopper.d.ts +7 -4
  171. package/dist/es6/menu/lib/MenuPopper.d.ts.map +1 -1
  172. package/dist/es6/menu/lib/MenuTarget.d.ts +35 -20
  173. package/dist/es6/menu/lib/MenuTarget.d.ts.map +1 -1
  174. package/dist/es6/menu/lib/useMenuModel.d.ts +173 -44
  175. package/dist/es6/menu/lib/useMenuModel.d.ts.map +1 -1
  176. package/dist/es6/tabs/lib/Tabs.d.ts +626 -176
  177. package/dist/es6/tabs/lib/Tabs.d.ts.map +1 -1
  178. package/dist/es6/tabs/lib/TabsItem.d.ts +28 -17
  179. package/dist/es6/tabs/lib/TabsItem.d.ts.map +1 -1
  180. package/dist/es6/tabs/lib/TabsList.d.ts +28 -16
  181. package/dist/es6/tabs/lib/TabsList.d.ts.map +1 -1
  182. package/dist/es6/tabs/lib/TabsMenuPopper.d.ts +7 -4
  183. package/dist/es6/tabs/lib/TabsMenuPopper.d.ts.map +1 -1
  184. package/dist/es6/tabs/lib/TabsOverflowButton.d.ts +28 -16
  185. package/dist/es6/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
  186. package/dist/es6/tabs/lib/TabsPanel.d.ts +28 -16
  187. package/dist/es6/tabs/lib/TabsPanel.d.ts.map +1 -1
  188. package/dist/es6/tabs/lib/TabsPanels.d.ts +14 -8
  189. package/dist/es6/tabs/lib/TabsPanels.d.ts.map +1 -1
  190. package/dist/es6/tabs/lib/useTabsModel.d.ts +521 -116
  191. package/dist/es6/tabs/lib/useTabsModel.d.ts.map +1 -1
  192. package/package.json +3 -3
@@ -8,6 +8,7 @@ export * from './lib/useListRenderItem';
8
8
  export * from './lib/useListResetCursorOnBlur';
9
9
  export * from './lib/useListItemRovingFocus';
10
10
  export * from './lib/useListItemSelect';
11
+ export * from './lib/useListLoader';
11
12
  export * from './lib/useListModel';
12
13
  export * from './lib/useGridModel';
13
14
  export * from './lib/useListItemAllowChildStrings';
@@ -279,6 +279,8 @@ export const navigationManager = createNavigationManager({
279
279
  getLastOfRow,
280
280
  });
281
281
 
282
+ type Writeable<T> = {-readonly [P in keyof T]: T[P]};
283
+
282
284
  /**
283
285
  * A `CursorModel` extends a `ListModel` and adds a "cursor" to the list. A cursor is a pointer to a
284
286
  * current position in the list. The most common use-case is keeping track of which item currently
@@ -322,11 +324,9 @@ export const useCursorListModel = createModelHook({
322
324
  const pageSizeRef = React.useRef(config.pageSize);
323
325
  const columnCount = config.columnCount || 0;
324
326
  const list = useBaseListModel(config);
325
- const initialCurrentRef = React.useRef(
326
- config.initialCursorId || (config.items?.length ? list.state.items![0].id : '')
327
- );
328
327
  const navigation = config.navigation;
329
- const cursorIndexRef = React.useRef(-1);
328
+ // Cast as a readonly to signify this value should never be set
329
+ const cursorIndexRef = React.useRef(-1) as {readonly current: number};
330
330
  const setCursor = (index: number) => {
331
331
  const id = state.items[index]?.id || '';
332
332
  setCursorId(id);
@@ -334,9 +334,12 @@ export const useCursorListModel = createModelHook({
334
334
 
335
335
  // Keep the cursorIndex up to date with the cursor ID
336
336
  if (cursorId && list.state.items[cursorIndexRef.current]?.id !== cursorId) {
337
- cursorIndexRef.current = list.state.items.findIndex(item => item.id === cursorId);
337
+ // We cast back as a writeable because this is the only place the value should be changed.
338
+ (cursorIndexRef as Writeable<typeof cursorIndexRef>).current = list.state.items.findIndex(
339
+ item => item.id === cursorId
340
+ );
338
341
  } else if (!cursorId) {
339
- cursorIndexRef.current = -1;
342
+ (cursorIndexRef as Writeable<typeof cursorIndexRef>).current = -1;
340
343
  }
341
344
 
342
345
  const state = {
@@ -354,22 +357,18 @@ export const useCursorListModel = createModelHook({
354
357
  * based on the size of the list container and the number of items fitting within the container.
355
358
  */
356
359
  pageSizeRef,
360
+ /**
361
+ * A readonly [React.Ref](https://react.dev/learn/referencing-values-with-refs) that tracks the
362
+ * index of the `state.cursorId`. This value is automatically updated when the `state.cursorId`
363
+ * or the `items` change.
364
+ *
365
+ * @readonly
366
+ */
367
+ cursorIndexRef,
357
368
  };
358
369
 
359
370
  const events = {
360
371
  ...list.events,
361
- /**
362
- * Register an item to the list. Takes in an identifier, a React.Ref and an optional index. This
363
- * should be called on component mount
364
- */
365
- registerItem(data: Parameters<typeof list.events.registerItem>[0]) {
366
- // point the cursor at the first item
367
- if (!initialCurrentRef.current) {
368
- initialCurrentRef.current = list.getId(data.item);
369
- setCursorId(initialCurrentRef.current);
370
- }
371
- list.events.registerItem(data);
372
- },
373
372
  /** Directly sets the cursor to the list item by its identifier. */
374
373
  goTo(data: {id: string}) {
375
374
  const index = state.items.findIndex(item => item.id === data.id);
@@ -2,9 +2,9 @@ import {createElemPropsHook} from '@workday/canvas-kit-react/common';
2
2
  import {useListModel} from '@workday/canvas-kit-react/collection';
3
3
 
4
4
  /**
5
- * This elemProps hook allows for children values to be considered identifiers if the children
6
- * are strings. This can be useful for autocomplete or select components that allow string values.
7
- * This hook must be defined _before_ {@link useListItemRegister} because it sets the `data-id`
5
+ * This elemProps hook allows for children values to be considered identifiers if the children are
6
+ * strings. This can be useful for autocomplete or select components that allow string values. This
7
+ * hook must be passed _after_ {@link useListItemRegister} because this hook sets the `data-id`
8
8
  * attribute if one hasn't been defined by the application.
9
9
  *
10
10
  * An example might look like:
@@ -4,6 +4,13 @@ import {useIsRTL, createElemPropsHook} from '@workday/canvas-kit-react/common';
4
4
  import {useCursorListModel} from './useCursorListModel';
5
5
  import {keyboardEventToCursorEvents} from './keyUtils';
6
6
 
7
+ // retry a function each frame so we don't rely on the timing mechanism of React's render cycle.
8
+ const retryEachFrame = (cb: () => boolean, iterations: number) => {
9
+ if (cb() === false && iterations > 1) {
10
+ requestAnimationFrame(() => retryEachFrame(cb, iterations - 1));
11
+ }
12
+ };
13
+
7
14
  /**
8
15
  * This elemProps hook is used for cursor navigation by using [Roving
9
16
  * Tabindex](https://w3c.github.io/aria-practices/#kbd_roving_tabindex). Only a single item in the
@@ -19,7 +26,7 @@ import {keyboardEventToCursorEvents} from './keyUtils';
19
26
  ```
20
27
  */
21
28
  export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)(
22
- (model, ref, elemProps: {'data-id'?: string} = {}) => {
29
+ (model, _ref, elemProps: {'data-id'?: string} = {}) => {
23
30
  // Create a ref out of state. We don't want to watch state on unmount, so we use a ref to get the
24
31
  // current value at the time of unmounting. Otherwise, `state.items` would be a cached value of an
25
32
  // empty array
@@ -35,11 +42,17 @@ export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)(
35
42
  if (model.state.isVirtualized) {
36
43
  model.state.UNSTABLE_virtual.scrollToIndex(item.index);
37
44
  }
38
- requestAnimationFrame(() => {
39
- document
40
- .querySelector<HTMLElement>(`[data-focus-id="${`${model.state.id}-${item.id}`}"]`)
41
- ?.focus();
42
- });
45
+
46
+ // In React concurrent mode, there could be several render attempts before the element we're
47
+ // looking for could be available in the DOM
48
+ retryEachFrame(() => {
49
+ const element = document.querySelector<HTMLElement>(
50
+ `[data-focus-id="${`${model.state.id}-${item.id}`}"]`
51
+ );
52
+
53
+ element?.focus();
54
+ return !!element;
55
+ }, 5); // 5 should be enough, right?!
43
56
 
44
57
  keyDownRef.current = false;
45
58
  }
@@ -47,6 +60,13 @@ export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)(
47
60
  // eslint-disable-next-line react-hooks/exhaustive-deps
48
61
  }, [model.state.cursorId]);
49
62
 
63
+ // Roving focus must always have a focus stop to function correctly
64
+ React.useEffect(() => {
65
+ if (!model.state.cursorId && model.state.items.length) {
66
+ model.events.goTo({id: model.state.items[0].id});
67
+ }
68
+ }, [model.state.cursorId, model.state.items, model.events]);
69
+
50
70
  return {
51
71
  onKeyDown(event: React.KeyboardEvent) {
52
72
  const handled = keyboardEventToCursorEvents(event, model, isRTL);
@@ -64,7 +84,6 @@ export const useListItemRovingFocus = createElemPropsHook(useCursorListModel)(
64
84
  : !!elemProps['data-id'] && model.state.cursorId === elemProps['data-id']
65
85
  ? 0 // A name is known and cursor is here
66
86
  : -1, // A name is known an cursor is somewhere else
67
- ref,
68
87
  };
69
88
  }
70
89
  );
@@ -0,0 +1,313 @@
1
+ import React from 'react';
2
+
3
+ import {useListModel} from './useListModel';
4
+
5
+ export interface LoadReturn<T> {
6
+ items: T[];
7
+ total?: number;
8
+ }
9
+
10
+ export interface LoadParams {
11
+ pageNumber: number;
12
+ pageSize: number;
13
+ filter: CollectionLoader['filter'];
14
+ sorter: CollectionLoader['sorter'];
15
+ }
16
+ export interface AsyncCollectionConfig<T> {
17
+ pageSize: number;
18
+ total: number;
19
+ load(params: LoadParams): LoadReturn<T> | Promise<LoadReturn<T>>;
20
+ }
21
+
22
+ export interface CollectionLoader {
23
+ filter: string | Record<string, string>;
24
+ updateFilter(value: string): void;
25
+ sorter: string | Record<string, string>;
26
+ updateSorter(value: string): void;
27
+ isLoading: boolean;
28
+ }
29
+
30
+ /**
31
+ * A loading buffer is how many items we want to look at ahead of the current virtual window. A
32
+ * higher amount can load too many pages initially. A lower number can cause items to not be loaded
33
+ * yet when scrolling. This will probably become a floating number later based on scrolling speed.
34
+ * Ideally the faster the user scrolls, this number will be higher or "predict" where to focus loading
35
+ * so the user doesn't see unloaded items. But if the user is scrolling too fast, we may simply delay
36
+ * data loading. That logic would be based on timers. For now, we'll just hardcode.
37
+ */
38
+ const loadingBuffer = 3;
39
+
40
+ function updateItems<T>(newItems: T[], startIndex = 0) {
41
+ return function itemStateUpdater(items: T[]) {
42
+ // replace existing indexes with data from external service
43
+ newItems.forEach((item, index) => {
44
+ items[index + startIndex] = item;
45
+ });
46
+ const returnedItems = items.concat([]);
47
+ return returnedItems; // return a new reference
48
+ };
49
+ }
50
+
51
+ type Updater<T> = {total?: number; updater: (items: T[]) => T[]};
52
+
53
+ function load<T>(
54
+ params: {
55
+ pageNumber: number;
56
+ pageSize: number;
57
+ filter: CollectionLoader['filter'];
58
+ sorter: CollectionLoader['sorter'];
59
+ },
60
+ loadCb: AsyncCollectionConfig<T>['load'],
61
+ loadingCache: Record<number, boolean>
62
+ ): Promise<Updater<T>> {
63
+ if (loadingCache[params.pageNumber]) {
64
+ return Promise.resolve({updater: items => items});
65
+ }
66
+ loadingCache[params.pageNumber] = true;
67
+
68
+ return Promise.resolve(loadCb(params)).then(({items, total}) => {
69
+ loadingCache[params.pageNumber] = false;
70
+ return {total, updater: updateItems(items, (params.pageNumber - 1) * params.pageSize)};
71
+ });
72
+ }
73
+
74
+ export function getPageNumber(index: number, pageSize: number) {
75
+ if (index <= 0) {
76
+ return 1;
77
+ }
78
+
79
+ return Math.ceil((index + 1) / pageSize);
80
+ }
81
+
82
+ function clamp(input: number, min: number, max: number) {
83
+ return Math.max(Math.min(input, max), min);
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param start The start index of the virtual window
89
+ * @param end The end index of the virtual window
90
+ * @param pageSize Expected load page size
91
+ * @param items Sparse array of all loaded items
92
+ * @param overScan How many items ahead we want to look ahead to trigger loading. Unloaded items
93
+ * that are this many items outside the current start and end numbers will trigger a loading of
94
+ * a page
95
+ */
96
+ export function getPagesToLoad<T>(
97
+ start: number,
98
+ end: number,
99
+ pageSize: number,
100
+ items: T[],
101
+ overScan = 3
102
+ ): number[] {
103
+ const pagesToLoad: number[] = [];
104
+
105
+ const lookBehindIndex = clamp(start - overScan, 0, items.length - 1);
106
+ if (start >= 0 && !items[lookBehindIndex]) {
107
+ const pageNumber = getPageNumber(lookBehindIndex, pageSize);
108
+ pagesToLoad.push(pageNumber);
109
+ }
110
+
111
+ const lookaheadIndex = clamp(end + overScan - 1, 0, items.length - 1);
112
+ if (end < items.length - 1 && !items[lookaheadIndex]) {
113
+ const pageNumber = getPageNumber(lookaheadIndex, pageSize);
114
+ if (!pagesToLoad.includes(pageNumber)) {
115
+ pagesToLoad.push(pageNumber);
116
+ }
117
+ }
118
+
119
+ return pagesToLoad;
120
+ }
121
+
122
+ const resetItems = (total: number) => Array(total).fill(undefined);
123
+
124
+ /**
125
+ * Create a data loader and a model. The model should be passed to a collection component. The
126
+ * loader can be used to manipulate filters, sorters, and clear cache.
127
+ *
128
+ * A simple loader using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
129
+ * could look like the following:
130
+ *
131
+ * ```ts
132
+ * const {model, loader} = useListLoader(
133
+ * {
134
+ * total: 1000,
135
+ * pageSize: 20,
136
+ * async load({pageNumber, pageSize}) {
137
+ * return fetch('/myUrl')
138
+ * .then(response => response.json())
139
+ * .then(response => {
140
+ * return {total: response.total, items: response.items};
141
+ * });
142
+ * },
143
+ * },
144
+ * useListModel
145
+ * );
146
+ * ```
147
+ *
148
+ * @param config Config that contains the loader config and the model config
149
+ * @param modelHook A model hook that describes which model should be returned.
150
+ */
151
+ export function useListLoader<
152
+ T,
153
+ // I cannot get Typescript to accept models that extend from `useListModel` to be considered valid
154
+ M extends ((...args: any[]) => any) & Omit<typeof useListModel, 'TConfig' | 'Context'>
155
+ >(
156
+ config: AsyncCollectionConfig<T> & typeof useListModel.TConfig,
157
+ modelHook: M
158
+ ): {model: ReturnType<M>; loader: CollectionLoader} {
159
+ const [total, setTotal] = React.useState(config.total);
160
+ const [items, setItems] = React.useState<T[]>(() => {
161
+ return resetItems(config.total);
162
+ });
163
+
164
+ // keep track of pages that are currently loading
165
+ const loadingRef = React.useRef<Record<number, boolean>>({});
166
+
167
+ const [filter, setFilter] = React.useState<CollectionLoader['filter']>('');
168
+ const [sorter, setSorter] = React.useState<CollectionLoader['sorter']>('');
169
+
170
+ const updateItems = React.useCallback(
171
+ ({updater, total: newTotal}: Updater<T>) => {
172
+ if (newTotal !== undefined && total !== newTotal) {
173
+ setTotal(newTotal);
174
+ setItems(() => {
175
+ const items = resetItems(newTotal);
176
+ return updater(items);
177
+ });
178
+ return;
179
+ }
180
+ setItems(updater);
181
+ },
182
+ [total]
183
+ );
184
+
185
+ const updateFilter = (filter: CollectionLoader['filter']) => {
186
+ loadingRef.current = {};
187
+ setFilter(filter);
188
+ const params = {
189
+ pageNumber: 1,
190
+ pageSize: config.pageSize,
191
+ filter,
192
+ sorter,
193
+ };
194
+ load(params, loadRef.current, loadingRef.current).then(updateItems);
195
+ };
196
+
197
+ const updateSorter = (sorter: CollectionLoader['sorter']) => {
198
+ loadingRef.current = {};
199
+ setSorter(sorter);
200
+ load(
201
+ {
202
+ pageNumber: 1,
203
+ pageSize: config.pageSize,
204
+ filter,
205
+ sorter,
206
+ },
207
+ loadRef.current,
208
+ loadingRef.current
209
+ ).then(updateItems);
210
+ };
211
+
212
+ // Our `useEffect` functions use `config.load`, but we don't want to rerun the effects if the user
213
+ // sends a different load function every render
214
+ const loadRef = React.useRef(config.load);
215
+ loadRef.current = config.load;
216
+
217
+ const itemsRef = React.useRef(items);
218
+ itemsRef.current = items;
219
+ const requestAnimationFrameRef = React.useRef(0);
220
+
221
+ const shouldLoadIndex = (
222
+ navigationMethod: Exclude<keyof ReturnType<typeof useListModel>['navigation'], 'getItem'>,
223
+ eventKey: keyof ReturnType<typeof useListModel>['events']
224
+ ) => {
225
+ return (data: any) => {
226
+ const index = model.navigation[navigationMethod](model.state.cursorIndexRef.current, model);
227
+ if (!items[index]) {
228
+ load(
229
+ {
230
+ pageNumber: Math.floor(index / config.pageSize) + 1,
231
+ pageSize: config.pageSize,
232
+ filter,
233
+ sorter,
234
+ },
235
+ loadRef.current,
236
+ loadingRef.current
237
+ )
238
+ .then(updateItems)
239
+ .then(() => {
240
+ cancelAnimationFrame(requestAnimationFrameRef.current);
241
+ requestAnimationFrameRef.current = requestAnimationFrame(() => {
242
+ model.events[eventKey](data as any);
243
+ });
244
+ });
245
+ return false;
246
+ }
247
+ return true;
248
+ };
249
+ };
250
+
251
+ const model = modelHook(
252
+ modelHook.mergeConfig(config, {
253
+ items,
254
+ shouldGoToNext: shouldLoadIndex('getNext', 'goToNext'),
255
+ shouldGoToPrevious: shouldLoadIndex('getPrevious', 'goToPrevious'),
256
+ shouldGoToLast: shouldLoadIndex('getLast', 'goToLast'),
257
+ shouldGoToFirst: shouldLoadIndex('getFirst', 'goToFirst'),
258
+ shouldGoToNextPage: shouldLoadIndex('getNextPage', 'goToNextPage'),
259
+ shouldGoToPreviousPage: shouldLoadIndex('getPreviousPage', 'goToPreviousPage'),
260
+ })
261
+ ) as ReturnType<typeof useListModel>;
262
+
263
+ const {virtualItems} = model.state.UNSTABLE_virtual;
264
+
265
+ // Our only signal to trigger loading is if our virtual indexes are too close to boundaries.
266
+ React.useEffect(() => {
267
+ if (!virtualItems.length) {
268
+ return;
269
+ }
270
+
271
+ const firstItem = virtualItems[0];
272
+ const lastItem = virtualItems[virtualItems.length - 1];
273
+
274
+ const pagesToLoad = getPagesToLoad(
275
+ firstItem.index,
276
+ lastItem.index,
277
+ config.pageSize,
278
+ itemsRef.current,
279
+ loadingBuffer
280
+ );
281
+
282
+ pagesToLoad.forEach(pageNumber => {
283
+ load(
284
+ {pageNumber, pageSize: config.pageSize, filter, sorter},
285
+ loadRef.current,
286
+ loadingRef.current
287
+ ).then(updateItems);
288
+ });
289
+
290
+ return () => {
291
+ cancelAnimationFrame(requestAnimationFrameRef.current);
292
+ };
293
+ }, [virtualItems, config.pageSize, filter, sorter, updateItems]);
294
+
295
+ // Typescript won't allow me to say the included model extends `useListModel` for some reason, so
296
+ // we have no way to type check it. Without the constraint, Typescript doesn't know how type it
297
+ // properly, so we have an `as any`.
298
+ return {
299
+ model: model as any,
300
+ loader: {
301
+ filter,
302
+ updateFilter,
303
+ sorter,
304
+ updateSorter,
305
+ // we reduce the ref instead of a state variable because the loader mutates this ref and
306
+ // triggers state changes when loading is finished.
307
+ isLoading: Object.values(loadingRef.current).reduce(
308
+ (loading, result) => loading || result,
309
+ false
310
+ ),
311
+ },
312
+ };
313
+ }