import { faker } from "@faker-js/faker" import { generateEvmAddress, truncateAddress, } from "@opensea/testing-utils/address" import { wait } from "@opensea/testing-utils/wait" import { StoryObj, StoryFn, Meta } from "@storybook/react" import { addSeconds } from "date-fns" import { list } from "radash" import React, { useEffect, useState } from "react" import { useCallbackRef, useRelativeTimeFormatter, useItemsWithSkeletons, } from "../../hooks" import { Satellite } from "../../icons" import { classNames } from "../../utils" import { Item } from "../Item" import { SideModule } from "../SideModule" import { Skeleton } from "../Skeleton" import { tableRowSizeVariants, useTable } from "../Table" import { TableColumnSort } from "../Table/TableHeaderCell" import { Text } from "../Text" import { VirtualizedTable, VirtualizedTableProps, VirtualizedTableWindowModeProps, } from "./VirtualizedTable" const PAGE_SIZE = 50 const generateItem = (rowIndex: number) => ({ key: faker.string.uuid(), index: rowIndex, primary: faker.string.numeric(), secondary: faker.string.numeric(), tertiary: faker.string.numeric(), quaternary: faker.string.numeric(), }) type ItemType = ReturnType const COLUMN_CLASSNAMES = [ classNames("w-[400px]"), classNames("w-[200px]"), classNames("w-[400px]"), classNames("w-[400px]"), classNames("w-[400px]"), ] const NOOP_PAGINATION = { hasNext: false, isLoadingNext: false, loadNext: () => Promise.resolve(), } const meta: Meta = { title: "Design System/VirtualizedTable", component: VirtualizedTable, } export default meta type Story = StoryObj> const stickyClassName = classNames("sticky top-0 z-[1]") const Template = (args: Partial>) => { const [items, setItems] = useState( list(PAGE_SIZE - 1).map(i => generateItem(i)), ) const [isLoadingNext, setIsLoadingNext] = useState(false) const loadNext = async () => { setIsLoadingNext(true) await wait(500) setItems([ ...items, ...list(PAGE_SIZE - 1).map(index => generateItem(items.length + index)), ]) setIsLoadingNext(false) } const pagination = { hasNext: true, isLoadingNext, loadNext, paginationThreshold: 10, } return ( Row # Primary Secondary Tertiary Quaternary } itemKey={item => item?.key} items={items} renderRow={({ item }) => ( {item.index} {item.primary} {item.secondary} {item.tertiary} {item.quaternary} )} {...pagination} // making types happy {...(args as Partial>)} /> ) } export const Sticky: Story = { render: Template, } export const FixedTableHeight: Story = { render: Template, args: { className: "h-[500px]", scrollMode: "table", }, } export const NoScrollBorder: Story = { render: Template, args: { className: "h-[500px]", scrollMode: "table", headerBorderOnScroll: false, }, } export const NestedTableScrollMode = ( args: Partial> & { parentScrollContainer?: boolean }, ) => { const [items, setItems] = useState( list(PAGE_SIZE - 1).map(i => generateItem(i)), ) const [isLoadingNext, setIsLoadingNext] = useState(false) const loadNext = async () => { setIsLoadingNext(true) await wait(500) setItems([ ...items, ...list(PAGE_SIZE - 1).map(index => generateItem(items.length + index)), ]) setIsLoadingNext(false) } const pagination = { hasNext: true, isLoadingNext, loadNext, paginationThreshold: 10, } const [containerRef, setContainerREf] = useCallbackRef() return ( Random content Table Row # Primary Secondary Tertiary Quaternary } itemKey={item => item?.key} items={items} renderRow={({ item }) => ( {item.index} {item.primary} {item.secondary} {item.tertiary} {item.quaternary} )} scrollContainerRef={containerRef} scrollMode="table" {...pagination} // making types happy {...(args as Partial>)} /> ) } export const FitWidth = (args: Partial>) => { const items = list(PAGE_SIZE - 1).map(i => generateItem(i)) return ( >)} header={ Row # Primary Secondary Tertiary Quaternary } itemKey={item => item?.key} items={items} renderRow={({ item }) => ( {item.index} {item.primary} {item.secondary} {item.tertiary} {item.quaternary} )} {...NOOP_PAGINATION} /> ) } export const WithSortableHeaders: StoryFn = () => { const [items, setItems] = useState( list(PAGE_SIZE - 1).map(i => generateItem(i)), ) const [isLoadingNext, setIsLoadingNext] = useState(false) const sorts = [ "index", "primary", "secondary", "tertiary", "quaternary", ] as const const [selectedSort, setSelectedSort] = useState>() const loadNext = async () => { setIsLoadingNext(true) await wait(500) setItems([ ...items, ...list(PAGE_SIZE - 1).map(index => generateItem(items.length + index)), ]) setIsLoadingNext(false) } const pagination = { hasNext: true, isLoadingNext, loadNext, paginationThreshold: 10, } const sortedItems = [...items].sort((a, b) => { if (!selectedSort) { return 0 } const [aValue, bValue] = [a[selectedSort.column], b[selectedSort.column]] if (aValue === bValue) { return 0 } if (selectedSort.order === "asc") { return aValue < bValue ? -1 : 1 } return aValue < bValue ? 1 : -1 }) return ( Row # Primary Secondary Tertiary Quaternary } itemKey={item => item?.key} items={sortedItems} renderRow={({ item }) => ( {item.index} {item.primary} {item.secondary} {item.tertiary} {item.quaternary} )} {...pagination} /> ) } function InteractiveTemplate({ onRowClick, dividers, }: { onRowClick?: () => unknown dividers?: boolean }) { const [items, setItems] = useState( list(PAGE_SIZE - 1).map(i => generateItem(i)), ) const [isLoadingNext, setIsLoadingNext] = useState(false) const loadNext = async () => { setIsLoadingNext(true) await wait(500) setItems([ ...items, ...list(PAGE_SIZE - 1).map(index => generateItem(items.length + index)), ]) setIsLoadingNext(false) } const pagination = { hasNext: true, isLoadingNext, loadNext, paginationThreshold: 10, } return ( Row # Primary Secondary Tertiary Quaternary } itemKey={item => item?.key} items={items} renderRow={({ item }) => ( { onRowClick?.() setItems(items => items.filter(i => i.index !== item.index)) }} > {item.index} {item.primary} {item.secondary} {item.tertiary} {item.quaternary} )} {...pagination} /> ) } export const Interactive: StoryObj<{ onRowClick?: () => unknown dividers?: boolean }> = { render: InteractiveTemplate, } export const InteractiveWithDividers: StoryObj<{ onRowClick?: () => unknown dividers?: boolean }> = { render: InteractiveTemplate, args: { dividers: true, }, } const startingDate = new Date() const generateActivity = (rowIndex: number) => { return { key: faker.string.uuid(), event: "Sale", name: faker.person.fullName(), collectionName: faker.internet.displayName(), collectionImage: faker.image.dataUri(), value: faker.number.float({ min: 0, max: 10 }), from: generateEvmAddress(), to: generateEvmAddress(), createdDate: addSeconds(startingDate, -rowIndex), } } const ACTIVITY_COLUMN_CLASSNAMES = [ classNames("w-[180px]"), classNames("w-[300px] grow"), classNames("w-[180px]"), classNames("w-[180px]"), classNames("w-[180px]"), classNames("w-[180px]"), ] const ActivityTemplate = (args: { hasError?: boolean }) => { const [activities, setActivities] = useState< ReturnType[] >([]) const [isLoading, setIsLoading] = useState(false) const [isLoadingNext, setIsLoadingNext] = useState(false) useEffect(() => { const loadInitialItems = async () => { setIsLoading(true) await wait(1000) setActivities(list(PAGE_SIZE - 1).map(generateActivity)) setIsLoading(false) } loadInitialItems() }, []) const loadNext = async () => { setIsLoadingNext(true) await wait(1500) if (args.hasError) { setIsLoadingNext(false) throw new Error("Failed to load next page") } setActivities([ ...activities, ...list(PAGE_SIZE - 1).map(index => generateActivity(activities.length + index), ), ]) setIsLoadingNext(false) } const pagination = { hasNext: true, isLoadingNext, loadNext, paginationThreshold: 10, } const items = useItemsWithSkeletons({ items: activities, pageSize: PAGE_SIZE, isLoading, isLoadingNext, }) const format = useRelativeTimeFormatter() return ( Event Item Value From To Time } itemKey={item => item?.key} items={items} renderRow={function Render({ item }) { const { size } = useTable() // Render skeletons whenever item is undefined if (item === undefined) { return ( ) } return ( Sale {item.name} {item.value.toFixed(3)} ETH {truncateAddress(item.from)} {truncateAddress(item.to)} {format(item.createdDate)} ) }} /> ) } export const Activity: StoryObj<{ hasError?: boolean }> = { render: ActivityTemplate, } export const ActivityWithError: StoryObj<{ hasError?: boolean }> = { render: ActivityTemplate, args: { hasError: true, }, } export const DuplicateKeys: StoryObj = { render: () => { return ( // Should only render 1 row "1"} items={list(100).map(i => generateItem(i))} renderRow={({ item }) =>
{item.index}
} /> ) }, }