mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 22:09:08 +08:00
feat(component): virtual scroll emoji groups in emoji picker (#13671)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Revamped Emoji Picker: grouped browsing with sticky group headers, footer navigation, and a new EmojiButton for quicker selection. - Recent emojis with persisted history and single-tap add. - Programmatic group navigation and callbacks for sticky-group changes. - Style - Updated scroll area paddings for emoji and icon pickers. - Enhanced group header background for better contrast. - Refactor - Simplified emoji picker internals for leaner, more responsive rendering. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,10 +2,12 @@ import clsx from 'clsx';
|
||||
import { debounce } from 'lodash-es';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -61,29 +63,39 @@ export interface MasonryProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
columns?: number;
|
||||
resizeDebounce?: number;
|
||||
preloadHeight?: number;
|
||||
|
||||
onStickyGroupChange?: (groupId?: string) => void;
|
||||
}
|
||||
|
||||
export const Masonry = ({
|
||||
items,
|
||||
gapX = 12,
|
||||
gapY = 12,
|
||||
itemWidth = 'stretch',
|
||||
itemWidthMin = 100,
|
||||
paddingX = 0,
|
||||
paddingY = 0,
|
||||
className,
|
||||
virtualScroll = false,
|
||||
locateMode = 'leftTop',
|
||||
groupsGap = 0,
|
||||
groupHeaderGapWithItems = 0,
|
||||
stickyGroupHeader = true,
|
||||
collapsedGroups,
|
||||
columns,
|
||||
preloadHeight = 50,
|
||||
resizeDebounce = 20,
|
||||
onGroupCollapse,
|
||||
...props
|
||||
}: MasonryProps) => {
|
||||
export type MasonryRef = {
|
||||
scrollToGroup: (groupId: string) => void;
|
||||
};
|
||||
|
||||
export const Masonry = forwardRef<MasonryRef, MasonryProps>(function Masonry(
|
||||
{
|
||||
items,
|
||||
gapX = 12,
|
||||
gapY = 12,
|
||||
itemWidth = 'stretch',
|
||||
itemWidthMin = 100,
|
||||
paddingX = 0,
|
||||
paddingY = 0,
|
||||
className,
|
||||
virtualScroll = false,
|
||||
locateMode = 'leftTop',
|
||||
groupsGap = 0,
|
||||
groupHeaderGapWithItems = 0,
|
||||
stickyGroupHeader = true,
|
||||
collapsedGroups,
|
||||
columns,
|
||||
preloadHeight = 50,
|
||||
resizeDebounce = 20,
|
||||
onGroupCollapse,
|
||||
onStickyGroupChange,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [layoutMap, setLayoutMap] = useState<
|
||||
@@ -212,7 +224,9 @@ export const Masonry = ({
|
||||
const scrollY = (e.target as HTMLElement).scrollTop;
|
||||
updateActiveMap(layoutMap, scrollY);
|
||||
if (stickyGroupHeader) {
|
||||
setStickyGroupId(calcSticky({ scrollY, layoutMap }));
|
||||
const stickyGroupId = calcSticky({ scrollY, layoutMap });
|
||||
setStickyGroupId(stickyGroupId);
|
||||
onStickyGroupChange?.(stickyGroupId);
|
||||
}
|
||||
}, 50);
|
||||
rootEl.addEventListener('scroll', handler);
|
||||
@@ -221,7 +235,29 @@ export const Masonry = ({
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [layoutMap, stickyGroupHeader, updateActiveMap, virtualScroll]);
|
||||
}, [
|
||||
layoutMap,
|
||||
onStickyGroupChange,
|
||||
stickyGroupHeader,
|
||||
updateActiveMap,
|
||||
virtualScroll,
|
||||
]);
|
||||
|
||||
const scrollToGroup = useCallback(
|
||||
(groupId: string) => {
|
||||
const group = layoutMap.get(groupId);
|
||||
if (!group) return;
|
||||
rootRef.current?.scrollTo({
|
||||
top: group.y,
|
||||
behavior: 'instant',
|
||||
});
|
||||
},
|
||||
[layoutMap]
|
||||
);
|
||||
|
||||
useImperativeHandle<MasonryRef, MasonryRef>(ref, () => {
|
||||
return { scrollToGroup };
|
||||
});
|
||||
|
||||
return (
|
||||
<Scrollable.Root>
|
||||
@@ -312,7 +348,7 @@ export const Masonry = ({
|
||||
<Scrollable.Scrollbar className={styles.scrollbar} />
|
||||
</Scrollable.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
type MasonryItemProps = MasonryItem &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'id' | 'height'> & {
|
||||
|
||||
@@ -112,12 +112,18 @@ export const calcLayout = (
|
||||
const ratioMode = 'ratio' in item;
|
||||
const height = ratioMode ? item.ratio * width : item.height;
|
||||
|
||||
const aroundGapXValue =
|
||||
columns > 1
|
||||
? (totalWidth - paddingX * 2 - width * columns) / (columns - 1)
|
||||
: 0;
|
||||
const gapXValue = Math.max(gapX, aroundGapXValue);
|
||||
|
||||
if (ratioMode) {
|
||||
const minRatio = Math.min(...ratioStack);
|
||||
const minRatioIndex = ratioStack.indexOf(minRatio);
|
||||
const minHeight = heightStack[minRatioIndex];
|
||||
const hasGap = heightStack[minRatioIndex] ? gapY : 0;
|
||||
const x = minRatioIndex * (width + gapX) + paddingX;
|
||||
const x = minRatioIndex * (width + gapXValue) + paddingX;
|
||||
const y = finalHeight + minHeight + hasGap;
|
||||
|
||||
ratioStack[minRatioIndex] += item.ratio * 10000;
|
||||
@@ -133,7 +139,7 @@ export const calcLayout = (
|
||||
const minHeight = Math.min(...heightStack);
|
||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||
const hasGap = heightStack[minHeightIndex] ? gapY : 0;
|
||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||
const x = minHeightIndex * (width + gapXValue) + paddingX;
|
||||
const y = finalHeight + minHeight + hasGap;
|
||||
|
||||
const ratio = height / width;
|
||||
@@ -193,7 +199,7 @@ export const calcSticky = (options: {
|
||||
|
||||
const stickyGroupEntry = groupEntries.find(([_, xywh], index) => {
|
||||
const next = groupEntries[index + 1];
|
||||
return xywh.y < scrollY && (!next || next[1].y > scrollY);
|
||||
return xywh.y <= scrollY && (!next || next[1].y > scrollY);
|
||||
});
|
||||
|
||||
return stickyGroupEntry
|
||||
|
||||
Reference in New Issue
Block a user