UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

380 lines (379 loc) • 16.8 kB
"use strict"; 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 }; });