@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.
- package/collection/index.ts +1 -0
- package/collection/lib/useCursorListModel.tsx +17 -18
- package/collection/lib/useListItemAllowChildStrings.ts +3 -3
- package/collection/lib/useListItemRovingFocus.tsx +26 -7
- package/collection/lib/useListLoader.ts +313 -0
- package/dist/commonjs/action-bar/lib/ActionBar.d.ts +427 -124
- package/dist/commonjs/action-bar/lib/ActionBar.d.ts.map +1 -1
- package/dist/commonjs/action-bar/lib/ActionBarItem.d.ts +21 -12
- package/dist/commonjs/action-bar/lib/ActionBarItem.d.ts.map +1 -1
- package/dist/commonjs/action-bar/lib/ActionBarList.d.ts +21 -12
- package/dist/commonjs/action-bar/lib/ActionBarList.d.ts.map +1 -1
- package/dist/commonjs/action-bar/lib/ActionBarOverflowButton.d.ts +28 -16
- package/dist/commonjs/action-bar/lib/ActionBarOverflowButton.d.ts.map +1 -1
- package/dist/commonjs/action-bar/lib/useActionBarModel.d.ts +205 -52
- package/dist/commonjs/action-bar/lib/useActionBarModel.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/Breadcrumbs.d.ts +448 -136
- package/dist/commonjs/breadcrumbs/lib/Breadcrumbs.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts +28 -16
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsItem.d.ts +21 -12
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsItem.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsList.d.ts +14 -8
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsList.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsMenu.d.ts +208 -64
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsMenu.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts +28 -16
- package/dist/commonjs/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts.map +1 -1
- package/dist/commonjs/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts +205 -52
- package/dist/commonjs/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts.map +1 -1
- package/dist/commonjs/collection/index.d.ts +1 -0
- package/dist/commonjs/collection/index.d.ts.map +1 -1
- package/dist/commonjs/collection/index.js +1 -0
- package/dist/commonjs/collection/lib/ListBox.d.ts +162 -48
- package/dist/commonjs/collection/lib/ListBox.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useCursorListModel.d.ts +354 -56
- package/dist/commonjs/collection/lib/useCursorListModel.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useCursorListModel.js +12 -15
- package/dist/commonjs/collection/lib/useGridModel.d.ts +155 -44
- package/dist/commonjs/collection/lib/useGridModel.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListItemAllowChildStrings.d.ts +10 -7
- package/dist/commonjs/collection/lib/useListItemAllowChildStrings.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListItemAllowChildStrings.js +3 -3
- package/dist/commonjs/collection/lib/useListItemRegister.d.ts +7 -4
- package/dist/commonjs/collection/lib/useListItemRegister.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListItemRovingFocus.d.ts +7 -5
- package/dist/commonjs/collection/lib/useListItemRovingFocus.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListItemRovingFocus.js +21 -8
- package/dist/commonjs/collection/lib/useListItemSelect.d.ts +7 -4
- package/dist/commonjs/collection/lib/useListItemSelect.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListLoader.d.ts +67 -0
- package/dist/commonjs/collection/lib/useListLoader.d.ts.map +1 -0
- package/dist/commonjs/collection/lib/useListLoader.js +213 -0
- package/dist/commonjs/collection/lib/useListModel.d.ts +155 -44
- package/dist/commonjs/collection/lib/useListModel.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useListResetCursorOnBlur.d.ts +7 -4
- package/dist/commonjs/collection/lib/useListResetCursorOnBlur.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useOverflowListItemMeasure.d.ts +7 -4
- package/dist/commonjs/collection/lib/useOverflowListItemMeasure.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useOverflowListMeasure.d.ts +7 -4
- package/dist/commonjs/collection/lib/useOverflowListMeasure.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useOverflowListModel.d.ts +191 -44
- package/dist/commonjs/collection/lib/useOverflowListModel.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useOverflowListTarget.d.ts +7 -4
- package/dist/commonjs/collection/lib/useOverflowListTarget.d.ts.map +1 -1
- package/dist/commonjs/collection/lib/useSelectionListModel.d.ts +155 -44
- package/dist/commonjs/collection/lib/useSelectionListModel.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/Menu.d.ts +201 -60
- package/dist/commonjs/menu/lib/Menu.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuCard.d.ts +14 -8
- package/dist/commonjs/menu/lib/MenuCard.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuContextTarget.d.ts +21 -12
- package/dist/commonjs/menu/lib/MenuContextTarget.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuItem.d.ts +14 -9
- package/dist/commonjs/menu/lib/MenuItem.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuList.d.ts +14 -8
- package/dist/commonjs/menu/lib/MenuList.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuPopper.d.ts +7 -4
- package/dist/commonjs/menu/lib/MenuPopper.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/MenuTarget.d.ts +35 -20
- package/dist/commonjs/menu/lib/MenuTarget.d.ts.map +1 -1
- package/dist/commonjs/menu/lib/useMenuModel.d.ts +173 -44
- package/dist/commonjs/menu/lib/useMenuModel.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/Tabs.d.ts +626 -176
- package/dist/commonjs/tabs/lib/Tabs.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsItem.d.ts +28 -17
- package/dist/commonjs/tabs/lib/TabsItem.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsList.d.ts +28 -16
- package/dist/commonjs/tabs/lib/TabsList.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsMenuPopper.d.ts +7 -4
- package/dist/commonjs/tabs/lib/TabsMenuPopper.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts +28 -16
- package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsPanel.d.ts +28 -16
- package/dist/commonjs/tabs/lib/TabsPanel.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsPanels.d.ts +14 -8
- package/dist/commonjs/tabs/lib/TabsPanels.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/useTabsModel.d.ts +521 -116
- package/dist/commonjs/tabs/lib/useTabsModel.d.ts.map +1 -1
- package/dist/es6/action-bar/lib/ActionBar.d.ts +427 -124
- package/dist/es6/action-bar/lib/ActionBar.d.ts.map +1 -1
- package/dist/es6/action-bar/lib/ActionBarItem.d.ts +21 -12
- package/dist/es6/action-bar/lib/ActionBarItem.d.ts.map +1 -1
- package/dist/es6/action-bar/lib/ActionBarList.d.ts +21 -12
- package/dist/es6/action-bar/lib/ActionBarList.d.ts.map +1 -1
- package/dist/es6/action-bar/lib/ActionBarOverflowButton.d.ts +28 -16
- package/dist/es6/action-bar/lib/ActionBarOverflowButton.d.ts.map +1 -1
- package/dist/es6/action-bar/lib/useActionBarModel.d.ts +205 -52
- package/dist/es6/action-bar/lib/useActionBarModel.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/Breadcrumbs.d.ts +448 -136
- package/dist/es6/breadcrumbs/lib/Breadcrumbs.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts +28 -16
- package/dist/es6/breadcrumbs/lib/BreadcrumbsCurrentItem.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/BreadcrumbsItem.d.ts +21 -12
- package/dist/es6/breadcrumbs/lib/BreadcrumbsItem.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/BreadcrumbsList.d.ts +14 -8
- package/dist/es6/breadcrumbs/lib/BreadcrumbsList.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/BreadcrumbsMenu.d.ts +208 -64
- package/dist/es6/breadcrumbs/lib/BreadcrumbsMenu.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts +28 -16
- package/dist/es6/breadcrumbs/lib/BreadcrumbsOverflowButton.d.ts.map +1 -1
- package/dist/es6/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts +205 -52
- package/dist/es6/breadcrumbs/lib/hooks/useBreadcrumbsModel.d.ts.map +1 -1
- package/dist/es6/collection/index.d.ts +1 -0
- package/dist/es6/collection/index.d.ts.map +1 -1
- package/dist/es6/collection/index.js +1 -0
- package/dist/es6/collection/lib/ListBox.d.ts +162 -48
- package/dist/es6/collection/lib/ListBox.d.ts.map +1 -1
- package/dist/es6/collection/lib/useCursorListModel.d.ts +354 -56
- package/dist/es6/collection/lib/useCursorListModel.d.ts.map +1 -1
- package/dist/es6/collection/lib/useCursorListModel.js +12 -15
- package/dist/es6/collection/lib/useGridModel.d.ts +155 -44
- package/dist/es6/collection/lib/useGridModel.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListItemAllowChildStrings.d.ts +10 -7
- package/dist/es6/collection/lib/useListItemAllowChildStrings.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListItemAllowChildStrings.js +3 -3
- package/dist/es6/collection/lib/useListItemRegister.d.ts +7 -4
- package/dist/es6/collection/lib/useListItemRegister.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListItemRovingFocus.d.ts +7 -5
- package/dist/es6/collection/lib/useListItemRovingFocus.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListItemRovingFocus.js +21 -8
- package/dist/es6/collection/lib/useListItemSelect.d.ts +7 -4
- package/dist/es6/collection/lib/useListItemSelect.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListLoader.d.ts +67 -0
- package/dist/es6/collection/lib/useListLoader.d.ts.map +1 -0
- package/dist/es6/collection/lib/useListLoader.js +204 -0
- package/dist/es6/collection/lib/useListModel.d.ts +155 -44
- package/dist/es6/collection/lib/useListModel.d.ts.map +1 -1
- package/dist/es6/collection/lib/useListResetCursorOnBlur.d.ts +7 -4
- package/dist/es6/collection/lib/useListResetCursorOnBlur.d.ts.map +1 -1
- package/dist/es6/collection/lib/useOverflowListItemMeasure.d.ts +7 -4
- package/dist/es6/collection/lib/useOverflowListItemMeasure.d.ts.map +1 -1
- package/dist/es6/collection/lib/useOverflowListMeasure.d.ts +7 -4
- package/dist/es6/collection/lib/useOverflowListMeasure.d.ts.map +1 -1
- package/dist/es6/collection/lib/useOverflowListModel.d.ts +191 -44
- package/dist/es6/collection/lib/useOverflowListModel.d.ts.map +1 -1
- package/dist/es6/collection/lib/useOverflowListTarget.d.ts +7 -4
- package/dist/es6/collection/lib/useOverflowListTarget.d.ts.map +1 -1
- package/dist/es6/collection/lib/useSelectionListModel.d.ts +155 -44
- package/dist/es6/collection/lib/useSelectionListModel.d.ts.map +1 -1
- package/dist/es6/menu/lib/Menu.d.ts +201 -60
- package/dist/es6/menu/lib/Menu.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuCard.d.ts +14 -8
- package/dist/es6/menu/lib/MenuCard.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuContextTarget.d.ts +21 -12
- package/dist/es6/menu/lib/MenuContextTarget.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuItem.d.ts +14 -9
- package/dist/es6/menu/lib/MenuItem.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuList.d.ts +14 -8
- package/dist/es6/menu/lib/MenuList.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuPopper.d.ts +7 -4
- package/dist/es6/menu/lib/MenuPopper.d.ts.map +1 -1
- package/dist/es6/menu/lib/MenuTarget.d.ts +35 -20
- package/dist/es6/menu/lib/MenuTarget.d.ts.map +1 -1
- package/dist/es6/menu/lib/useMenuModel.d.ts +173 -44
- package/dist/es6/menu/lib/useMenuModel.d.ts.map +1 -1
- package/dist/es6/tabs/lib/Tabs.d.ts +626 -176
- package/dist/es6/tabs/lib/Tabs.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsItem.d.ts +28 -17
- package/dist/es6/tabs/lib/TabsItem.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsList.d.ts +28 -16
- package/dist/es6/tabs/lib/TabsList.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsMenuPopper.d.ts +7 -4
- package/dist/es6/tabs/lib/TabsMenuPopper.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsOverflowButton.d.ts +28 -16
- package/dist/es6/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsPanel.d.ts +28 -16
- package/dist/es6/tabs/lib/TabsPanel.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsPanels.d.ts +14 -8
- package/dist/es6/tabs/lib/TabsPanels.d.ts.map +1 -1
- package/dist/es6/tabs/lib/useTabsModel.d.ts +521 -116
- package/dist/es6/tabs/lib/useTabsModel.d.ts.map +1 -1
- package/package.json +3 -3
package/collection/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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,
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|