@sproutsocial/seeds-react-card 1.1.53 → 1.1.55

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.
@@ -1,313 +0,0 @@
1
- import React from "react";
2
- import {
3
- render,
4
- screen,
5
- waitFor,
6
- } from "@sproutsocial/seeds-react-testing-library";
7
- import { PointerEventsCheckLevel } from "@testing-library/user-event";
8
- import Card from "../";
9
- import Badge from "@sproutsocial/seeds-react-badge";
10
- import {
11
- CardContent,
12
- CardFooter,
13
- CardHeader,
14
- CardLink,
15
- } from "../subComponents";
16
- import { theme } from "@sproutsocial/seeds-react-theme";
17
-
18
- jest.mock("../utils");
19
- const mockCardClick = jest.fn();
20
-
21
- describe("A card is interactive", () => {
22
- it("should be clickable", async () => {
23
- const { user } = render(
24
- <Card role="button" onClick={mockCardClick}>
25
- Test
26
- </Card>
27
- );
28
-
29
- const card = screen.getByRole("button");
30
-
31
- await user.click(card);
32
- expect(mockCardClick).toBeCalledTimes(1);
33
- });
34
-
35
- it("should function as a link", async () => {
36
- const { user } = render(
37
- <Card role="link" href="https://sproutsocial.com/">
38
- Hello
39
- <CardLink>Test</CardLink>
40
- </Card>
41
- );
42
-
43
- const card = screen.getByText("Hello");
44
- const link = screen.getByText("Test");
45
-
46
- // listen to the child link to make sure that clicking the parent Card clicks the link programmatically
47
- link.addEventListener("click", mockCardClick);
48
-
49
- expect(card).toContainElement(link);
50
- expect(link).toHaveAttribute("href", "https://sproutsocial.com/");
51
-
52
- await user.click(card);
53
- expect(mockCardClick).toBeCalled();
54
- });
55
-
56
- it("should function as a button", async () => {
57
- const { user } = render(
58
- <Card role="button" onClick={mockCardClick}>
59
- Test
60
- </Card>
61
- );
62
-
63
- const card = screen.getByRole("button");
64
- expect(card).toHaveAttribute("role", "button");
65
-
66
- await user.click(card);
67
- expect(mockCardClick).toBeCalledTimes(1);
68
- });
69
-
70
- it("can be purely presentational", () => {
71
- render(<Card role="presentation">Test</Card>);
72
-
73
- const card = screen.getByRole("presentation");
74
- expect(card).toHaveAttribute("role", "presentation");
75
- });
76
-
77
- it("can be disabled", async () => {
78
- const { user } = render(
79
- <Card role="button" onClick={mockCardClick} disabled={true}>
80
- Test
81
- </Card>
82
- );
83
-
84
- const card = screen.getByRole("button");
85
-
86
- // Disabled cards should have aria-disabled attribute
87
- expect(card).toHaveAttribute("aria-disabled", "true");
88
-
89
- // Disabled cards should not be focusable
90
- expect(card).toHaveAttribute("tabindex", "-1");
91
-
92
- // Disabled cards should not respond to clicks
93
- await user
94
- .setup({
95
- pointerEventsCheck: PointerEventsCheckLevel.Never,
96
- })
97
- .click(card);
98
- expect(mockCardClick).not.toHaveBeenCalled();
99
- });
100
-
101
- it("disabled cards do not respond to keyboard events", async () => {
102
- const { user } = render(
103
- <Card role="button" onClick={mockCardClick} disabled={true}>
104
- Test
105
- </Card>
106
- );
107
-
108
- const card = screen.getByRole("button");
109
-
110
- // Try to focus the card by tabbing
111
- await user.tab();
112
-
113
- // Disabled cards should not receive focus
114
- expect(card).not.toHaveFocus();
115
-
116
- // Even if we force focus (for testing), Enter key should not trigger onClick
117
- card.focus();
118
- await user.keyboard("{Enter}");
119
- expect(mockCardClick).not.toHaveBeenCalled();
120
- });
121
-
122
- it("should have an adjustable hover state style for interactive cards", async () => {
123
- const { user } = render(
124
- <Card role="button" onClick={mockCardClick} elevation="high">
125
- Test
126
- </Card>
127
- );
128
-
129
- const card = screen.getByRole("button");
130
-
131
- // apparently, jest-dom can't do this with toHaveStyle
132
- // https://github.com/testing-library/jest-dom/issues/59
133
- //
134
- // have to use toHaveStyleRule from jest-styled-comps
135
- // https://github.com/styled-components/jest-styled-components#tohavestylerule
136
- await user.hover(card);
137
- expect(card).toHaveStyleRule("box-shadow", theme.shadows.high, {
138
- modifier: '&[role="button"]:hover',
139
- });
140
- });
141
-
142
- it("can be focused when interactive", async () => {
143
- const { user } = render(
144
- <Card role="button" onClick={mockCardClick}>
145
- Test
146
- <button>child click test</button>
147
- </Card>
148
- );
149
-
150
- const [card, button] = screen.getAllByRole("button");
151
-
152
- expect(card).toHaveAttribute("tabindex", "0");
153
-
154
- await user.tab();
155
- expect(card).toHaveFocus();
156
-
157
- await user.tab();
158
- expect(button).toHaveFocus();
159
-
160
- await user.tab({ shift: true });
161
- expect(card).toHaveFocus();
162
- });
163
-
164
- it('handles onKeyDown "enter"', async () => {
165
- const { rerender, debug, user } = render(
166
- <Card role="button" onClick={mockCardClick}>
167
- Test
168
- </Card>
169
- );
170
-
171
- debug;
172
-
173
- const cardAsButton = screen.getByRole("button");
174
-
175
- await user.tab();
176
- expect(cardAsButton).toHaveFocus();
177
-
178
- await user.type(cardAsButton, "{enter}");
179
- expect(mockCardClick).toBeCalledTimes(1);
180
-
181
- rerender(
182
- <Card role="link" href="https://sproutsocial.com/">
183
- Hello
184
- <CardLink>Test</CardLink>
185
- </Card>
186
- );
187
-
188
- const cardAsLink = screen.getByText("Test");
189
-
190
- await user.tab();
191
- expect(cardAsLink).toHaveFocus();
192
- expect(cardAsLink).toHaveAttribute("href", "https://sproutsocial.com/");
193
- });
194
-
195
- it("is selectable", async () => {
196
- const TestCheckboxCard = () => {
197
- const [selected, setSelected] = React.useState<boolean>(false);
198
- return (
199
- <Card
200
- role="checkbox"
201
- onClick={() => setSelected(!selected)}
202
- selected={selected}
203
- >
204
- Test
205
- </Card>
206
- );
207
- };
208
-
209
- const { user } = render(<TestCheckboxCard />);
210
-
211
- const card = screen.getByRole("checkbox");
212
- await user.click(card);
213
-
214
- await waitFor(() => expect(screen.getByRole("checkbox")).toBeChecked());
215
- expect(card).toHaveStyle(
216
- `border: ${theme.borderWidths[500]} solid ${theme.colors.container.border.selected}`
217
- );
218
- });
219
-
220
- it("should support interactive children in button cards", async () => {
221
- const mockChildClick = jest.fn((e) => e.stopPropagation());
222
-
223
- const { user } = render(
224
- <Card role="button" onClick={mockCardClick}>
225
- Test
226
- <button onClick={mockChildClick}>child click test</button>
227
- </Card>
228
- );
229
-
230
- const buttons = screen.getAllByRole("button");
231
- const card = buttons[0]!;
232
- const cardChild = buttons[1]!;
233
-
234
- // Expect clicking the card to trigger the card's onClick but not the child's
235
- await user.click(card);
236
- expect(mockCardClick).toBeCalledTimes(1);
237
- expect(mockChildClick).toBeCalledTimes(0);
238
-
239
- // Expect clicking the interactive child NOT to trigger the card's onClick.
240
- await user.click(cardChild);
241
- expect(mockCardClick).toBeCalledTimes(1);
242
- expect(mockChildClick).toBeCalledTimes(1);
243
- });
244
-
245
- it("presentation cards are not interactive but support interactive children", async () => {
246
- const mockChildClick = jest.fn();
247
-
248
- const { user } = render(
249
- <Card role="presentation">
250
- Test
251
- <button onClick={mockChildClick}>child click test</button>
252
- </Card>
253
- );
254
-
255
- const card = screen.getByRole("presentation");
256
- const cardChild = screen.getByText("child click test");
257
-
258
- // Presentation cards should not be focusable
259
- expect(card).toHaveAttribute("tabindex", "-1");
260
-
261
- // Interactive children should still work
262
- await user.click(cardChild);
263
- expect(mockChildClick).toBeCalledTimes(1);
264
- });
265
- });
266
-
267
- describe("A Card supports composable layouts", () => {
268
- it("should support children", () => {
269
- render(
270
- <Card role="presentation">
271
- Hello, world!
272
- <Badge>Cool Badge</Badge>
273
- </Card>
274
- );
275
-
276
- const parent = screen.getByRole("presentation");
277
- const child = screen.getByText("Cool Badge");
278
-
279
- expect(parent).toContainElement(child);
280
- });
281
-
282
- it("should accept system props", () => {
283
- render(
284
- <Card role="presentation" p={300}>
285
- Hello, world!
286
- </Card>
287
- );
288
-
289
- const card = screen.getByRole("presentation");
290
- expect(card).toHaveStyle(`padding: ${theme.space[300]}`);
291
- });
292
-
293
- it("adjusts it's styles dynamically when subcomponents are present", () => {
294
- render(
295
- <Card role="presentation">
296
- <CardHeader>Card Header</CardHeader>
297
- <CardContent>CardContent</CardContent>
298
- <CardFooter>CardFooter</CardFooter>
299
- </Card>
300
- );
301
-
302
- const card = screen.getByRole("presentation");
303
- const cardSubcomponent = screen.getByText("CardFooter");
304
-
305
- expect(cardSubcomponent).toBeInTheDocument();
306
- // We have to wait for the new classes to be set once the state changes. Not sure how to do this better but a function seems to work.
307
- () => expect(card).toHaveStyle("padding: 0px");
308
- });
309
- });
310
-
311
- afterEach(() => {
312
- jest.clearAllMocks();
313
- });
@@ -1,78 +0,0 @@
1
- import * as React from "react";
2
- import Card from "..";
3
-
4
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
- function CardTypes() {
6
- return (
7
- <>
8
- <Card role="link" href="https://sproutsocial.com/">
9
- This is a card.
10
- </Card>
11
- {/* @ts-expect-error - test that href is required with role=link */}
12
- <Card role="link">This is a card.</Card>
13
- {/* @ts-expect-error - test that onClick is never allowed with role=link */}
14
- <Card
15
- role="link"
16
- onClick={() => {
17
- return;
18
- }}
19
- >
20
- This is a card.
21
- </Card>
22
- <Card
23
- role="button"
24
- onClick={() => {
25
- return;
26
- }}
27
- >
28
- This is a card.
29
- </Card>
30
- {/* @ts-expect-error - test that onClick is required with role=button */}
31
- <Card role="button">This is a card.</Card>
32
- <Card
33
- role="checkbox"
34
- selected={true}
35
- onClick={() => {
36
- return;
37
- }}
38
- >
39
- This is a card.
40
- </Card>
41
- {/* @ts-expect-error - test that select is required with role=checkbox */}
42
- <Card
43
- role="checkbox"
44
- onClick={() => {
45
- return;
46
- }}
47
- >
48
- This is a card.
49
- </Card>
50
- {/* @ts-expect-error - test that onClick is required with role=checkbox */}
51
- <Card role="checkbox" selected={true}>
52
- This is a card.
53
- </Card>
54
- <Card role="presentation">This is a card.</Card>
55
- {/* @ts-expect-error - test that onClick is never allowed with role=presentation */}
56
- <Card
57
- role="presentation"
58
- onClick={() => {
59
- return;
60
- }}
61
- >
62
- This is a card.
63
- </Card>
64
- {/* @ts-expect-error - test that href is never allowed with role=presentation */}
65
- <Card role="presentation" href="">
66
- This is a card.
67
- </Card>
68
- {/* @ts-expect-error - test that selected is never allowed with role=presentation */}
69
- <Card role="presentation" disabled={true} selected={true}>
70
- This is a card.
71
- </Card>
72
- {/* @ts-expect-error - test that consumer level transient props fail */}
73
- <Card role="presentation" $disabled={true} $selected={true}>
74
- This is a card.
75
- </Card>
76
- </>
77
- );
78
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- import Card from "./Card";
2
-
3
- export default Card;
4
- export { Card };
5
- export { CardHeader, CardContent, CardFooter, CardLink } from "./subComponents";
6
- export * from "./CardTypes";
package/src/styles.tsx DELETED
@@ -1,176 +0,0 @@
1
- import styled from "styled-components";
2
- import {
3
- border,
4
- color,
5
- flexbox,
6
- grid,
7
- layout,
8
- position,
9
- space,
10
- typography,
11
- } from "styled-system";
12
- import { focusRing, disabled } from "@sproutsocial/seeds-react-mixins";
13
- import type {
14
- TypeStyledCard,
15
- TypeCardArea,
16
- TypeStyledSelectedIcon,
17
- TypeCardLink,
18
- } from "./CardTypes";
19
- import Icon from "@sproutsocial/seeds-react-icon";
20
-
21
- // TODO: Would be really cool to cherry pick specific props from style functions. For example,
22
- // removing the css prop 'color' from the color function or importing just the specific
23
- // props the component needs. It appears to be possible with some and not others.
24
- // https://github.com/styled-system/styled-system/issues/1569
25
-
26
- export const StyledCardContent = styled.div<TypeCardArea>`
27
- display: flex;
28
- flex-direction: column;
29
- padding: ${({ theme }) => theme.space[400]};
30
- box-sizing: border-box;
31
-
32
- ${border}
33
- ${color}
34
- ${flexbox}
35
- ${grid}
36
- ${layout}
37
- ${space}
38
- `;
39
-
40
- export const StyledCardHeader = styled(StyledCardContent)`
41
- flex-direction: row;
42
- border-bottom: ${({ theme }) => `${theme.borderWidths[500]} solid
43
- ${theme.colors.container.border.base}`};
44
- border-top-left-radius: ${({ theme }) => theme.radii.inner};
45
- border-top-right-radius: ${({ theme }) => theme.radii.inner};
46
-
47
- ${border}
48
- ${color}
49
- ${flexbox}
50
- ${grid}
51
- ${layout}
52
- ${space}
53
- `;
54
-
55
- export const StyledCardFooter = styled(StyledCardContent)`
56
- flex-direction: row;
57
- border-top: ${({ theme }) => `${theme.borderWidths[500]} solid
58
- ${theme.colors.container.border.base}`};
59
- border-bottom-left-radius: ${({ theme }) => theme.radii.inner};
60
- border-bottom-right-radius: ${({ theme }) => theme.radii.inner};
61
-
62
- ${border}
63
- ${color}
64
- ${flexbox}
65
- ${grid}
66
- ${layout}
67
- ${space}
68
- `;
69
-
70
- export const SelectedIconWrapper = styled.div`
71
- display: flex;
72
- align-items: center;
73
- justify-content: center;
74
- position: absolute;
75
- top: -8px;
76
- right: -8px;
77
- `;
78
-
79
- export const StyledSelectedIcon = styled(Icon)<TypeStyledSelectedIcon>`
80
- border-radius: 50%;
81
- background: ${({ theme }) => theme.colors.container.background.base};
82
- opacity: 0;
83
- transition: opacity ${({ theme }) => theme.duration.medium};
84
-
85
- ${({ $selected }) =>
86
- $selected &&
87
- `
88
- opacity: 1;
89
- `}
90
- `;
91
-
92
- export const StyledCardLink = styled.a<TypeCardLink>`
93
- font-family: ${(p) => p.theme.fontFamily};
94
- font-weight: ${(p) => p.theme.fontWeights.bold};
95
- color: ${(p) => p.theme.colors.text.headline};
96
- ${(p) => p.theme.typography[400]};
97
-
98
- ${color}
99
- ${typography}
100
- `;
101
-
102
- export const StyledCard = styled.div<TypeStyledCard>`
103
- position: relative;
104
- display: flex;
105
- flex-direction: column;
106
- box-sizing: border-box;
107
- margin: 0;
108
- background: ${({ theme }) => theme.colors.container.background.base};
109
- border: ${({ theme }) => theme.borderWidths[500]} solid
110
- ${({ theme }) => theme.colors.container.border.base};
111
- padding: ${({ theme, $compositionalComponents }) =>
112
- $compositionalComponents ? 0 : theme.space[400]};
113
- border-radius: ${({ theme }) => theme.radii.outer};
114
- transition: box-shadow ${({ theme }) => theme.duration.medium},
115
- border ${({ theme }) => theme.duration.medium};
116
-
117
- &[role="button"],
118
- &[role="checkbox"] {
119
- cursor: pointer;
120
-
121
- &:hover {
122
- box-shadow: ${({ theme, $elevation = "low" }) =>
123
- theme.shadows[$elevation]};
124
- }
125
- }
126
-
127
- ${({ $isRoleLink, theme, $elevation = "low" }) =>
128
- $isRoleLink &&
129
- `
130
- cursor: pointer;
131
-
132
- &:hover {
133
- box-shadow: ${theme.shadows[$elevation]};
134
- }
135
- `}
136
-
137
- &:focus-within {
138
- ${({ $isRoleLink }) => ($isRoleLink ? focusRing : null)}
139
- ${StyledCardLink}:focus {
140
- border: none;
141
- box-shadow: none;
142
- outline: none;
143
- }
144
- }
145
-
146
- &:focus {
147
- ${focusRing}
148
- }
149
-
150
- ${({ $disabled }) =>
151
- $disabled &&
152
- `
153
- ${disabled}
154
- `}
155
-
156
- ${({ $selected, theme }) =>
157
- $selected &&
158
- `
159
- border: ${theme.borderWidths[500]} solid ${theme.colors.container.border.selected};
160
- `}
161
-
162
- ${border}
163
- ${color}
164
- ${flexbox}
165
- ${grid}
166
- ${layout}
167
- ${position}
168
- ${space}
169
- `;
170
-
171
- export const StyledCardAffordance = styled(Icon)`
172
- ${StyledCard}:hover & {
173
- transform: translateX(${(p) => p.theme.space[200]});
174
- }
175
- transition: ${(p) => p.theme.duration.medium};
176
- `;
@@ -1,110 +0,0 @@
1
- import React, { useContext } from "react";
2
- import { useChildContext, SubComponentContext } from "./utils";
3
- import type {
4
- TypeCardLink,
5
- TypeSharedCardSystemProps,
6
- TypeStyledSelectedIcon,
7
- } from "./CardTypes";
8
- import {
9
- StyledCardContent,
10
- StyledCardHeader,
11
- StyledCardFooter,
12
- StyledSelectedIcon,
13
- SelectedIconWrapper,
14
- StyledCardAffordance,
15
- StyledCardLink,
16
- } from "./styles";
17
-
18
- interface TypeSharedSubComponentProps extends TypeSharedCardSystemProps {
19
- children?: React.ReactNode;
20
- }
21
-
22
- export const CardContent = ({
23
- children,
24
- ...rest
25
- }: TypeSharedSubComponentProps) => {
26
- // TODO: It could be cool to possibly adjust the context to include an array of names of child components.
27
- // Then, if CardHeader or CardFooter aren't used with CardContent throw an error.
28
- useChildContext();
29
- return <StyledCardContent {...rest}>{children}</StyledCardContent>;
30
- };
31
-
32
- export const CardHeader = ({
33
- children,
34
- ...rest
35
- }: TypeSharedSubComponentProps) => {
36
- useChildContext();
37
- return <StyledCardHeader {...rest}>{children}</StyledCardHeader>;
38
- };
39
-
40
- export const CardFooter = ({
41
- children,
42
- ...rest
43
- }: TypeSharedSubComponentProps) => {
44
- useChildContext();
45
- return <StyledCardFooter {...rest}>{children}</StyledCardFooter>;
46
- };
47
-
48
- interface TypeSelectedIconProps {
49
- $selected?: TypeStyledSelectedIcon["$selected"];
50
- }
51
-
52
- export const SelectedIcon = ({ $selected }: TypeSelectedIconProps) => {
53
- return (
54
- <SelectedIconWrapper>
55
- <StyledSelectedIcon
56
- aria-hidden
57
- color="icon.base"
58
- name="circle-check-solid"
59
- $selected={$selected}
60
- />
61
- </SelectedIconWrapper>
62
- );
63
- };
64
-
65
- export const CardAffordance = ({ ...rest }) => {
66
- return (
67
- <StyledCardAffordance
68
- {...rest}
69
- size="mini"
70
- name="arrow-right-solid"
71
- // TODO: probably need to make this available to the top level for external links https://sprout.atlassian.net/browse/DS-2223
72
- aria-hidden
73
- />
74
- );
75
- };
76
-
77
- export const CardLink = ({
78
- affordance,
79
- children,
80
- external = false,
81
- color,
82
- ...rest
83
- }: React.PropsWithChildren<TypeCardLink>) => {
84
- const { href, linkRef } = useContext(SubComponentContext);
85
-
86
- // Because we are hijacking Card click event to directly click this link, we need to stop propagation to avoid a double click event.
87
- const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
88
- e.stopPropagation();
89
- };
90
-
91
- return (
92
- <StyledCardLink
93
- {...rest}
94
- target={external ? "_blank" : undefined}
95
- rel={external ? "noreferrer" : undefined}
96
- href={href}
97
- onClick={handleClick}
98
- ref={linkRef}
99
- // TODO: fix this type since `color` should be valid here. TS can't resolve the correct type.
100
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
101
- // @ts-ignore
102
- color={color}
103
- >
104
- <>
105
- {children}
106
- {affordance ? <CardAffordance ml={300} /> : null}
107
- </>
108
- </StyledCardLink>
109
- );
110
- };