rsuite
Version:
A suite of react components
219 lines (214 loc) • 8.16 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useRef, useState, useEffect } from 'react';
import isUndefined from 'lodash/isUndefined';
import isString from 'lodash/isString';
import isNumber from 'lodash/isNumber';
import findIndex from 'lodash/findIndex';
import pickBy from 'lodash/pickBy';
import getPosition from 'dom-lib/getPosition';
import scrollTop from 'dom-lib/scrollTop';
import getHeight from 'dom-lib/getHeight';
import get from 'lodash/get';
import classNames from 'classnames';
import ListItemGroup from "./ListItemGroup.js";
import useCombobox from "./hooks/useCombobox.js";
import Highlight from "../../Highlight/index.js";
import { List, AutoSizer, VariableSizeList } from "../Windowing/index.js";
import { RSUITE_PICKER_GROUP_KEY } from "../symbols.js";
import { useStyles, useMount, useEventCallback } from "../hooks/index.js";
import { shallowEqual, mergeRefs, mergeStyles, getCssValue } from "../utils/index.js";
import { KEY_GROUP_TITLE } from "../utils/getDataGroupBy.js";
/**
* Props for the Listbox component.
*/
/**
* Props for the Listbox component.
* @template Multiple - Whether multiple selection is enabled.
*/
const Listbox = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
data = [],
groupBy,
maxHeight = 320,
activeItemValues = [],
disabledItemValues = [],
classPrefix = 'listbox',
valueKey = 'value',
labelKey = 'label',
virtualized,
listProps,
listRef: virtualizedListRef,
className,
style,
focusItemValue,
listItemClassPrefix,
listItemAs: ListItem,
listItemProps,
rowHeight = 36,
rowGroupHeight = 48,
query,
renderOptionGroup,
renderOption,
onGroupTitleClick,
onSelect,
...rest
} = props;
const {
prefix,
merge,
rootPrefix
} = useStyles(classPrefix);
const groupable = typeof groupBy !== 'undefined';
const classes = merge(className, rootPrefix('picker-listbox'), prefix('items', {
grouped: groupable
}));
const {
id,
labelId,
popupType,
multiple
} = useCombobox();
const menuBodyContainerRef = useRef(null);
const listRef = useRef(null);
const [foldedGroupKeys, setFoldedGroupKeys] = useState([]);
const handleGroupTitleClick = useEventCallback((key, event) => {
const nextGroupKeys = foldedGroupKeys.filter(item => item !== key);
if (nextGroupKeys.length === foldedGroupKeys.length) {
nextGroupKeys.push(key);
}
setFoldedGroupKeys(nextGroupKeys);
onGroupTitleClick === null || onGroupTitleClick === void 0 || onGroupTitleClick(event);
});
const handleSelect = useEventCallback((item, value, event, checked) => {
onSelect === null || onSelect === void 0 || onSelect(value, item, event, checked);
});
const getRowHeight = (list, index) => {
const item = list[index];
if (groupable && item[RSUITE_PICKER_GROUP_KEY] && index !== 0) {
return rowGroupHeight;
}
return rowHeight;
};
useEffect(() => {
const container = menuBodyContainerRef.current;
if (!container) {
return;
}
let activeItem = container.querySelector(`.${prefix('item-focus')}`);
if (!activeItem) {
activeItem = container.querySelector(`.${prefix('item-active')}`);
}
if (!activeItem) {
return;
}
const position = getPosition(activeItem, container);
const sTop = scrollTop(container);
const sHeight = getHeight(container);
if (sTop > position.top) {
scrollTop(container, Math.max(0, position.top - 20));
} else if (position.top > sTop + sHeight) {
scrollTop(container, Math.max(0, position.top - sHeight + 32));
}
}, [focusItemValue, menuBodyContainerRef, prefix]);
const filteredItems = groupable ? data.filter(item => {
var _item$parent;
// Display group title items
if (item[RSUITE_PICKER_GROUP_KEY]) return true;
// Display items under the unfolded group
const groupValue = get(item, groupBy, '') || (// FIXME-Doma
// Usage of `item.parent` is strongly discouraged
// It's only here for legacy support
// Remove once `item.parent` is completely removed across related components
(_item$parent = item.parent) === null || _item$parent === void 0 ? void 0 : _item$parent[KEY_GROUP_TITLE]);
return !foldedGroupKeys.includes(groupValue);
}) : data;
const rowCount = filteredItems.length;
const renderItem = ({
index = 0,
style,
data,
item: itemData
}) => {
const item = itemData || data[index];
const value = item[valueKey];
const itemLabel = item[labelKey];
const label = query ? /*#__PURE__*/React.createElement(Highlight, {
query: query,
as: "span"
}, itemLabel) : itemLabel;
if (isUndefined(label) && !item[RSUITE_PICKER_GROUP_KEY]) {
throw Error(`labelKey "${labelKey}" is not defined in "data" : ${index}`);
}
// Use `value` in keys when If `value` is string or number
const itemKey = isString(value) || isNumber(value) ? value : index;
// Render <ListboxGroup> component when `groupBy` is defined
if (groupable && item[RSUITE_PICKER_GROUP_KEY]) {
const groupValue = item[KEY_GROUP_TITLE];
return /*#__PURE__*/React.createElement(ListItemGroup, {
style: style,
classPrefix: 'picker-menu-group',
className: classNames({
folded: foldedGroupKeys.some(key => key === groupValue)
}),
key: `group-${groupValue}`,
onClick: handleGroupTitleClick.bind(null, groupValue)
}, renderOptionGroup ? renderOptionGroup(groupValue, item) : groupValue);
} else if (isUndefined(value) && !isUndefined(item[RSUITE_PICKER_GROUP_KEY])) {
throw Error(`valueKey "${valueKey}" is not defined in "data" : ${index} `);
}
const disabled = disabledItemValues === null || disabledItemValues === void 0 ? void 0 : disabledItemValues.some(disabledValue => shallowEqual(disabledValue, value));
const active = activeItemValues === null || activeItemValues === void 0 ? void 0 : activeItemValues.some(v => shallowEqual(v, value));
const focus = !isUndefined(focusItemValue) && shallowEqual(focusItemValue, value);
return /*#__PURE__*/React.createElement(ListItem, _extends({
"aria-posinset": index + 1,
"aria-setsize": rowCount,
style: style,
key: itemKey,
disabled: disabled,
active: active,
focus: focus,
value: value,
classPrefix: listItemClassPrefix,
onSelect: handleSelect.bind(null, item)
}, pickBy(listItemProps, v => v !== undefined)), renderOption ? renderOption(label, item) : label);
};
useMount(() => {
var _listRef$current, _listRef$current$scro;
const itemIndex = findIndex(filteredItems, item => item[valueKey] === (activeItemValues === null || activeItemValues === void 0 ? void 0 : activeItemValues[0]));
(_listRef$current = listRef.current) === null || _listRef$current === void 0 || (_listRef$current$scro = _listRef$current.scrollToItem) === null || _listRef$current$scro === void 0 || _listRef$current$scro.call(_listRef$current, itemIndex);
});
const styles = mergeStyles(style, {
'--rs-picker-listbox-max-height': getCssValue(maxHeight)
});
return /*#__PURE__*/React.createElement("div", _extends({
role: "listbox",
id: `${id}-${popupType}`,
"aria-labelledby": labelId,
"aria-multiselectable": multiple,
className: classes,
ref: mergeRefs(menuBodyContainerRef, ref),
style: styles
}, rest), virtualized ? /*#__PURE__*/React.createElement(AutoSizer, {
defaultHeight: maxHeight,
style: {
width: 'auto',
height: 'auto'
}
}, ({
height
}) => /*#__PURE__*/React.createElement(List, _extends({
as: VariableSizeList,
ref: mergeRefs(listRef, virtualizedListRef),
height: height || maxHeight,
itemCount: rowCount,
itemData: filteredItems,
itemSize: getRowHeight.bind(this, filteredItems),
className: rootPrefix('virt-list')
}, listProps), renderItem)) : filteredItems.map((item, index) => renderItem({
index,
item
})));
});
Listbox.displayName = 'Listbox';
export default Listbox;