@sproutsocial/seeds-react-list 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +13 -0
- package/dist/esm/index.js +1053 -38
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +57 -5
- package/dist/index.d.ts +57 -5
- package/dist/index.js +1059 -41
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/src/DataItemSkeleton.tsx +43 -0
- package/src/DataItemSkeletonTypes.ts +8 -0
- package/src/DataList.stories.tsx +659 -0
- package/src/DataList.tsx +88 -0
- package/src/DataListTypes.ts +31 -0
- package/src/DynamicVirtualizedExample.tsx +85 -0
- package/src/List.stories.tsx +13 -5
- package/src/List.tsx +1 -10
- package/src/ListItem.tsx +22 -19
- package/src/ListItemButton.tsx +17 -5
- package/src/ListItemTypes.ts +2 -0
- package/src/ListTypes.ts +2 -0
- package/src/VirtualizedDataList.stories.tsx +353 -0
- package/src/VirtualizedDataList.tsx +122 -0
- package/src/VirtualizedExamples.tsx +55 -0
- package/src/__tests__/DataList.test.tsx +218 -0
- package/src/__tests__/List.test.tsx +10 -10
- package/src/index.ts +13 -1
package/src/DataList.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import List from "./List";
|
|
4
|
+
import DataItemSkeleton from "./DataItemSkeleton";
|
|
5
|
+
import ListItem from "./ListItem";
|
|
6
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
7
|
+
import Container from "./styles";
|
|
8
|
+
import type { TypeDataListProps } from "./DataListTypes";
|
|
9
|
+
import styled from "styled-components";
|
|
10
|
+
|
|
11
|
+
const HeaderContent = styled(Box)`
|
|
12
|
+
width: 100%;
|
|
13
|
+
transition: transform 0.4s ease-in-out;
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const DataList = <T,>({
|
|
17
|
+
as = "ul",
|
|
18
|
+
data,
|
|
19
|
+
renderItem,
|
|
20
|
+
isLoading,
|
|
21
|
+
LoadingComponent = DataItemSkeleton,
|
|
22
|
+
listItemProps = {},
|
|
23
|
+
estimateItemSize = 50,
|
|
24
|
+
Header,
|
|
25
|
+
...rest
|
|
26
|
+
}: TypeDataListProps<T>) => {
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const loadingItems = [0, 1, 2, 3, 4];
|
|
29
|
+
|
|
30
|
+
// Early return for loading state
|
|
31
|
+
if (isLoading) {
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{Header && (
|
|
35
|
+
<HeaderContent
|
|
36
|
+
style={{
|
|
37
|
+
position: "sticky",
|
|
38
|
+
top: 0,
|
|
39
|
+
left: 0,
|
|
40
|
+
width: "100%",
|
|
41
|
+
zIndex: 1,
|
|
42
|
+
}}
|
|
43
|
+
id="header"
|
|
44
|
+
>
|
|
45
|
+
{Header}
|
|
46
|
+
</HeaderContent>
|
|
47
|
+
)}
|
|
48
|
+
<List as={as} {...rest}>
|
|
49
|
+
{loadingItems.map((index) => (
|
|
50
|
+
<ListItem key={index} {...listItemProps}>
|
|
51
|
+
<LoadingComponent />
|
|
52
|
+
</ListItem>
|
|
53
|
+
))}
|
|
54
|
+
</List>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{Header && (
|
|
62
|
+
<HeaderContent
|
|
63
|
+
style={{
|
|
64
|
+
position: "sticky",
|
|
65
|
+
top: 0,
|
|
66
|
+
left: 0,
|
|
67
|
+
width: "100%",
|
|
68
|
+
zIndex: 1,
|
|
69
|
+
}}
|
|
70
|
+
id="header"
|
|
71
|
+
>
|
|
72
|
+
{Header}
|
|
73
|
+
</HeaderContent>
|
|
74
|
+
)}
|
|
75
|
+
<List as={as} {...rest}>
|
|
76
|
+
{data.map((item, index) => (
|
|
77
|
+
<ListItem key={index} {...listItemProps}>
|
|
78
|
+
{renderItem(item, index)}
|
|
79
|
+
</ListItem>
|
|
80
|
+
))}
|
|
81
|
+
</List>
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
DataList.displayName = "DataList";
|
|
87
|
+
|
|
88
|
+
export default DataList;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { TypeListProps } from "./ListTypes";
|
|
3
|
+
import type { TypeDataItemSkeletonProps } from "./DataItemSkeletonTypes";
|
|
4
|
+
import type { TypeListItemProps } from "./ListItemTypes";
|
|
5
|
+
|
|
6
|
+
export interface TypeDataListProps<T> extends Omit<TypeListProps, "children"> {
|
|
7
|
+
/** Array of data items to render */
|
|
8
|
+
data: T[];
|
|
9
|
+
/** Function to render each item in the data array */
|
|
10
|
+
renderItem: (item: T, index?: number) => React.ReactNode;
|
|
11
|
+
/** If true, shows loading skeletons instead of data */
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
/** Component to render when loading. Receives props for customizing skeleton appearance */
|
|
14
|
+
LoadingComponent?: React.ComponentType<TypeDataItemSkeletonProps>;
|
|
15
|
+
/** Estimated height of each item in pixels for virtualization. Defaults to 60 */
|
|
16
|
+
estimateItemSize?: number;
|
|
17
|
+
/** Props to pass to the list item component */
|
|
18
|
+
listItemProps?: Omit<TypeListItemProps, "children">;
|
|
19
|
+
/** Callback function called when the end of the list is reached (for infinite scrolling) */
|
|
20
|
+
onEndReached?: () => void;
|
|
21
|
+
/** Whether there is a next page available (for infinite scrolling) */
|
|
22
|
+
hasNextPage?: boolean;
|
|
23
|
+
/** Whether the next page is currently being fetched */
|
|
24
|
+
isFetchingNextPage?: boolean;
|
|
25
|
+
/** Header component to display at the top of the list */
|
|
26
|
+
Header?: React.ReactNode;
|
|
27
|
+
/** Scroll offset in pixels at which the header should be removed/hidden. Defaults to 500 */
|
|
28
|
+
removeHeaderDepth?: number;
|
|
29
|
+
/** Height of the header in pixels. Defaults to 60 */
|
|
30
|
+
headerHeight?: number;
|
|
31
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import List from "./List";
|
|
4
|
+
import ListItem from "./ListItem";
|
|
5
|
+
|
|
6
|
+
import { useVirtualizer, useWindowVirtualizer } from "@tanstack/react-virtual";
|
|
7
|
+
|
|
8
|
+
const randomNumber = (min: number, max: number) =>
|
|
9
|
+
faker.number.int({ min, max });
|
|
10
|
+
|
|
11
|
+
const sentences = new Array(10000)
|
|
12
|
+
.fill(true)
|
|
13
|
+
.map(() => faker.lorem.sentence(randomNumber(20, 70)));
|
|
14
|
+
|
|
15
|
+
export function RowVirtualizerDynamic() {
|
|
16
|
+
const parentRef = React.useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
const count = sentences.length;
|
|
19
|
+
const virtualizer = useVirtualizer({
|
|
20
|
+
count,
|
|
21
|
+
getScrollElement: () => parentRef.current,
|
|
22
|
+
estimateSize: () => 45,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const items = virtualizer.getVirtualItems();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
height: "100%",
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
gap: 20,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<h1>Dynamic Virtualizer Usage</h1>
|
|
37
|
+
<div style={{ width: "100%", flexGrow: 1 }}>
|
|
38
|
+
<div
|
|
39
|
+
ref={parentRef}
|
|
40
|
+
className="List"
|
|
41
|
+
style={{
|
|
42
|
+
width: "100%",
|
|
43
|
+
height: "100%",
|
|
44
|
+
overflowY: "auto",
|
|
45
|
+
contain: "strict",
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
style={{
|
|
50
|
+
height: virtualizer.getTotalSize(),
|
|
51
|
+
width: "100%",
|
|
52
|
+
position: "relative",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<ul
|
|
56
|
+
style={{
|
|
57
|
+
position: "absolute",
|
|
58
|
+
top: 0,
|
|
59
|
+
left: 0,
|
|
60
|
+
width: "100%",
|
|
61
|
+
transform: `translateY(${items[0]?.start ?? 0}px)`,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{items.map((virtualRow) => (
|
|
65
|
+
<li
|
|
66
|
+
key={virtualRow.key}
|
|
67
|
+
data-index={virtualRow.index}
|
|
68
|
+
ref={virtualizer.measureElement}
|
|
69
|
+
className={
|
|
70
|
+
virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<div style={{ padding: "10px 0" }}>
|
|
74
|
+
<div>Row {virtualRow.index}</div>
|
|
75
|
+
<div>{sentences[virtualRow.index]}</div>
|
|
76
|
+
</div>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/List.stories.tsx
CHANGED
|
@@ -139,8 +139,6 @@ export const WithIcons: Story = {
|
|
|
139
139
|
},
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
-
// We will revisit this story when we have a more correct button component
|
|
143
|
-
/*
|
|
144
142
|
export const WithListItemButtons: Story = {
|
|
145
143
|
name: "With List Item Buttons",
|
|
146
144
|
render: () => {
|
|
@@ -157,12 +155,23 @@ export const WithListItemButtons: Story = {
|
|
|
157
155
|
</ListItemButton>
|
|
158
156
|
</ListItem>
|
|
159
157
|
<ListItem>
|
|
160
|
-
<ListItemButton>
|
|
158
|
+
<ListItemButton onClick={() => alert("outer click")}>
|
|
161
159
|
<ListItemContent
|
|
162
160
|
iconName="tag-outline"
|
|
163
161
|
text="Another Item"
|
|
164
|
-
details="This is a detail about the item"
|
|
162
|
+
details="This is a detail about the item 2"
|
|
165
163
|
aside="Dec 10"
|
|
164
|
+
// Inside of a ListItemButton other buttons need to have a higher zIndex to be clickable
|
|
165
|
+
actions={
|
|
166
|
+
<Button
|
|
167
|
+
appearance="unstyled"
|
|
168
|
+
aria-label="Reconnect"
|
|
169
|
+
onClick={() => alert("inner click")}
|
|
170
|
+
style={{ zIndex: 1 }}
|
|
171
|
+
>
|
|
172
|
+
<Icon name="plug-outline" size="small" aria-hidden="true" />
|
|
173
|
+
</Button>
|
|
174
|
+
}
|
|
166
175
|
/>
|
|
167
176
|
</ListItemButton>
|
|
168
177
|
</ListItem>
|
|
@@ -170,7 +179,6 @@ export const WithListItemButtons: Story = {
|
|
|
170
179
|
);
|
|
171
180
|
},
|
|
172
181
|
};
|
|
173
|
-
*/
|
|
174
182
|
|
|
175
183
|
export const WithActions: Story = {
|
|
176
184
|
name: "With Actions",
|
package/src/List.tsx
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { Children } from "react";
|
|
3
|
-
import Box from "@sproutsocial/seeds-react-box";
|
|
4
1
|
import Container from "./styles";
|
|
5
2
|
import type { TypeListProps } from "./ListTypes";
|
|
6
3
|
|
|
7
4
|
const List = ({ as = "ul", children, ...rest }: TypeListProps) => {
|
|
8
|
-
const listItems = Children.toArray(children);
|
|
9
|
-
|
|
10
5
|
return (
|
|
11
6
|
<Container as={as} {...rest}>
|
|
12
|
-
{
|
|
13
|
-
<Box as="li" key={index} mt={300}>
|
|
14
|
-
{item}
|
|
15
|
-
</Box>
|
|
16
|
-
))}
|
|
7
|
+
{children}
|
|
17
8
|
</Container>
|
|
18
9
|
);
|
|
19
10
|
};
|
package/src/ListItem.tsx
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import Box from "@sproutsocial/seeds-react-box";
|
|
3
|
-
import Text from "@sproutsocial/seeds-react-text";
|
|
4
|
-
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
5
3
|
import type { TypeListItemProps } from "./ListItemTypes";
|
|
6
4
|
|
|
7
|
-
const ListItem =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
const ListItem = React.forwardRef<HTMLLIElement, TypeListItemProps>(
|
|
6
|
+
({ isBordered, children, ...rest }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<Box
|
|
9
|
+
as="li"
|
|
10
|
+
ref={ref}
|
|
11
|
+
borderRadius="outer"
|
|
12
|
+
{...(isBordered
|
|
13
|
+
? {
|
|
14
|
+
border: 500,
|
|
15
|
+
borderColor: "container.border.base",
|
|
16
|
+
p: 300,
|
|
17
|
+
}
|
|
18
|
+
: {})}
|
|
19
|
+
mt={300}
|
|
20
|
+
{...rest}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</Box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
24
27
|
|
|
25
28
|
ListItem.displayName = "ListItem";
|
|
26
29
|
|
package/src/ListItemButton.tsx
CHANGED
|
@@ -6,24 +6,36 @@ import Text from "@sproutsocial/seeds-react-text";
|
|
|
6
6
|
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
7
7
|
import type { TypeListItemButtonProps } from "./ListItemButtonTypes";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const ButtonContainer = styled(Box)`
|
|
10
10
|
border: 1px solid ${(props) => props.theme.colors.container.border.base};
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
position: relative;
|
|
12
|
+
padding: ${(props) => props.theme.space[300]};
|
|
13
|
+
border-radius: ${(props) => props.theme.radii[500]};
|
|
14
|
+
background-color: ${(props) => props.theme.colors.container.background.base};
|
|
15
|
+
color: ${(props) => props.theme.colors.text.body};
|
|
13
16
|
`;
|
|
14
17
|
|
|
18
|
+
const StyledButton = styled(Button)`
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0;
|
|
21
|
+
left: 0;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
`;
|
|
15
25
|
// This button is not currently used. It is possible to pass interactive elements inside the button which is an accessibility violation.
|
|
16
26
|
// In order to reintroduce it, we will want to mimic confluence's approach of using relative positioning to add a sibling to content which allows clicking
|
|
17
27
|
const ListItemButton = ({
|
|
18
28
|
appearance = "unstyled",
|
|
19
29
|
width = "100%",
|
|
20
30
|
children,
|
|
31
|
+
onClick,
|
|
21
32
|
...rest
|
|
22
33
|
}: TypeListItemButtonProps) => {
|
|
23
34
|
return (
|
|
24
|
-
<
|
|
35
|
+
<ButtonContainer>
|
|
36
|
+
<StyledButton onClick={onClick}> </StyledButton>
|
|
25
37
|
{children}
|
|
26
|
-
</
|
|
38
|
+
</ButtonContainer>
|
|
27
39
|
);
|
|
28
40
|
};
|
|
29
41
|
|
package/src/ListItemTypes.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface TypeListItemProps extends TypeStyledComponentsCommonProps {
|
|
|
7
7
|
isBordered?: boolean;
|
|
8
8
|
/** Children to display inside the list item */
|
|
9
9
|
children: React.ReactNode;
|
|
10
|
+
/** Inline styles to apply to the list item element */
|
|
11
|
+
style?: React.CSSProperties;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface TypeListItemContentProps
|
package/src/ListTypes.ts
CHANGED