@sproutsocial/seeds-react-list 0.3.41 → 0.4.0
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 +11 -11
- package/CHANGELOG.md +12 -0
- package/dist/esm/index.js +223 -44
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +223 -53
- package/dist/index.js.map +1 -1
- package/dist/list.css +112 -0
- package/package.json +3 -3
- package/src/List.tsx +13 -9
- package/src/ListHybrid.tsx +28 -0
- package/src/ListItem.tsx +7 -21
- package/src/ListItemButton.tsx +8 -37
- package/src/ListItemButtonHybrid.tsx +28 -0
- package/src/ListItemButtonStyled.tsx +46 -0
- package/src/ListItemButtonTailwind.tsx +36 -0
- package/src/ListItemContent.tsx +7 -48
- package/src/ListItemContentHybrid.tsx +28 -0
- package/src/ListItemContentStyled.tsx +59 -0
- package/src/ListItemContentTailwind.tsx +56 -0
- package/src/ListItemHybrid.tsx +34 -0
- package/src/ListItemStyled.tsx +41 -0
- package/src/ListItemTailwind.tsx +35 -0
- package/src/ListItemTypes.ts +32 -3
- package/src/ListStyled.tsx +19 -0
- package/src/ListTailwind.tsx +32 -0
- package/src/ListTypes.ts +25 -2
- package/src/__tests__/ListContract.test.tsx +137 -0
- package/src/__tests__/ListItemButton.test.tsx +57 -0
- package/src/__tests__/ListItemContent.test.tsx +92 -0
- package/src/__tests__/ListRouting.test.tsx +102 -0
- package/src/cn.ts +27 -0
- package/src/list.css +112 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
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
|
+
import type { TypeListItemContentProps } from "./ListItemTypes";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Styled-components implementation of ListItemContent. Used by
|
|
9
|
+
* ListItemContentHybrid whenever a consumer passes a styled-system prop or a
|
|
10
|
+
* styled() extension. This is the unchanged legacy rendering path (Box/Text/Icon
|
|
11
|
+
* composition).
|
|
12
|
+
*/
|
|
13
|
+
const ListItemContentStyled = ({
|
|
14
|
+
text,
|
|
15
|
+
details,
|
|
16
|
+
aside,
|
|
17
|
+
iconName,
|
|
18
|
+
iconLabel,
|
|
19
|
+
actions,
|
|
20
|
+
...rest
|
|
21
|
+
}: TypeListItemContentProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<Box
|
|
24
|
+
display="flex"
|
|
25
|
+
justifyContent="space-between"
|
|
26
|
+
borderRadius="outer"
|
|
27
|
+
alignItems="baseline"
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
<Box display="flex" flex="1">
|
|
31
|
+
{iconName && (
|
|
32
|
+
<span>
|
|
33
|
+
<Icon mr={300} name={iconName} size="small" ariaLabel={iconLabel} />
|
|
34
|
+
</span>
|
|
35
|
+
)}
|
|
36
|
+
<Box flex="1">
|
|
37
|
+
<Text.SmallSubHeadline as="div">{text}</Text.SmallSubHeadline>
|
|
38
|
+
{details && (
|
|
39
|
+
<Text.Byline as="div" mt={200}>
|
|
40
|
+
{details}
|
|
41
|
+
</Text.Byline>
|
|
42
|
+
)}
|
|
43
|
+
</Box>
|
|
44
|
+
</Box>
|
|
45
|
+
<Box display="flex" alignItems="center" gap={300} flexShrink={0}>
|
|
46
|
+
{aside && <Text.Byline as="div">{aside}</Text.Byline>}
|
|
47
|
+
{actions && (
|
|
48
|
+
<Box display="flex" alignItems="center">
|
|
49
|
+
{actions}
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
</Box>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
ListItemContentStyled.displayName = "ListItemContent";
|
|
58
|
+
|
|
59
|
+
export default ListItemContentStyled;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
3
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
4
|
+
import { cn } from "./cn";
|
|
5
|
+
import type { TypeListItemContentProps } from "./ListItemTypes";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tailwind CSS implementation of ListItemContent. The Box layout scaffolding is
|
|
9
|
+
* expressed through semantic `seeds-list-item-content*` classes from list.css
|
|
10
|
+
* (no runtime CSS-in-JS). Text and Icon remain as primitives — the spacing they
|
|
11
|
+
* previously received via system props (Icon mr, Byline mt) is now applied by
|
|
12
|
+
* the parent classes so those primitives stay on their own static paths.
|
|
13
|
+
*/
|
|
14
|
+
const ListItemContentTailwind = ({
|
|
15
|
+
text,
|
|
16
|
+
details,
|
|
17
|
+
aside,
|
|
18
|
+
iconName,
|
|
19
|
+
iconLabel,
|
|
20
|
+
actions,
|
|
21
|
+
className,
|
|
22
|
+
...rest
|
|
23
|
+
}: TypeListItemContentProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cn("seeds-list-item-content", className)}
|
|
27
|
+
{...(rest as React.HTMLAttributes<HTMLDivElement>)}
|
|
28
|
+
>
|
|
29
|
+
<div className="seeds-list-item-content-main">
|
|
30
|
+
{iconName && (
|
|
31
|
+
<span className="seeds-list-item-content-icon">
|
|
32
|
+
<Icon name={iconName} size="small" ariaLabel={iconLabel} />
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
<div className="seeds-list-item-content-text">
|
|
36
|
+
<Text.SmallSubHeadline as="div">{text}</Text.SmallSubHeadline>
|
|
37
|
+
{details && (
|
|
38
|
+
<Text.Byline as="div" className="seeds-list-item-content-details">
|
|
39
|
+
{details}
|
|
40
|
+
</Text.Byline>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="seeds-list-item-content-aside">
|
|
45
|
+
{aside && <Text.Byline as="div">{aside}</Text.Byline>}
|
|
46
|
+
{actions && (
|
|
47
|
+
<div className="seeds-list-item-content-actions">{actions}</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
ListItemContentTailwind.displayName = "ListItemContent";
|
|
55
|
+
|
|
56
|
+
export default ListItemContentTailwind;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid ListItem Component
|
|
3
|
+
* Automatically chooses between Tailwind and styled-components based on props.
|
|
4
|
+
*
|
|
5
|
+
* Routes to the styled-components path when a styled-system prop is present
|
|
6
|
+
* (hasStyledProps) or the component was extended with styled()
|
|
7
|
+
* (isStyledExtension). Otherwise renders the Tailwind path (preferred — no
|
|
8
|
+
* runtime CSS-in-JS).
|
|
9
|
+
*
|
|
10
|
+
* `isBordered` is a semantic public prop, not a styled-system prop, so it maps
|
|
11
|
+
* to a class on the Tailwind path and does not force styled-components.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import {
|
|
15
|
+
hasStyledProps,
|
|
16
|
+
isStyledExtension,
|
|
17
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
18
|
+
import type { TypeListItemProps } from "./ListItemTypes";
|
|
19
|
+
import ListItemStyled from "./ListItemStyled";
|
|
20
|
+
import ListItemTailwind from "./ListItemTailwind";
|
|
21
|
+
|
|
22
|
+
const ListItemHybrid = React.forwardRef<HTMLLIElement, TypeListItemProps>(
|
|
23
|
+
(props, ref) => {
|
|
24
|
+
if (hasStyledProps(props) || isStyledExtension(props)) {
|
|
25
|
+
return <ListItemStyled {...props} ref={ref} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <ListItemTailwind {...props} ref={ref} />;
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
ListItemHybrid.displayName = "ListItem";
|
|
33
|
+
|
|
34
|
+
export default ListItemHybrid;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import type { TypeListItemProps } from "./ListItemTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Styled-components implementation of ListItem. Used by ListItemHybrid whenever
|
|
7
|
+
* a consumer passes a styled-system prop or a styled() extension. This is the
|
|
8
|
+
* unchanged legacy rendering path (a Box rendered as <li>).
|
|
9
|
+
*/
|
|
10
|
+
const ListItemStyled = React.forwardRef<HTMLLIElement, TypeListItemProps>(
|
|
11
|
+
({ isBordered, children, ...rest }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<Box
|
|
14
|
+
as="li"
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
16
|
+
// @ts-ignore legacy ref type mismatch: Box types its ref as a div, but
|
|
17
|
+
// it renders an <li> here and forwards the ref to that element.
|
|
18
|
+
ref={ref}
|
|
19
|
+
borderRadius="outer"
|
|
20
|
+
{...(isBordered
|
|
21
|
+
? {
|
|
22
|
+
border: 500,
|
|
23
|
+
borderColor: "container.border.base",
|
|
24
|
+
p: 300,
|
|
25
|
+
}
|
|
26
|
+
: {})}
|
|
27
|
+
mt={300}
|
|
28
|
+
// rest carries system props + native <li> attributes. Box is typed to a
|
|
29
|
+
// div, so the li-specific event-handler element types are widened away;
|
|
30
|
+
// the ref (on its own line above) is unaffected by this spread cast.
|
|
31
|
+
{...(rest as Record<string, unknown>)}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
ListItemStyled.displayName = "ListItem";
|
|
40
|
+
|
|
41
|
+
export default ListItemStyled;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import type { TypeListItemProps } from "./ListItemTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tailwind CSS implementation of ListItem. Renders an <li> with the semantic
|
|
7
|
+
* `seeds-list-item` class (plus `seeds-list-item-bordered` when isBordered)
|
|
8
|
+
* from list.css — no runtime CSS-in-JS.
|
|
9
|
+
*
|
|
10
|
+
* The semantic class is placed before the consumer `className` so consumer
|
|
11
|
+
* utilities can override the layered base styles.
|
|
12
|
+
*/
|
|
13
|
+
const ListItemTailwind = React.forwardRef<HTMLLIElement, TypeListItemProps>(
|
|
14
|
+
({ isBordered, children, className, ...rest }, ref) => {
|
|
15
|
+
const classes = cn(
|
|
16
|
+
"seeds-list-item",
|
|
17
|
+
{ "seeds-list-item-bordered": !!isBordered },
|
|
18
|
+
className
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<li
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={classes}
|
|
25
|
+
{...(rest as React.LiHTMLAttributes<HTMLLIElement>)}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</li>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
ListItemTailwind.displayName = "ListItem";
|
|
34
|
+
|
|
35
|
+
export default ListItemTailwind;
|
package/src/ListItemTypes.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypeStyledComponentsCommonProps,
|
|
4
|
+
TypeBorderSystemProps,
|
|
5
|
+
TypeColorSystemProps,
|
|
6
|
+
TypeFlexboxSystemProps,
|
|
7
|
+
TypeGridSystemProps,
|
|
8
|
+
TypeLayoutSystemProps,
|
|
9
|
+
TypePositionSystemProps,
|
|
10
|
+
TypeSpaceSystemProps,
|
|
11
|
+
TypeTypographySystemProps,
|
|
12
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
3
13
|
import type { TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
4
14
|
|
|
5
|
-
export interface TypeListItemProps
|
|
15
|
+
export interface TypeListItemProps
|
|
16
|
+
extends Omit<React.LiHTMLAttributes<HTMLLIElement>, "color">,
|
|
17
|
+
TypeStyledComponentsCommonProps,
|
|
18
|
+
TypeBorderSystemProps,
|
|
19
|
+
TypeColorSystemProps,
|
|
20
|
+
TypeFlexboxSystemProps,
|
|
21
|
+
TypeGridSystemProps,
|
|
22
|
+
TypeLayoutSystemProps,
|
|
23
|
+
TypePositionSystemProps,
|
|
24
|
+
TypeSpaceSystemProps,
|
|
25
|
+
TypeTypographySystemProps {
|
|
6
26
|
/** If true, adds a border and padding around the list item which looks like a card or button */
|
|
7
27
|
isBordered?: boolean;
|
|
8
28
|
/** Children to display inside the list item */
|
|
@@ -12,7 +32,16 @@ export interface TypeListItemProps extends TypeStyledComponentsCommonProps {
|
|
|
12
32
|
}
|
|
13
33
|
|
|
14
34
|
export interface TypeListItemContentProps
|
|
15
|
-
extends
|
|
35
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
|
36
|
+
TypeStyledComponentsCommonProps,
|
|
37
|
+
TypeBorderSystemProps,
|
|
38
|
+
TypeColorSystemProps,
|
|
39
|
+
TypeFlexboxSystemProps,
|
|
40
|
+
TypeGridSystemProps,
|
|
41
|
+
TypeLayoutSystemProps,
|
|
42
|
+
TypePositionSystemProps,
|
|
43
|
+
TypeSpaceSystemProps,
|
|
44
|
+
TypeTypographySystemProps {
|
|
16
45
|
/** Main text displayed as SmallSubheadline */
|
|
17
46
|
text: React.ReactNode;
|
|
18
47
|
/** Details text displayed as Byline below the main text */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Container from "./styles";
|
|
2
|
+
import type { TypeListProps } from "./ListTypes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Styled-components implementation of List. Used by ListHybrid whenever a
|
|
6
|
+
* consumer passes a styled-system prop or a styled() extension. This is the
|
|
7
|
+
* unchanged legacy rendering path.
|
|
8
|
+
*/
|
|
9
|
+
const ListStyled = ({ as = "ul", children, ...rest }: TypeListProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<Container as={as} {...rest}>
|
|
12
|
+
{children}
|
|
13
|
+
</Container>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
ListStyled.displayName = "List";
|
|
18
|
+
|
|
19
|
+
export default ListStyled;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import type { TypeListProps } from "./ListTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tailwind CSS implementation of List. Renders the `as` element (ul/ol) with
|
|
7
|
+
* the semantic `seeds-list` class from list.css — no runtime CSS-in-JS.
|
|
8
|
+
*
|
|
9
|
+
* The semantic class is placed before the consumer `className` so consumer
|
|
10
|
+
* utilities can override the layered base styles.
|
|
11
|
+
*/
|
|
12
|
+
const ListTailwind = ({
|
|
13
|
+
as: Element = "ul",
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
...rest
|
|
17
|
+
}: TypeListProps) => {
|
|
18
|
+
return (
|
|
19
|
+
<Element
|
|
20
|
+
className={cn("seeds-list", className)}
|
|
21
|
+
// On the Tailwind path system props are absent at runtime (they route to
|
|
22
|
+
// the styled path), so the remaining rest is native attributes only.
|
|
23
|
+
{...(rest as React.HTMLAttributes<HTMLUListElement | HTMLOListElement>)}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</Element>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ListTailwind.displayName = "List";
|
|
31
|
+
|
|
32
|
+
export default ListTailwind;
|
package/src/ListTypes.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypeStyledComponentsCommonProps,
|
|
4
|
+
TypeBorderSystemProps,
|
|
5
|
+
TypeColorSystemProps,
|
|
6
|
+
TypeFlexboxSystemProps,
|
|
7
|
+
TypeGridSystemProps,
|
|
8
|
+
TypeLayoutSystemProps,
|
|
9
|
+
TypePositionSystemProps,
|
|
10
|
+
TypeSpaceSystemProps,
|
|
11
|
+
TypeTypographySystemProps,
|
|
12
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
3
13
|
|
|
4
14
|
export type TypeListElement = "ul" | "ol";
|
|
5
15
|
|
|
6
|
-
export interface TypeListProps
|
|
16
|
+
export interface TypeListProps
|
|
17
|
+
extends Omit<
|
|
18
|
+
React.HTMLAttributes<HTMLUListElement | HTMLOListElement>,
|
|
19
|
+
"color"
|
|
20
|
+
>,
|
|
21
|
+
TypeStyledComponentsCommonProps,
|
|
22
|
+
TypeBorderSystemProps,
|
|
23
|
+
TypeColorSystemProps,
|
|
24
|
+
TypeFlexboxSystemProps,
|
|
25
|
+
TypeGridSystemProps,
|
|
26
|
+
TypeLayoutSystemProps,
|
|
27
|
+
TypePositionSystemProps,
|
|
28
|
+
TypeSpaceSystemProps,
|
|
29
|
+
TypeTypographySystemProps {
|
|
7
30
|
/** The HTML element to render as - either "ul" or "ol" */
|
|
8
31
|
as?: TypeListElement;
|
|
9
32
|
/** Array of elements to render as list items */
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import { theme } from "@sproutsocial/seeds-react-theme";
|
|
4
|
+
import List from "../List";
|
|
5
|
+
import ListItem from "../ListItem";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Permanent contract tests for List and ListItem.
|
|
9
|
+
*
|
|
10
|
+
* These freeze the observable public behavior of both components and must stay
|
|
11
|
+
* GREEN across the styled-components -> Tailwind hybrid migration. They assert
|
|
12
|
+
* behavior (element, ordering, spacing, ref, attribute forwarding) rather than
|
|
13
|
+
* implementation details, so the same file gates both the legacy and hybrid
|
|
14
|
+
* implementations.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe("List contract", () => {
|
|
18
|
+
it("renders a <ul> by default", () => {
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<List>
|
|
21
|
+
<li>Item</li>
|
|
22
|
+
</List>
|
|
23
|
+
);
|
|
24
|
+
expect(container.querySelector("ul")).toBeInTheDocument();
|
|
25
|
+
expect(container.querySelector("ol")).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders the element named by `as`", () => {
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<List as="ol">
|
|
31
|
+
<li>Item</li>
|
|
32
|
+
</List>
|
|
33
|
+
);
|
|
34
|
+
expect(container.querySelector("ol")).toBeInTheDocument();
|
|
35
|
+
expect(container.querySelector("ul")).not.toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("resets list styling (no margin, padding, or marker)", () => {
|
|
39
|
+
// Asserted on the preserved styled path (routed via a styled() extension
|
|
40
|
+
// className), where the reset values are observable in jest. The default
|
|
41
|
+
// Tailwind path expresses the same reset through the `seeds-list` class,
|
|
42
|
+
// whose CSS is verified by the CSS-delivery check.
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<List className="sc-reset">
|
|
45
|
+
<li>Item</li>
|
|
46
|
+
</List>
|
|
47
|
+
);
|
|
48
|
+
const list = container.querySelector("ul") as HTMLElement;
|
|
49
|
+
expect(list).toHaveStyleRule("margin", "0");
|
|
50
|
+
expect(list).toHaveStyleRule("padding", "0");
|
|
51
|
+
expect(list).toHaveStyleRule("list-style", "none");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("forwards native attributes and inline style to the list element", () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<List id="my-list" data-testid="list" style={{ color: "red" }}>
|
|
57
|
+
<li>Item</li>
|
|
58
|
+
</List>
|
|
59
|
+
);
|
|
60
|
+
const list = container.querySelector("ul") as HTMLElement;
|
|
61
|
+
expect(list).toHaveAttribute("id", "my-list");
|
|
62
|
+
expect(list).toHaveAttribute("data-testid", "list");
|
|
63
|
+
expect(list.style.color).toBe("red");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders its children", () => {
|
|
67
|
+
render(
|
|
68
|
+
<List>
|
|
69
|
+
<li>First</li>
|
|
70
|
+
<li>Second</li>
|
|
71
|
+
</List>
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByText("First")).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByText("Second")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("ListItem contract", () => {
|
|
79
|
+
it("renders an <li>", () => {
|
|
80
|
+
const { container } = render(<ListItem>Item</ListItem>);
|
|
81
|
+
expect(container.querySelector("li")).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("forwards a ref to the <li> element", () => {
|
|
85
|
+
const ref = React.createRef<HTMLLIElement>();
|
|
86
|
+
render(<ListItem ref={ref}>Item</ListItem>);
|
|
87
|
+
expect(ref.current).toBeInstanceOf(HTMLLIElement);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("applies top margin so items stack with spacing", () => {
|
|
91
|
+
// Asserted on the preserved styled path (routed via a styled() extension
|
|
92
|
+
// className), where the token value is observable in jest. The default
|
|
93
|
+
// Tailwind path expresses the same spacing through the `seeds-list-item`
|
|
94
|
+
// class.
|
|
95
|
+
const { container } = render(
|
|
96
|
+
<ListItem className="sc-spacing">Item</ListItem>
|
|
97
|
+
);
|
|
98
|
+
const item = container.querySelector("li") as HTMLElement;
|
|
99
|
+
expect(item).toHaveStyleRule("margin-top", theme.space[300]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("adds card-like border and padding when isBordered", () => {
|
|
103
|
+
// Asserted on the preserved styled path; the default Tailwind path expresses
|
|
104
|
+
// the same via the `seeds-list-item-bordered` class.
|
|
105
|
+
const { container } = render(
|
|
106
|
+
<ListItem className="sc-bordered" isBordered>
|
|
107
|
+
Item
|
|
108
|
+
</ListItem>
|
|
109
|
+
);
|
|
110
|
+
const item = container.querySelector("li") as HTMLElement;
|
|
111
|
+
expect(item).toHaveStyleRule("padding", theme.space[300]);
|
|
112
|
+
expect(item).toHaveStyleRule(
|
|
113
|
+
"border-color",
|
|
114
|
+
theme.colors.container.border.base
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not add border or padding when not bordered", () => {
|
|
119
|
+
const { container } = render(
|
|
120
|
+
<ListItem className="sc-plain">Item</ListItem>
|
|
121
|
+
);
|
|
122
|
+
const item = container.querySelector("li") as HTMLElement;
|
|
123
|
+
expect(item).not.toHaveStyleRule("padding", theme.space[300]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("forwards native attributes and inline style to the <li>", () => {
|
|
127
|
+
const { container } = render(
|
|
128
|
+
<ListItem id="row" data-testid="row" style={{ color: "red" }}>
|
|
129
|
+
Item
|
|
130
|
+
</ListItem>
|
|
131
|
+
);
|
|
132
|
+
const item = container.querySelector("li") as HTMLElement;
|
|
133
|
+
expect(item).toHaveAttribute("id", "row");
|
|
134
|
+
expect(item).toHaveAttribute("data-testid", "row");
|
|
135
|
+
expect(item.style.color).toBe("red");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
fireEvent,
|
|
6
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
|
+
import ListItemButton from "../ListItemButton";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Contract + routing tests for the ListItemButton Tailwind hybrid.
|
|
11
|
+
*
|
|
12
|
+
* The default (Tailwind) path renders semantic `seeds-list-item-button*`
|
|
13
|
+
* classes; the styled-components path is retained for system props and styled()
|
|
14
|
+
* extensions. CSS is mocked in jest, so the Tailwind path is asserted via class
|
|
15
|
+
* names.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe("ListItemButton contract", () => {
|
|
19
|
+
it("renders its children", () => {
|
|
20
|
+
render(<ListItemButton>Card body</ListItemButton>);
|
|
21
|
+
expect(screen.getByText("Card body")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders an overlay button that fires onClick", () => {
|
|
25
|
+
const onClick = jest.fn();
|
|
26
|
+
render(<ListItemButton onClick={onClick}>Card body</ListItemButton>);
|
|
27
|
+
const button = screen.getByRole("button");
|
|
28
|
+
fireEvent.click(button);
|
|
29
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("ListItemButton routing", () => {
|
|
34
|
+
it("renders the Tailwind path by default", () => {
|
|
35
|
+
const { container } = render(<ListItemButton>Body</ListItemButton>);
|
|
36
|
+
expect(container.firstChild).toHaveClass("seeds-list-item-button");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("applies the overlay class to the overlay button", () => {
|
|
40
|
+
render(<ListItemButton>Body</ListItemButton>);
|
|
41
|
+
expect(screen.getByRole("button")).toHaveClass(
|
|
42
|
+
"seeds-list-item-button-overlay"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("routes to the styled-components path for a styled() extension className", () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<ListItemButton className="sc-abc123">Body</ListItemButton>
|
|
49
|
+
);
|
|
50
|
+
expect(container.firstChild).not.toHaveClass("seeds-list-item-button");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("routes to the styled-components path when a system prop is present", () => {
|
|
54
|
+
const { container } = render(<ListItemButton p={300}>Body</ListItemButton>);
|
|
55
|
+
expect(container.firstChild).not.toHaveClass("seeds-list-item-button");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import ListItemContent from "../ListItemContent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contract + routing tests for the ListItemContent Tailwind hybrid.
|
|
7
|
+
*
|
|
8
|
+
* The default (Tailwind) path renders semantic `seeds-list-item-content*`
|
|
9
|
+
* classes; the styled-components path is retained for system props and styled()
|
|
10
|
+
* extensions. CSS is mocked in jest, so the Tailwind path is asserted via class
|
|
11
|
+
* names.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
describe("ListItemContent contract", () => {
|
|
15
|
+
it("renders the main text", () => {
|
|
16
|
+
render(<ListItemContent text="Headline" />);
|
|
17
|
+
expect(screen.getByText("Headline")).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders details, aside, and actions when provided", () => {
|
|
21
|
+
render(
|
|
22
|
+
<ListItemContent
|
|
23
|
+
text="Headline"
|
|
24
|
+
details="Some details"
|
|
25
|
+
aside="Nov 5"
|
|
26
|
+
actions={<button>Do</button>}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText("Some details")).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText("Nov 5")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByRole("button", { name: "Do" })).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders an accessible icon when iconName + iconLabel are given", async () => {
|
|
35
|
+
const { runA11yCheck } = render(
|
|
36
|
+
<ListItemContent
|
|
37
|
+
text="Headline"
|
|
38
|
+
iconName="tag-solid"
|
|
39
|
+
iconLabel="Tagged"
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
expect(screen.getByLabelText("Tagged")).toBeInTheDocument();
|
|
43
|
+
await runA11yCheck();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("omits optional slots when not provided", () => {
|
|
47
|
+
render(<ListItemContent text="Headline" />);
|
|
48
|
+
expect(screen.queryByText("Some details")).not.toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("spaces details below the headline via a parent-qualified rule", () => {
|
|
52
|
+
// The details node is a Text.Byline, so it also carries `.seeds-text`
|
|
53
|
+
// (margin: 0). The details margin only applies because list.css qualifies it
|
|
54
|
+
// as `.seeds-list-item-content .seeds-list-item-content-details` to outrank
|
|
55
|
+
// `.seeds-text`. Assert the class and the ancestor the selector depends on
|
|
56
|
+
// so a future refactor that breaks the nesting fails here.
|
|
57
|
+
render(<ListItemContent text="Headline" details="Some details" />);
|
|
58
|
+
const details = screen.getByText("Some details");
|
|
59
|
+
expect(details).toHaveClass("seeds-list-item-content-details");
|
|
60
|
+
expect(details.closest(".seeds-list-item-content")).not.toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("ListItemContent routing", () => {
|
|
65
|
+
it("renders the Tailwind path by default", () => {
|
|
66
|
+
const { container } = render(<ListItemContent text="Headline" />);
|
|
67
|
+
expect(container.firstChild).toHaveClass("seeds-list-item-content");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("places semantic class before consumer className", () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<ListItemContent text="Headline" className="consumer" />
|
|
73
|
+
);
|
|
74
|
+
expect((container.firstChild as HTMLElement).className).toBe(
|
|
75
|
+
"seeds-list-item-content consumer"
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("routes to the styled-components path for a styled() extension className", () => {
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<ListItemContent text="Headline" className="sc-abc123" />
|
|
82
|
+
);
|
|
83
|
+
// The styled path renders through Box (styled-components), so the outer
|
|
84
|
+
// element does not carry the semantic Tailwind class.
|
|
85
|
+
expect(container.firstChild).not.toHaveClass("seeds-list-item-content");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("routes to the styled-components path when a system prop is present", () => {
|
|
89
|
+
const { container } = render(<ListItemContent text="Headline" p={300} />);
|
|
90
|
+
expect(container.firstChild).not.toHaveClass("seeds-list-item-content");
|
|
91
|
+
});
|
|
92
|
+
});
|