UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

186 lines (185 loc) • 8.16 kB
import React from 'react'; import { useVirtual } from './react-virtual'; import { useUniqueId, createModelHook } from '@workday/canvas-kit-react/common'; export const defaultGetId = (item) => { if (process.env.NODE_ENV === 'development') { if (typeof item === 'object' && item.id === undefined) { console.warn("List item was an object, but no `getId` was passed to the model to inform the list where to find the item's identifier. Please pass a `getId` to the list model"); } } return item === undefined ? '' : typeof item === 'string' ? item : item.id || ''; }; export const defaultGetTextValue = (item) => { if (process.env.NODE_ENV === 'development') { if (typeof item === 'object' && item.text === undefined) { console.warn("List item was an object, but no `getTextValue` was passed to the model to inform the list where to find the item's text value. The item's text value is used for accessibility. Please pass a `getTextValue` to the list model"); } } return typeof item === 'string' ? item : item === undefined ? '' : item.text || ''; }; // interface Collection<T> { // getKeys(): string[]; // getItem(id: string): Item<T> | null; // at(index: number): Item<T> | null; // size: number; // } // TODO: Build an interface that allows representation of different collections. For example an array and a dynamically loaded dataset // class StaticCollection<T> implements Collection<T> { // // eslint-disable-next-line no-empty-function // private items: Item<T>[]; // constructor(items: T[], getId = defaultGetId, getTextValue = defaultGetTextValue) { // this.items = items.map((item, index) => ({ // index, // value: item, // id: getId(item), // textValue: getTextValue(item), // })); // } // getKeys() { // return this.items.map(item => item.id); // } // getItem(id: string) { // return this.items.find(item => item.id === id) || null; // } // at(index: number) { // return this.items[index] || null; // } // get size() { // return this.items.length; // } // } // force Typescript to use `Generic` as a symbol const genericDefaultConfig = { items: [], }; export const useBaseListModel = createModelHook({ defaultConfig: { ...genericDefaultConfig, /** IDREF of the list. Children ids can be derived from this id */ id: '', /** * Optional function to return an id of an item. If not provided, the default function will * return the `id` property from the object of each item. If you did not provide `items`, do not * override this function. If you don't provided `items` and instead provide static items via * JSX, the list will create an internal array of items where `id` is the only property and the * default `getId` will return the desired result. */ getId: defaultGetId, /** * Optional function to return the text representation of an item. If not provided, the default * function will return the `text` property of the object of each item or an empty string if * there is no `text` property. If you did not provide `items`, do not override this function. */ getTextValue: defaultGetTextValue, /** * Array of all ids which are currently disabled. This is used for navigation to skip over items * which are not focusable. */ nonInteractiveIds: [], /** * The orientation of a list of items. Values are either `vertical` or `horizontal`. This value will * effect which ids activate progression through a list. For example, `horizontal` will activate with * left and right arrows while `vertical` will activate with up and down arrows. * @default 'vertical' */ orientation: 'vertical', /** * Best guess to the default item height for virtualization. Getting this number correct * avoids a rerender while the list is initializing. * * @default 50 */ defaultItemHeight: 50, shouldVirtualize: true, }, })(config => { var _a; const id = useUniqueId(config.id); // Optimization to not redo items when `getId` and `getTextValue` references change. They will not // likely change during the lifecycle and we don't want to recalculate items when a lamba is // passed instead of a stable reference. const getIdRef = React.useRef(defaultGetId); const getTextValueRef = React.useRef(defaultGetTextValue); getIdRef.current = config.getId || defaultGetId; getTextValueRef.current = config.getTextValue || config.getId || defaultGetTextValue; const [orientation] = React.useState(config.orientation || 'vertical'); const [UNSTABLE_defaultItemHeight, setDefaultItemHeight] = React.useState(config.defaultItemHeight); const isVirtualized = config.shouldVirtualize && !!((_a = config.items) === null || _a === void 0 ? void 0 : _a.length); const indexRef = React.useRef(0); const containerRef = React.useRef(null); const items = React.useMemo(() => (config.items || []).map((item, index) => { return { id: getIdRef.current(item), index, value: item, textValue: getTextValueRef.current(item), }; }), [config.items]); const [staticItems, setStaticItems] = React.useState([]); const UNSTABLE_virtual = useVirtual({ size: items.length, parentRef: containerRef, estimateSize: React.useCallback(() => UNSTABLE_defaultItemHeight, [UNSTABLE_defaultItemHeight]), horizontal: config.orientation === 'horizontal', overscan: 3, // overscan of 3 helps rapid navigation }); // Force Typescript to recognize the `Generic` symbol const genericState = { items: items.length ? items : staticItems, }; const state = { ...genericState, UNSTABLE_virtual, UNSTABLE_defaultItemHeight, containerRef, id, orientation, indexRef, nonInteractiveIds: config.nonInteractiveIds || [], isVirtualized, }; const events = { /** * Register an item to the list. Takes in an identifier, a React.Ref and an optional index. This * should be called on component mount. This event is only called for static rendering. */ registerItem(data) { indexRef.current++; setStaticItems(items => { return items.concat({ ...data, value: data, index: items.length, }); }); }, /** * Unregister an item by its identifier. This should be called when the component is unmounted. * This event is only called for static rendering. */ unregisterItem(data) { setStaticItems(items => { // this extra `if` ensures reference stability for no-ops if (items.find(item => item.id === data.id)) { return items.filter(item => item.id !== data.id); } else { return items; } }); }, /** * Updates the default item height. This should only be called when item height is measured. * Calling this with a different default item height than previous will cause a virtual list to * recalculate the overall height of the list and invalidate any height caching. Doing this only * on the first item may save the user from experiencing odd scrolling behavior where the * scrollbar updates while scrolling. If the user uses the mouse to drag the bar, it can become * "detached" since the browser recalculates scroll bar position while the overflow container is * updated. */ updateItemHeight(data) { setDefaultItemHeight(data.value); }, }; return { state, events, getId: config.getId }; });