Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ bun.lockb

# dumi
.dumi/tmp
.dumi/tmp-production
.dumi/tmp-production

.claude
8 changes: 8 additions & 0 deletions docs/demos/semantic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Semantic DOM
nav:
title: Demo
path: /demo
---

<code src="../examples/semantic.tsx"></code>
105 changes: 105 additions & 0 deletions docs/examples/semantic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useRef, useState } from 'react';
import Listy, {
type ListyRef,
type ListyClassNames,
type ListyStyles,
} from '@rc-component/listy';
import '../../assets/index.less';

const GROUPS = [
{ id: 'frontend', label: 'Frontend' },
{ id: 'backend', label: 'Backend' },
{ id: 'infra', label: 'Infrastructure' },
{ id: 'mobile', label: 'Mobile' },
];

interface Task {
id: string;
name: string;
groupId: string;
}

const items: Task[] = GROUPS.flatMap((group) =>
Array.from({ length: 8 }, (_, index) => ({
id: `${group.id}-${index}`,
name: `${group.label} task #${index + 1}`,
groupId: group.id,
})),
);

// The `classNames` and `styles` props share the same semantic slots:
// `root`, `item`, and `groupHeader` (including the sticky clone).
const classNames: ListyClassNames = {
root: 'listy-semantic-root',
groupHeader: 'listy-semantic-header',
item: 'listy-semantic-item',
};

// Inline `styles` stack on top of the classNames above.
const styles: ListyStyles = {
root: { borderRadius: 10 },
groupHeader: { letterSpacing: 0.5 },
item: { transition: 'background 0.15s ease' },
};

const CSS = `
.listy-semantic-root {
border: 1px solid #e5e7eb;
overflow: hidden;
background: #fff;
font-family: system-ui, sans-serif;
}
.listy-semantic-header {
padding: 10px 16px;
font-weight: 600;
color: #fff;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
}
.listy-semantic-item {
padding: 0 16px;
line-height: 40px;
border-bottom: 1px solid #f3f4f6;
}
.listy-semantic-item:hover {
/* hover state — only a className slot can express this, not inline styles */
background: #f5f3ff;
}
`;

export default () => {
const listRef = useRef<ListyRef>(null);
const [virtual, setVirtual] = useState(true);

return (
<>
<style>{CSS}</style>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="checkbox"
checked={virtual}
onChange={(event) => setVirtual(event.target.checked)}
/>
virtual — the same classNames / styles apply in both render modes
</label>
<Listy
ref={listRef}
height={360}
itemHeight={40}
items={items}
rowKey="id"
sticky
virtual={virtual}
classNames={classNames}
styles={styles}
itemRender={(item) => item.name}
group={{
key: (item) => item.groupId,
title: (groupKey) =>
GROUPS.find((group) => group.id === groupKey)?.label ?? groupKey,
}}
/>
</div>
</>
);
};
14 changes: 10 additions & 4 deletions src/GroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface GroupHeaderProps<T, K extends React.Key = React.Key> {
prefixCls: string;
fixed?: boolean;
sticky?: boolean;
className?: string;
style?: React.CSSProperties;
}

Expand All @@ -25,14 +26,19 @@ function GroupHeader<T, K extends React.Key = React.Key>(
prefixCls,
fixed,
sticky,
className: customClassName,
style,
} = props;

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

