@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
380 lines (379 loc) • 16.8 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useCursorListModel = exports.navigationManager = exports.wrappingNavigationManager = exports.getOffsetItem = exports.getWrappingOffsetItem = exports.getNextPage = exports.getPreviousPage = exports.getLastOfRow = exports.getFirstOfRow = exports.getLast = exports.getFirst = exports.createNavigationManager = void 0;
const react_1 = __importDefault(require("react"));
const common_1 = require("@workday/canvas-kit-react/common");
const useBaseListModel_1 = require("./useBaseListModel");
/**
* Factory function that does type checking to create navigation managers. Navigation managers
* are expected to handle all methods of a grid. If your use-case isn't meant to handle a grid,
* pick one of the existing navigation managers and override the methods you wish to implement.
*
* For example,
* ```tsx
* import {createNavigationManager, wrappingNavigationManager} from '@workday/canvas-kit-react/collection'
*
* const navigationManager = createNavigationManager({
* ...wrappingNavigationManager,
* getNext(id, {state}) {
* //
* }
* })
* ```
*/
const createNavigationManager = (manager) => manager;
exports.createNavigationManager = createNavigationManager;
/**
* Get the first item in a list regardless of column count
*/
const getFirst = () => 0;
exports.getFirst = getFirst;
/**
* Get the last item in a list regardless of column count
*/
const getLast = (_, { state }) => state.items.length - 1;
exports.getLast = getLast;
/**
* Get the first item in a row. If column count is 0, it will return the results of `getFirst`
*/
const getFirstOfRow = (index, { state }) => {
if (state.columnCount) {
const offset = index % state.columnCount;
return index - offset;
}
return (0, exports.getFirst)(index, { state });
};
exports.getFirstOfRow = getFirstOfRow;
/**
* get the last item in a row - if column count is 0, it will return the results of `getLast`
*/
const getLastOfRow = (index, { state }) => {
if (state.columnCount) {
const offset = (index % state.columnCount) - state.columnCount + 1;
let nextIndex = index - offset;
if (nextIndex >= state.items.length) {
nextIndex = state.items.length - 1;
}
return nextIndex;
}
return (0, exports.getLast)(index, { state });
};
exports.getLastOfRow = getLastOfRow;
/**
* get the item in the previous page. This can be author defined. By default it will return the
* first item for a list, and the first item in the same column for a grid.
*/
const getPreviousPage = (index, { state }) => {
if (state.columnCount) {
return index % state.columnCount;
}
return (0, exports.getFirst)(index, { state });
};
exports.getPreviousPage = getPreviousPage;
/**
* get the item in the next page. This can be author defined. By default, it will return the last
* item for a list, and the last item in the same column for a grid.
*/
const getNextPage = (index, { state }) => {
if (state.columnCount) {
const lastRowIndex = state.items.length - state.columnCount;
return lastRowIndex + (index % state.columnCount);
}
return (0, exports.getLast)(index, { state });
};
exports.getNextPage = getNextPage;
const getItem = (id, { state }) => {
return state.items.find(item => item.id === id);
};
const getWrappingOffsetItem = (offset) => (index, { state }, tries = state.items.length) => {
if (Number.isNaN(index)) {
// we have no valid index. If the offset is positive, we'll return the first item
if (offset === 1) {
return (0, exports.getFirst)(index, { state });
}
if (offset === -1) {
return (0, exports.getLast)(index, { state });
}
}
const items = state.items;
let nextIndex = index + offset;
// calculate idealLength as in if the grid was a perfect rectangle
const rows = Math.ceil(items.length / state.columnCount);
const idealLength = rows * state.columnCount;
if (nextIndex < 0) {
if (offset === -1) {
// if the offset is -1, we want to wrap to the end
nextIndex = items.length - 1;
}
else {
// if the offset is smaller than -1, we want to wrap by column
if (idealLength + nextIndex >= items.length) {
// we'll overflow the grid because there isn't enough items. Move `nextIndex` up so we wrap in
// the right spot
nextIndex -= state.columnCount;
}
nextIndex = idealLength + nextIndex;
}
}
else if (nextIndex >= items.length) {
if (offset === 1) {
// if the offset is 1, we want to wrap to the beginning
nextIndex = 0;
}
else {
// if the offset is larger than 1, we want to wrap by column
if (nextIndex - idealLength < 0) {
// we're going to overflow the grid because there isn't enough items. Move `nextIndex` down to
// the missing item in the next row. This way we'll end up wrapping in the right spot
nextIndex += state.columnCount;
}
nextIndex = nextIndex - idealLength;
}
}
if (items.length > 1 && state.nonInteractiveIds.includes(items[nextIndex].id) && tries > 0) {
// The next item is disabled, try again, but only if we haven't already tried everything.
// Avoid an infinite loop with `tries`
return (0, exports.getWrappingOffsetItem)(offset)(nextIndex, { state }, tries - 1);
}
return nextIndex;
};
exports.getWrappingOffsetItem = getWrappingOffsetItem;
const getOffsetItem = (offset) => (index, { state }, tries = state.items.length) => {
const { items, columnCount } = state;
let nextIndex = index + offset;
if (Math.abs(offset) < columnCount) {
// if we're here, the columnCount is non-zero and the absolute value of offset is less than the
// column count. We don't want to wrap, so we'll bound within the row
const currentIndexInRow = index % columnCount;
const nextIndexInRow = nextIndex - index + currentIndexInRow;
if (nextIndexInRow >= columnCount || nextIndexInRow < 0) {
nextIndex = index;
}
}
else if (columnCount) {
// if we're here, there's a column count, but the offset will move into another row. We need to
// bound to row values
const nextRow = Math.floor(nextIndex / columnCount);
if (nextRow < 0 || nextRow >= columnCount) {
nextIndex = index;
}
// make sure we don't go out of bounds if the grid isn't a perfect rectangle
if (nextIndex > items.length - 1) {
nextIndex = index;
}
}
// make sure we're always in bounds
if (nextIndex < 0) {
nextIndex = 0;
}
else if (nextIndex >= items.length) {
nextIndex = items.length - 1;
}
if (state.nonInteractiveIds.includes(items[nextIndex].id) && tries > 0) {
// The next item is disabled, try again, but only if we haven't already tried everything.
// Avoid an infinite loop with `tries`
return (0, exports.getOffsetItem)(offset)(nextIndex, { state }, tries - 1);
}
return nextIndex;
};
exports.getOffsetItem = getOffsetItem;
/**
* The default navigation manager of lists. This navigation manager will wrap around when the edge
* is hit. For example, if the user is the the right-most item in a list or right-most item in a
* row, the cursor will wrap around to the beginning or the next row.
*/
exports.wrappingNavigationManager = (0, exports.createNavigationManager)({
getFirst: exports.getFirst,
getLast: exports.getLast,
getItem,
getNext: (0, exports.getWrappingOffsetItem)(1),
getNextRow: (index, { state }) => (0, exports.getWrappingOffsetItem)(state.columnCount)(index, { state }),
getPrevious: (0, exports.getWrappingOffsetItem)(-1),
getPreviousRow: (index, { state }) => (0, exports.getWrappingOffsetItem)(-state.columnCount)(index, { state }),
getPreviousPage: exports.getPreviousPage,
getNextPage: exports.getNextPage,
getFirstOfRow: exports.getFirstOfRow,
getLastOfRow: exports.getLastOfRow,
});
/**
* The default navigation of grids. This navigation manager will not wrap, but will stop when an
* edge is detected. This could be the last item in a list or the last item of a row in a grid.
*/
exports.navigationManager = (0, exports.createNavigationManager)({
getFirst: exports.getFirst,
getLast: exports.getLast,
getItem,
getNext: (0, exports.getOffsetItem)(1),
getNextRow: (index, { state }) => (0, exports.getOffsetItem)(state.columnCount)(index, { state }),
getPrevious: (0, exports.getOffsetItem)(-1),
getPreviousRow: (index, { state }) => (0, exports.getOffsetItem)(-state.columnCount)(index, { state }),
getPreviousPage: exports.getPreviousPage,
getNextPage: exports.getNextPage,
getFirstOfRow: exports.getFirstOfRow,
getLastOfRow: exports.getLastOfRow,
});
/**
* A `CursorModel` extends a `ListModel` and adds a "cursor" to the list. A cursor is a pointer to a
* current position in the list. The most common use-case is keeping track of which item currently
* has focus within the list. Many w3c list role types specify a single tab stop within the list.
*/
exports.useCursorListModel = (0, common_1.createModelHook)({
defaultConfig: {
...useBaseListModel_1.useBaseListModel.defaultConfig,
/**
* Initial cursor position. If not provided, the cursor will point to the first item in the list
*/
initialCursorId: '',
/**
* If this is set it will cause a wrapping of a list that will turn it into a grid
* @default 0
*/
columnCount: 0,
/**
* Controls the state changes when the user sends navigation events to the model. For example,
* when the user hits the "right" arrow, a behavior hook will determine directionality
* (left-to-right or right-to-left) and call the correct navigation method. In our example, a
* left-to-right language would send a `getNext`. The navigation manager may return the next item
* in the list. Different managers can be created for slightly different use cases. The default
* navigation manager will accept `orientation` and directionality to determine mapping.
*
* An example override might be a tab list with an overflow menu that is meant to be transparent
* to screen reader users. This would require the overflow menu to accept both up/down keys as
* well as left/right keys to give a more consistent experience to all users.
*/
navigation: exports.wrappingNavigationManager,
/**
* Controls how much a pageUp/pageDown navigation request will jump. If not provided, the size
* of the list and number of items rendered will determine this value.
*/
pageSize: 0,
},
requiredConfig: useBaseListModel_1.useBaseListModel.requiredConfig,
contextOverride: useBaseListModel_1.useBaseListModel.Context,
})(config => {
var _a;
const [cursorId, setCursorId] = react_1.default.useState(config.initialCursorId);
const pageSizeRef = react_1.default.useRef(config.pageSize);
const columnCount = config.columnCount || 0;
const list = (0, useBaseListModel_1.useBaseListModel)(config);
const navigation = config.navigation;
// Cast as a readonly to signify this value should never be set
const cursorIndexRef = react_1.default.useRef(-1);
const setCursor = (index) => {
var _a;
const id = ((_a = state.items[index]) === null || _a === void 0 ? void 0 : _a.id) || '';
setCursorId(id);
};
// Keep the cursorIndex up to date with the cursor ID
if (cursorId && ((_a = list.state.items[cursorIndexRef.current]) === null || _a === void 0 ? void 0 : _a.id) !== cursorId) {
// We cast back as a writeable because this is the only place the value should be changed.
cursorIndexRef.current = list.state.items.findIndex(item => item.id === cursorId);
}
else if (!cursorId) {
cursorIndexRef.current = -1;
}
const state = {
...list.state,
/** The id of the list item the cursor is pointing to */
cursorId,
/**
* Any positive non-zero value treats the list like a grid with rows and columns
* @default 0
* @private Use useGridModel instead to make a grid instead of a list
*/
columnCount,
/**
* A React.Ref of the current page size. Either provided as config, or determined at runtime
* based on the size of the list container and the number of items fitting within the container.
*/
pageSizeRef,
/**
* A readonly [React.Ref](https://react.dev/learn/referencing-values-with-refs) that tracks the
* index of the `state.cursorId`. This value is automatically updated when the `state.cursorId`
* or the `items` change.
*
* @readonly
*/
cursorIndexRef,
};
const events = {
...list.events,
/** Directly sets the cursor to the list item by its identifier. */
goTo(data) {
const index = state.items.findIndex(item => item.id === data.id);
setCursor(index);
},
/**
* Set the cursor to the "next" item in the list. This event delegates to the `getNext` method of
* the navigation manager. For a list, the default navigation manager will wrap if cursor is
* currently on the last item. For a grid, the default navigation manager will stay on the last
* item in a row.
*/
goToNext() {
const index = navigation.getNext(cursorIndexRef.current, { state });
setCursor(index);
},
/**
* Set the cursor to the "previous" item in the list. If the beginning of the list is detected,
* it will wrap to the last item
*/
goToPrevious() {
const index = navigation.getPrevious(cursorIndexRef.current, { state });
setCursor(index);
},
/**
* Previous item perpendicular to the orientation of a list, or the previous row in a grid. For
* example, if a list is horizontal, the previous row would describe an up direction. This could
* be ignored by the navigation manager, or return the same result as `previous()`. In a grid,
* this would be the previous row (current position - column count).
*/
goToPreviousRow() {
const index = navigation.getPreviousRow(cursorIndexRef.current, {
state,
});
setCursor(index);
},
/**
* Next item perpendicular to the orientation of a list, or the next row in a grid. For example,
* if a list is horizontal, the next row would describe a down direction. This could be ignored by
* the navigation manager, or return the same result as `next()`. In a grid, this would be the
* next row (current position + column count).
*/
goToNextRow() {
const index = navigation.getNextRow(cursorIndexRef.current, { state });
setCursor(index);
},
/** Set the cursor to the first item in the list */
goToFirst() {
const index = navigation.getFirst(cursorIndexRef.current, { state });
setCursor(index);
},
/** Set the cursor to the last item in the list */
goToLast() {
const index = navigation.getLast(cursorIndexRef.current, { state });
setCursor(index);
},
goToFirstOfRow() {
const index = navigation.getFirstOfRow(cursorIndexRef.current, { state });
setCursor(index);
},
goToLastOfRow() {
const index = navigation.getLastOfRow(cursorIndexRef.current, { state });
setCursor(index);
},
goToNextPage() {
const index = navigation.getNextPage(cursorIndexRef.current, { state });
setCursor(index);
},
goToPreviousPage() {
const index = navigation.getPreviousPage(cursorIndexRef.current, {
state,
});
setCursor(index);
},
};
return { ...list, state, events, navigation };
});
;