Skip to content
Merged
4 changes: 4 additions & 0 deletions src/GroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import clsx from 'clsx';
import type { Group } from './hooks/useGroupSegments';

// ============================== Types ===============================
export interface GroupHeaderProps<T, K extends React.Key = React.Key> {
group: Group<T, K>;
groupKey: K;
Expand All @@ -16,6 +17,7 @@ function GroupHeader<T, K extends React.Key = React.Key>(
props: GroupHeaderProps<T, K>,
ref: React.Ref<HTMLDivElement>,
) {
// ============================== Props ==============================
const {
group,
groupKey,
Expand All @@ -26,11 +28,13 @@ function GroupHeader<T, K extends React.Key = React.Key>(
style,
} = props;

// ============================= Classes =============================
const className = clsx(`${prefixCls}-group-header`, {
[`${prefixCls}-group-header-sticky`]: sticky,
[`${prefixCls}-group-header-fixed`]: fixed,
});

// ============================== Render ==============================
return (
<div ref={ref} className={className} style={style}>
{group.title(groupKey, groupItems)}
Expand Down
20 changes: 6 additions & 14 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import RawList from './RawList';
import VirtualList from './VirtualList';
import type { Group } from './hooks/useGroupSegments';

// ============================== Types ===============================
export type RowKey<T> = keyof T | ((item: T) => React.Key);

export type ScrollAlign = 'top' | 'bottom' | 'auto';
Expand Down Expand Up @@ -76,26 +77,17 @@ function Listy<T, K extends React.Key = React.Key>(
...restProps,
data,
prefixCls,
ref,
};

const listNode =
virtual === false ? (
<RawList
ref={ref}
{...sharedListProps}
/>
virtual ? (
<VirtualList {...sharedListProps} />
) : (
<VirtualList
ref={ref}
{...sharedListProps}
/>
<RawList {...sharedListProps} />
);

return (
<div className={prefixCls}>
{listNode}
</div>
);
return listNode;
}

// Const to support generic with forwardRef
Expand Down
58 changes: 21 additions & 37 deletions src/RawList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import * as React from 'react';
import ResizeObserver from '@rc-component/resize-observer';
import { useEvent } from '@rc-component/util';
import GroupHeader from '../GroupHeader';
import useGroupSegments from '../hooks/useGroupSegments';
import useRawListScroll from './useRawListScroll';
import type { ListComponentProps, ListyRef } from '../List';

// ============================== Types ===============================
export type RawListProps<T, K extends React.Key = React.Key> =
ListComponentProps<T, K>;

function RawList<T, K extends React.Key = React.Key>(
props: RawListProps<T, K>,
ref: React.Ref<ListyRef>,
) {
// ============================== Props ==============================
const {
data,
group,
Expand All @@ -24,12 +25,13 @@ function RawList<T, K extends React.Key = React.Key>(
sticky,
} = props;

const holderRef = useRawListScroll(ref);
// =============================== Refs ===============================
const holderRef = useRawListScroll(ref, prefixCls, !!(sticky && group));

// =============================== Data ===============================
const groupData = useGroupSegments<T, K>(data, group);
const [headerHeights, setHeaderHeights] = React.useState<
Map<K, number>
>(() => new Map());

// ============================== Utils ===============================
const getItemKey = useEvent((item: T): React.Key => {
if (typeof rowKey === 'function') {
return rowKey(item);
Expand All @@ -44,32 +46,20 @@ function RawList<T, K extends React.Key = React.Key>(
[],
);

const setGroupHeaderHeight = React.useCallback(
(groupKey: K, headerHeight: number) => {
setHeaderHeights((prev) => {
const next = new Map(prev);
next.set(groupKey, headerHeight);
return next;
});
},
[],
);

// ============================ Render Item ===========================
const renderItem = React.useCallback(
(item: T, index: number, groupKey?: K) => {
const key = getItemKey(item);
const scrollTargetProps = getScrollTargetProps(key);
const headerHeight =
sticky && groupKey !== undefined ? headerHeights.get(groupKey) : 0;

return (
<div
key={key}
className={`${prefixCls}-item`}
style={
headerHeight
sticky && groupKey !== undefined
? {
scrollMarginTop: headerHeight,
scrollMarginTop: `var(--${prefixCls}-item-scroll-margin-top, 0px)`,
}
: undefined
}
Expand All @@ -82,13 +72,13 @@ function RawList<T, K extends React.Key = React.Key>(
[
getItemKey,
getScrollTargetProps,
headerHeights,
itemRender,
prefixCls,
sticky,
],
);

// ============================= Content ==============================
const rawContent = group
? Array.from(groupData, ([groupKey, groupItems]) => {
const currentGroupItems = groupItems.map(({ item }) => item);
Expand All @@ -99,20 +89,13 @@ function RawList<T, K extends React.Key = React.Key>(
className={`${prefixCls}-group-section`}
{...getScrollTargetProps(groupKey)}
>
<ResizeObserver
disabled={!sticky}
onResize={({ offsetHeight }) => {
setGroupHeaderHeight(groupKey, offsetHeight);
}}
>
<GroupHeader
group={group}
groupKey={groupKey}
groupItems={currentGroupItems}
prefixCls={prefixCls}
sticky={sticky}
/>
</ResizeObserver>
<GroupHeader
group={group}
groupKey={groupKey}
groupItems={currentGroupItems}
prefixCls={prefixCls}
sticky={sticky}
/>
{groupItems.map(({ item, index }) => {
return renderItem(item, index, groupKey);
})}
Expand All @@ -123,18 +106,19 @@ function RawList<T, K extends React.Key = React.Key>(
return renderItem(item, index);
});

// ============================== Render ==============================
return (
<div
ref={holderRef}
className={`${prefixCls}-holder`}
className={prefixCls}
style={{
maxHeight: height,
overflowY: height === undefined ? undefined : 'auto',
overflowAnchor: 'none',
}}
onScroll={onScroll}
>
<div className={`${prefixCls}-holder-inner`}>{rawContent}</div>
{rawContent}
</div>
);
}
Expand Down
60 changes: 56 additions & 4 deletions src/RawList/useRawListScroll.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
import * as React from 'react';
import type { ListyRef, PositionScrollToConfig } from '../List';
import type { ListyRef, PositionScrollToConfig, ScrollAlign } from '../List';

export default function useRawListScroll(ref: React.Ref<ListyRef>) {
export default function useRawListScroll(
ref: React.Ref<ListyRef>,
prefixCls: string,
stickyGroup: boolean,
) {
// =============================== Refs ===============================
const holderRef = React.useRef<HTMLDivElement>(null);

// ============================== Utils ===============================
const getStickyHeaderHeight = React.useCallback(
(targetElement: HTMLElement) => {
if (!stickyGroup) {
return 0;
}

const groupSection = targetElement.closest<HTMLElement>(
`.${CSS.escape(`${prefixCls}-group-section`)}`,
);
const groupHeader = groupSection?.querySelector<HTMLElement>(
`.${CSS.escape(`${prefixCls}-group-header`)}`,
);

if (!groupHeader) {
return 0;
}

const rect = groupHeader.getBoundingClientRect();
const height =
rect.height || rect.bottom - rect.top || groupHeader.offsetHeight;

return Number.isFinite(height) ? height : 0;
},
[prefixCls, stickyGroup],
);

const setTargetScrollMargin = React.useCallback(
(targetElement: HTMLElement, align: ScrollAlign) => {
const marginTop =
align === 'top' ? getStickyHeaderHeight(targetElement) : 0;

targetElement.style.setProperty(
`--${prefixCls}-item-scroll-margin-top`,
`${marginTop}px`,
);
},
[getStickyHeaderHeight, prefixCls],
);

// ============================== Scroll ==============================
const scrollTo: ListyRef['scrollTo'] = React.useCallback(
(config) => {
const holder = holderRef.current;
Expand All @@ -17,13 +63,17 @@ export default function useRawListScroll(ref: React.Ref<ListyRef>) {
}

if ('key' in config || 'groupKey' in config) {
const { align = 'top' } = config;
const targetKey = 'groupKey' in config ? config.groupKey : config.key;
const targetElement = holder.querySelector<HTMLElement>(
`[data-key="${CSS.escape(String(targetKey))}"]`,
);

if (targetElement) {
const { align = 'top' } = config;
if ('key' in config) {
setTargetScrollMargin(targetElement, align);
}

targetElement.scrollIntoView({
block:
align === 'bottom'
Expand All @@ -45,9 +95,10 @@ export default function useRawListScroll(ref: React.Ref<ListyRef>) {
holder.scrollTop = top;
}
},
[],
[setTargetScrollMargin],
);

// ============================ Imperative ============================
React.useImperativeHandle(
ref,
() => ({
Expand All @@ -56,5 +107,6 @@ export default function useRawListScroll(ref: React.Ref<ListyRef>) {
[scrollTo],
);

// ============================== Return ==============================
return holderRef;
}
Loading
Loading