// ============================== Render ==============================
return (
Expand Down
10 changes: 10 additions & 0 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export type RowKey<T> = keyof T | ((item: T) => React.Key);

export type ScrollAlign = 'top' | 'bottom' | 'auto';

export type ListySemanticName = 'root' | 'item' | 'groupHeader';

export type ListyClassNames = Partial<Record<ListySemanticName, string>>;

export type ListyStyles = Partial<Record<ListySemanticName, React.CSSProperties>>;

export interface GroupScrollToConfig {
groupKey: React.Key;
align?: ScrollAlign;
Expand Down Expand Up @@ -46,6 +52,8 @@ export interface ListyProps<T, K extends React.Key = React.Key> {
virtual?: boolean;
prefixCls?: string;
rowKey: RowKey<T>;
classNames?: ListyClassNames;
styles?: ListyStyles;
onScroll?: React.UIEventHandler<HTMLElement>;
itemRender: (item: T, index: number) => React.ReactNode;
}
Expand All @@ -58,6 +66,8 @@ export interface ListComponentProps<T, K extends React.Key = React.Key> {
group?: Group<T, K>;
prefixCls: string;
rowKey: RowKey<T>;
classNames?: ListyClassNames;
styles?: ListyStyles;
onScroll?: React.UIEventHandler<HTMLElement>;
itemRender: (item: T, index: number) => React.ReactNode;
}
Expand Down
21 changes: 15 additions & 6 deletions src/RawList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import clsx from 'clsx';
import { useEvent } from '@rc-component/util';
import GroupHeader from '../GroupHeader';
import useGroupSegments from '../hooks/useGroupSegments';
Expand All @@ -23,6 +24,8 @@ function RawList<T, K extends React.Key = React.Key>(
prefixCls,
rowKey,
sticky,
classNames,
styles,
} = props;

// =============================== Refs ===============================
Expand Down Expand Up @@ -55,26 +58,29 @@ function RawList<T, K extends React.Key = React.Key>(
return (
<div
key={key}
className={`${prefixCls}-item`}
style={
sticky && groupKey !== undefined
className={clsx(`${prefixCls}-item`, classNames?.item)}
style={{
...styles?.item,
...(sticky && groupKey !== undefined
? {
scrollMarginTop: `var(--${prefixCls}-item-scroll-margin-top, 0px)`,
}
: undefined
}
: undefined),
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{...scrollTargetProps}
>
{itemRender(item, index)}
</div>
);
},
[
classNames?.item,
getItemKey,
getScrollTargetProps,
itemRender,
prefixCls,
sticky,
styles?.item,
],
);

Expand All @@ -95,6 +101,8 @@ function RawList<T, K extends React.Key = React.Key>(
groupItems={currentGroupItems}
prefixCls={prefixCls}
sticky={sticky}
className={classNames?.groupHeader}
style={styles?.groupHeader}
/>
{groupItems.map(({ item, index }) => {
return renderItem(item, index, groupKey);
Expand All @@ -110,11 +118,12 @@ function RawList<T, K extends React.Key = React.Key>(
return (
<div
ref={holderRef}
className={prefixCls}
className={clsx(prefixCls, classNames?.root)}
style={{
maxHeight: height,
overflowY: height === undefined ? undefined : 'auto',
overflowAnchor: 'none',
...styles?.root,
}}
onScroll={onScroll}
>
Expand Down
16 changes: 14 additions & 2 deletions src/VirtualList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import clsx from 'clsx';
import RcVirtualList, {
type ListRef as RcVirtualListRef,
type ScrollConfig,
Expand Down Expand Up @@ -33,6 +34,8 @@ function VirtualList<T, K extends React.Key = React.Key>(
prefixCls,
rowKey,
sticky,
classNames,
styles,
} = props;

// =============================== Refs ===============================
Expand Down Expand Up @@ -139,6 +142,8 @@ function VirtualList<T, K extends React.Key = React.Key>(
groupKeyToItems,
prefixCls,
listRef,
headerClassName: classNames?.groupHeader,
headerStyle: styles?.groupHeader,
});

// ============================ Render Row ============================
Expand All @@ -152,10 +157,12 @@ function VirtualList<T, K extends React.Key = React.Key>(
groupKey={groupKey}
groupItems={groupItems}
prefixCls={prefixCls}
className={classNames?.groupHeader}
style={styles?.groupHeader}
/>
);
},
[group, groupKeyToItems, prefixCls],
[classNames?.groupHeader, group, groupKeyToItems, prefixCls, styles?.groupHeader],
);

// ============================== Render ==============================
Expand All @@ -171,12 +178,17 @@ function VirtualList<T, K extends React.Key = React.Key>(
prefixCls={prefixCls}
virtual
extraRender={extraRender}
className={classNames?.root}
style={styles?.root}
>
{(row: Row<T, K>) =>
row.type === 'header'
? renderHeaderRow(row.groupKey)
: (
<div className={`${prefixCls}-item`}>
<div
className={clsx(`${prefixCls}-item`, classNames?.item)}
style={styles?.item}
>
{itemRender(row.item, row.index)}
</div>
)
Expand Down
20 changes: 18 additions & 2 deletions src/VirtualList/useStickyGroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface StickyHeaderParams<T, K extends React.Key = React.Key> {
groupKeyToItems: Map<K, T[]>;
prefixCls: string;
listRef: React.RefObject<RcVirtualListRef | null>;
headerClassName?: string;
headerStyle?: React.CSSProperties;
}

export default function useStickyGroupHeader<
Expand All @@ -60,6 +62,8 @@ export default function useStickyGroupHeader<
groupKeyToItems,
prefixCls,
listRef,
headerClassName,
headerStyle,
} = params;

// ============================ Extra Render ==========================
Expand Down Expand Up @@ -103,13 +107,25 @@ export default function useStickyGroupHeader<
groupKey={currGroupKey}
groupItems={groupItems}
prefixCls={prefixCls}
style={{ top }}
className={headerClassName}
// `top` is the computed sticky-push offset and must win over any
// user-supplied top in headerStyle, or the sticky behavior breaks.
style={{ ...headerStyle, top }}
/>
</div>
</Portal>
);
},
[enabled, group, groupKeys, groupKeyToItems, prefixCls, listRef],
[
enabled,
group,
groupKeys,
groupKeyToItems,
prefixCls,
listRef,
headerClassName,
headerStyle,
],
);

// ============================== Return ==============================
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Listy from './List';

export type { ListyRef, ListyProps } from './List';
export type {
ListyRef,
ListyProps,
ListySemanticName,
ListyClassNames,
ListyStyles,
} from './List';

export default Listy;
Loading
Loading