284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
import * as React from 'react';
|
|
import { useState, useMemo, useCallback } from 'react';
|
|
import { clsx } from 'clsx';
|
|
import ResizeObserver from '@rc-component/resize-observer';
|
|
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
|
import Item from "./Item";
|
|
import useEffectState, { useBatcher } from "./hooks/useEffectState";
|
|
import RawItem from "./RawItem";
|
|
import { OverflowContext } from "./context";
|
|
const RESPONSIVE = 'responsive';
|
|
const INVALIDATE = 'invalidate';
|
|
export { OverflowContext } from "./context";
|
|
function defaultRenderRest(omittedItems) {
|
|
return `+ ${omittedItems.length} ...`;
|
|
}
|
|
function Overflow(props, ref) {
|
|
const {
|
|
prefixCls = 'rc-overflow',
|
|
data = [],
|
|
renderItem,
|
|
renderRawItem,
|
|
itemKey,
|
|
itemWidth = 10,
|
|
ssr,
|
|
style,
|
|
className,
|
|
maxCount,
|
|
renderRest,
|
|
renderRawRest,
|
|
prefix,
|
|
suffix,
|
|
component: Component = 'div',
|
|
itemComponent,
|
|
onVisibleChange,
|
|
...restProps
|
|
} = props;
|
|
const fullySSR = ssr === 'full';
|
|
const notifyEffectUpdate = useBatcher();
|
|
const [containerWidth, setContainerWidth] = useEffectState(notifyEffectUpdate, null);
|
|
const mergedContainerWidth = containerWidth || 0;
|
|
const [itemWidths, setItemWidths] = useEffectState(notifyEffectUpdate, new Map());
|
|
const [prevRestWidth, setPrevRestWidth] = useEffectState(notifyEffectUpdate, 0);
|
|
const [restWidth, setRestWidth] = useEffectState(notifyEffectUpdate, 0);
|
|
const [prefixWidth, setPrefixWidth] = useEffectState(notifyEffectUpdate, 0);
|
|
const [suffixWidth, setSuffixWidth] = useEffectState(notifyEffectUpdate, 0);
|
|
const [suffixFixedStart, setSuffixFixedStart] = useState(null);
|
|
const [displayCount, setDisplayCount] = useState(null);
|
|
const mergedDisplayCount = React.useMemo(() => {
|
|
if (displayCount === null && fullySSR) {
|
|
return Number.MAX_SAFE_INTEGER;
|
|
}
|
|
return displayCount || 0;
|
|
}, [displayCount, containerWidth]);
|
|
const [restReady, setRestReady] = useState(false);
|
|
const itemPrefixCls = `${prefixCls}-item`;
|
|
|
|
// Always use the max width to avoid blink
|
|
const mergedRestWidth = Math.max(prevRestWidth, restWidth);
|
|
|
|
// ================================= Data =================================
|
|
const isResponsive = maxCount === RESPONSIVE;
|
|
const shouldResponsive = data.length && isResponsive;
|
|
const invalidate = maxCount === INVALIDATE;
|
|
|
|
/**
|
|
* When is `responsive`, we will always render rest node to get the real width of it for calculation
|
|
*/
|
|
const showRest = shouldResponsive || typeof maxCount === 'number' && data.length > maxCount;
|
|
const mergedData = useMemo(() => {
|
|
let items = data;
|
|
if (shouldResponsive) {
|
|
if (containerWidth === null && fullySSR) {
|
|
items = data;
|
|
} else {
|
|
items = data.slice(0, Math.min(data.length, mergedContainerWidth / itemWidth));
|
|
}
|
|
} else if (typeof maxCount === 'number') {
|
|
items = data.slice(0, maxCount);
|
|
}
|
|
return items;
|
|
}, [data, itemWidth, containerWidth, maxCount, shouldResponsive]);
|
|
const omittedItems = useMemo(() => {
|
|
if (shouldResponsive) {
|
|
return data.slice(mergedDisplayCount + 1);
|
|
}
|
|
return data.slice(mergedData.length);
|
|
}, [data, mergedData, shouldResponsive, mergedDisplayCount]);
|
|
|
|
// ================================= Item =================================
|
|
const getKey = useCallback((item, index) => {
|
|
if (typeof itemKey === 'function') {
|
|
return itemKey(item);
|
|
}
|
|
return (itemKey && item?.[itemKey]) ?? index;
|
|
}, [itemKey]);
|
|
const mergedRenderItem = useCallback(renderItem || (item => item), [renderItem]);
|
|
function updateDisplayCount(count, suffixFixedStartVal, notReady) {
|
|
// React 18 will sync render even when the value is same in some case.
|
|
// We take `mergedData` as deps which may cause dead loop if it's dynamic generate.
|
|
// ref: https://github.com/ant-design/ant-design/issues/36559
|
|
if (displayCount === count && (suffixFixedStartVal === undefined || suffixFixedStartVal === suffixFixedStart)) {
|
|
return;
|
|
}
|
|
setDisplayCount(count);
|
|
if (!notReady) {
|
|
setRestReady(count < data.length - 1);
|
|
onVisibleChange?.(count);
|
|
}
|
|
if (suffixFixedStartVal !== undefined) {
|
|
setSuffixFixedStart(suffixFixedStartVal);
|
|
}
|
|
}
|
|
|
|
// ================================= Size =================================
|
|
function onOverflowResize(_, element) {
|
|
setContainerWidth(element.clientWidth);
|
|
}
|
|
function registerSize(key, width) {
|
|
setItemWidths(origin => {
|
|
const clone = new Map(origin);
|
|
if (width === null) {
|
|
clone.delete(key);
|
|
} else {
|
|
clone.set(key, width);
|
|
}
|
|
return clone;
|
|
});
|
|
}
|
|
function registerOverflowSize(_, width) {
|
|
setRestWidth(width);
|
|
setPrevRestWidth(restWidth);
|
|
}
|
|
function registerPrefixSize(_, width) {
|
|
setPrefixWidth(width);
|
|
}
|
|
function registerSuffixSize(_, width) {
|
|
setSuffixWidth(width);
|
|
}
|
|
|
|
// ================================ Effect ================================
|
|
function getItemWidth(index) {
|
|
return itemWidths.get(getKey(mergedData[index], index));
|
|
}
|
|
useLayoutEffect(() => {
|
|
if (mergedContainerWidth && typeof mergedRestWidth === 'number' && mergedData) {
|
|
let totalWidth = prefixWidth + suffixWidth;
|
|
const len = mergedData.length;
|
|
const lastIndex = len - 1;
|
|
|
|
// When data count change to 0, reset this since not loop will reach
|
|
if (!len) {
|
|
updateDisplayCount(0, null);
|
|
return;
|
|
}
|
|
for (let i = 0; i < len; i += 1) {
|
|
let currentItemWidth = getItemWidth(i);
|
|
|
|
// Fully will always render
|
|
if (fullySSR) {
|
|
currentItemWidth = currentItemWidth || 0;
|
|
}
|
|
|
|
// Break since data not ready
|
|
if (currentItemWidth === undefined) {
|
|
updateDisplayCount(i - 1, undefined, true);
|
|
break;
|
|
}
|
|
|
|
// Find best match
|
|
totalWidth += currentItemWidth;
|
|
if (
|
|
// Only one means `totalWidth` is the final width
|
|
lastIndex === 0 && totalWidth <= mergedContainerWidth ||
|
|
// Last two width will be the final width
|
|
i === lastIndex - 1 && totalWidth + getItemWidth(lastIndex) <= mergedContainerWidth) {
|
|
// Additional check if match the end
|
|
updateDisplayCount(lastIndex, null);
|
|
break;
|
|
} else if (totalWidth + mergedRestWidth > mergedContainerWidth) {
|
|
// Can not hold all the content to show rest
|
|
updateDisplayCount(i - 1, totalWidth - currentItemWidth - suffixWidth + restWidth);
|
|
break;
|
|
}
|
|
}
|
|
if (suffix && getItemWidth(0) + suffixWidth > mergedContainerWidth) {
|
|
setSuffixFixedStart(null);
|
|
}
|
|
}
|
|
}, [mergedContainerWidth, itemWidths, restWidth, prefixWidth, suffixWidth, getKey, mergedData]);
|
|
|
|
// ================================ Render ================================
|
|
const displayRest = restReady && !!omittedItems.length;
|
|
let suffixStyle = {};
|
|
if (suffixFixedStart !== null && shouldResponsive) {
|
|
suffixStyle = {
|
|
position: 'absolute',
|
|
top: 0,
|
|
insetInlineStart: suffixFixedStart
|
|
};
|
|
}
|
|
const itemSharedProps = {
|
|
prefixCls: itemPrefixCls,
|
|
responsive: shouldResponsive,
|
|
component: itemComponent,
|
|
invalidate
|
|
};
|
|
|
|
// >>>>> Choice render fun by `renderRawItem`
|
|
const internalRenderItemNode = renderRawItem ? (item, index) => {
|
|
const key = getKey(item, index);
|
|
return /*#__PURE__*/React.createElement(OverflowContext.Provider, {
|
|
key: key,
|
|
value: {
|
|
...itemSharedProps,
|
|
order: index,
|
|
item,
|
|
itemKey: key,
|
|
registerSize,
|
|
display: index <= mergedDisplayCount
|
|
}
|
|
}, renderRawItem(item, index));
|
|
} : (item, index) => {
|
|
const key = getKey(item, index);
|
|
return /*#__PURE__*/React.createElement(Item, _extends({}, itemSharedProps, {
|
|
order: index,
|
|
key: key,
|
|
item: item,
|
|
renderItem: mergedRenderItem,
|
|
itemKey: key,
|
|
registerSize: registerSize,
|
|
display: index <= mergedDisplayCount
|
|
}));
|
|
};
|
|
|
|
// >>>>> Rest node
|
|
const restContextProps = {
|
|
order: displayRest ? mergedDisplayCount : Number.MAX_SAFE_INTEGER,
|
|
className: `${itemPrefixCls}-rest`,
|
|
registerSize: registerOverflowSize,
|
|
display: displayRest
|
|
};
|
|
const mergedRenderRest = renderRest || defaultRenderRest;
|
|
const restNode = renderRawRest ? /*#__PURE__*/React.createElement(OverflowContext.Provider, {
|
|
value: {
|
|
...itemSharedProps,
|
|
...restContextProps
|
|
}
|
|
}, renderRawRest(omittedItems)) : /*#__PURE__*/React.createElement(Item, _extends({}, itemSharedProps, restContextProps), typeof mergedRenderRest === 'function' ? mergedRenderRest(omittedItems) : mergedRenderRest);
|
|
const overflowNode = /*#__PURE__*/React.createElement(Component, _extends({
|
|
className: clsx(!invalidate && prefixCls, className),
|
|
style: style,
|
|
ref: ref
|
|
}, restProps), prefix && /*#__PURE__*/React.createElement(Item, _extends({}, itemSharedProps, {
|
|
responsive: isResponsive,
|
|
responsiveDisabled: !shouldResponsive,
|
|
order: -1,
|
|
className: `${itemPrefixCls}-prefix`,
|
|
registerSize: registerPrefixSize,
|
|
display: true
|
|
}), prefix), mergedData.map(internalRenderItemNode), showRest ? restNode : null, suffix && /*#__PURE__*/React.createElement(Item, _extends({}, itemSharedProps, {
|
|
responsive: isResponsive,
|
|
responsiveDisabled: !shouldResponsive,
|
|
order: mergedDisplayCount,
|
|
className: `${itemPrefixCls}-suffix`,
|
|
registerSize: registerSuffixSize,
|
|
display: true,
|
|
style: suffixStyle
|
|
}), suffix));
|
|
return isResponsive ? /*#__PURE__*/React.createElement(ResizeObserver, {
|
|
onResize: onOverflowResize,
|
|
disabled: !shouldResponsive
|
|
}, overflowNode) : overflowNode;
|
|
}
|
|
const ForwardOverflow = /*#__PURE__*/React.forwardRef(Overflow);
|
|
ForwardOverflow.Item = RawItem;
|
|
ForwardOverflow.RESPONSIVE = RESPONSIVE;
|
|
ForwardOverflow.INVALIDATE = INVALIDATE;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
ForwardOverflow.displayName = 'Overflow';
|
|
}
|
|
|
|
// Convert to generic type
|
|
export default ForwardOverflow; |