@ant-design/x
Version:
Craft AI-driven interfaces effortlessly
178 lines (167 loc) • 5.5 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import classNames from 'classnames';
import { useEvent } from 'rc-util';
import pickAttrs from "rc-util/es/pickAttrs";
import * as React from 'react';
import { useXProviderContext } from "../x-provider";
import Bubble, { BubbleContext } from "./Bubble";
import useListData from "./hooks/useListData";
import useStyle from "./style";
const BubbleListItem = ({
_key,
...restProps
}, ref) => /*#__PURE__*/React.createElement(Bubble, _extends({}, restProps, {
_key: _key,
ref: node => {
if (node) {
ref.current[_key] = node;
} else {
delete ref.current?.[_key];
}
}
}));
const MemoBubbleListItem = /*#__PURE__*/React.memo( /*#__PURE__*/React.forwardRef(BubbleListItem));
const TOLERANCE = 1;
const BubbleList = (props, ref) => {
const {
prefixCls: customizePrefixCls,
rootClassName,
className,
items,
autoScroll = true,
roles,
onScroll,
...restProps
} = props;
const domProps = pickAttrs(restProps, {
attr: true,
aria: true
});
// ============================= Refs =============================
const listRef = React.useRef(null);
const bubbleRefs = React.useRef({});
// ============================ Prefix ============================
const {
getPrefixCls
} = useXProviderContext();
const prefixCls = getPrefixCls('bubble', customizePrefixCls);
const listPrefixCls = `${prefixCls}-list`;
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
// ============================ Typing ============================
const [initialized, setInitialized] = React.useState(false);
React.useEffect(() => {
setInitialized(true);
return () => {
setInitialized(false);
};
}, []);
// ============================= Data =============================
const mergedData = useListData(items, roles);
// ============================ Scroll ============================
// Is current scrollTop at the end. User scroll will make this false.
const [scrollReachEnd, setScrollReachEnd] = React.useState(true);
const [updateCount, setUpdateCount] = React.useState(0);
const onInternalScroll = e => {
const target = e.target;
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
onScroll?.(e);
};
React.useEffect(() => {
if (autoScroll && listRef.current && scrollReachEnd) {
listRef.current.scrollTo({
top: listRef.current.scrollHeight
});
}
}, [updateCount]);
// Always scroll to bottom when data change
React.useEffect(() => {
if (autoScroll) {
// New date come, the origin last one is the second last one
const lastItemKey = mergedData[mergedData.length - 2]?.key;
const bubbleInst = bubbleRefs.current[lastItemKey];
// Auto scroll if last 2 item is visible
if (bubbleInst) {
const {
nativeElement
} = bubbleInst;
const {
top,
bottom
} = nativeElement.getBoundingClientRect();
const {
top: listTop,
bottom: listBottom
} = listRef.current.getBoundingClientRect();
const isVisible = top < listBottom && bottom > listTop;
if (isVisible) {
setUpdateCount(c => c + 1);
setScrollReachEnd(true);
}
}
}
}, [mergedData.length]);
// ========================== Outer Ref ===========================
React.useImperativeHandle(ref, () => ({
nativeElement: listRef.current,
scrollTo: ({
key,
offset,
behavior = 'smooth',
block
}) => {
if (typeof offset === 'number') {
// Offset scroll
listRef.current.scrollTo({
top: offset,
behavior
});
} else if (key !== undefined) {
// Key scroll
const bubbleInst = bubbleRefs.current[key];
if (bubbleInst) {
// Block current auto scrolling
const index = mergedData.findIndex(dataItem => dataItem.key === key);
setScrollReachEnd(index === mergedData.length - 1);
// Do native scroll
bubbleInst.nativeElement.scrollIntoView({
behavior,
block
});
}
}
}
}));
// =========================== Context ============================
// When bubble content update, we try to trigger `autoScroll` for sync
const onBubbleUpdate = useEvent(() => {
if (autoScroll) {
setUpdateCount(c => c + 1);
}
});
const context = React.useMemo(() => ({
onUpdate: onBubbleUpdate
}), []);
// ============================ Render ============================
return wrapCSSVar( /*#__PURE__*/React.createElement(BubbleContext.Provider, {
value: context
}, /*#__PURE__*/React.createElement("div", _extends({}, domProps, {
className: classNames(listPrefixCls, rootClassName, className, hashId, cssVarCls, {
[`${listPrefixCls}-reach-end`]: scrollReachEnd
}),
ref: listRef,
onScroll: onInternalScroll
}), mergedData.map(({
key,
...bubble
}) => /*#__PURE__*/React.createElement(MemoBubbleListItem, _extends({}, bubble, {
key: key,
_key: key,
ref: bubbleRefs,
typing: initialized ? bubble.typing : false
}))))));
};
const ForwardBubbleList = /*#__PURE__*/React.forwardRef(BubbleList);
if (process.env.NODE_ENV !== 'production') {
ForwardBubbleList.displayName = 'BubbleList';
}
export default ForwardBubbleList;