@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.
@@ -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
+ }
@@ -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
- {listItems.map((item, index) => (
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 = ({ isBordered, children, ...rest }: TypeListItemProps) => {
8
- return (
9
- <Box
10
- borderRadius="outer"
11
- {...(isBordered
12
- ? {
13
- border: 500,
14
- borderColor: "container.border.base",
15
- p: 300,
16
- }
17
- : {})}
18
- {...rest}
19
- >
20
- {children}
21
- </Box>
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
 
@@ -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 StyledButton = styled(Button)`
9
+ const ButtonContainer = styled(Box)`
10
10
  border: 1px solid ${(props) => props.theme.colors.container.border.base};
11
- text-align: initial;
12
- font-weight: initial;
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
- <StyledButton appearance={appearance} width={width} {...rest}>
35
+ <ButtonContainer>
36
+ <StyledButton onClick={onClick}>&nbsp;</StyledButton>
25
37
  {children}
26
- </StyledButton>
38
+ </ButtonContainer>
27
39
  );
28
40
  };
29
41
 
@@ -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
@@ -8,4 +8,6 @@ export interface TypeListProps extends TypeStyledComponentsCommonProps {
8
8
  as?: TypeListElement;
9
9
  /** Array of elements to render as list items */
10
10
  children: React.ReactNode;
11
+ /** Inline styles to apply to the list element */
12
+ style?: React.CSSProperties;
11
13
  }