UNPKG

react-widgets

Version:

An à la carte set of polished, extensible, and accessible inputs built for React

169 lines (158 loc) 5.98 kB
const _excluded = ["multiple", "data", "value", "onChange", "accessors", "className", "messages", "disabled", "renderItem", "renderGroup", "searchTerm", "groupBy", "elementRef", "optionComponent", "renderList"]; function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /* eslint-disable @typescript-eslint/no-empty-function */ import cn from 'classnames'; import PropTypes from 'prop-types'; import React, { useCallback, useImperativeHandle, useMemo } from 'react'; import ListOption from './ListOption'; import ListOptionGroup from './ListOptionGroup'; import { useMessagesWithDefaults } from './messages'; // import { WidgetHTMLProps } from './shared' import * as CustomPropTypes from './PropTypes'; import { groupBySortedKeys, makeArray, toItemArray } from './_'; import { useInstanceId } from './WidgetHelpers'; import useMutationObserver from '@restart/hooks/useMutationObserver'; import useCallbackRef from '@restart/hooks/useCallbackRef'; import useMergedRefs from '@restart/hooks/useMergedRefs'; const whitelist = ['style', 'className', 'role', 'id', 'autocomplete', 'size', 'tabIndex', 'maxLength', 'name']; const whitelistRegex = [/^aria-/, /^data-/, /^on[A-Z]\w+/]; function pickElementProps(props) { const result = {}; Object.keys(props).forEach(key => { if (whitelist.indexOf(key) !== -1 || whitelistRegex.some(r => !!key.match(r))) result[key] = props[key]; }); return result; } const propTypes = { data: PropTypes.array, dataKey: CustomPropTypes.accessor, textField: CustomPropTypes.accessor, onSelect: PropTypes.func, onMove: PropTypes.func, onHoverOption: PropTypes.func, optionComponent: PropTypes.elementType, renderItem: PropTypes.func, renderGroup: PropTypes.func, focusedItem: PropTypes.any, selectedItem: PropTypes.any, searchTerm: PropTypes.string, disabled: CustomPropTypes.disabled.acceptsArray, messages: PropTypes.shape({ emptyList: PropTypes.func.isRequired }) }; export const useScrollFocusedIntoView = (element, observeChanges = false) => { const scrollIntoView = useCallback(() => { if (!element) return; let selectedItem = element.querySelector('[data-rw-focused]'); if (selectedItem && selectedItem.scrollIntoView) { selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } }, [element]); useMutationObserver(observeChanges ? element : null, { subtree: true, attributes: true, attributeFilter: ['data-rw-focused'] }, scrollIntoView); return scrollIntoView; }; export function useHandleSelect(multiple, dataItems, onChange) { return (dataItem, event) => { if (multiple === false) { onChange(dataItem, { dataItem, lastValue: dataItems[0], originalEvent: event }); return; } const checked = dataItems.includes(dataItem); onChange(checked ? dataItems.filter(d => d !== dataItem) : [...dataItems, dataItem], { dataItem, lastValue: dataItems, action: checked ? 'remove' : 'insert', originalEvent: event }); }; } const List = /*#__PURE__*/React.forwardRef(function List(_ref, outerRef) { var _elementProps$role; let { multiple = false, data = [], value, onChange, accessors, className, messages, disabled, renderItem, renderGroup, searchTerm, groupBy, elementRef, optionComponent: Option = ListOption, renderList } = _ref, props = _objectWithoutPropertiesLoose(_ref, _excluded); const id = useInstanceId(); const dataItems = makeArray(value, multiple); const groupedData = useMemo(() => groupBy ? groupBySortedKeys(groupBy, data) : undefined, [data, groupBy]); const [element, ref] = useCallbackRef(); const disabledItems = toItemArray(disabled); const { emptyList } = useMessagesWithDefaults(messages); const divRef = useMergedRefs(ref, elementRef); const handleSelect = useHandleSelect(multiple, dataItems, onChange); const scrollIntoView = useScrollFocusedIntoView(element, true); let elementProps = pickElementProps(props); useImperativeHandle(outerRef, () => ({ scrollIntoView }), [scrollIntoView]); function renderOption(item, idx) { const textValue = accessors.text(item); const itemIsDisabled = disabledItems.includes(item); const itemIsSelected = dataItems.includes(item); return /*#__PURE__*/React.createElement(Option, { dataItem: item, key: `item_${idx}`, searchTerm: searchTerm, onSelect: handleSelect, disabled: itemIsDisabled, selected: itemIsSelected }, renderItem ? renderItem({ item, searchTerm, index: idx, text: textValue, // TODO: probably remove value: accessors.value(item), disabled: itemIsDisabled }) : textValue); } const items = groupedData ? groupedData.map(([group, items], idx) => /*#__PURE__*/React.createElement("div", { role: "group", key: `group_${idx}` }, /*#__PURE__*/React.createElement(ListOptionGroup, null, renderGroup ? renderGroup({ group }) : group), items.map(renderOption))) : data.map(renderOption); const rootProps = Object.assign({ id, tabIndex: 0, ref: divRef }, elementProps, { 'aria-multiselectable': !!multiple, className: cn(className, 'rw-list'), role: (_elementProps$role = elementProps.role) != null ? _elementProps$role : 'listbox', children: React.Children.count(items) ? items : /*#__PURE__*/React.createElement("div", { className: "rw-list-empty" }, emptyList()) }); return renderList ? renderList(rootProps) : /*#__PURE__*/React.createElement("div", rootProps); }); List.displayName = 'List'; List.propTypes = propTypes; export default List;