@sproutsocial/seeds-react-card 1.0.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.
@@ -0,0 +1,389 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Avatar } from "@sproutsocial/seeds-react-avatar";
3
+ import { Banner } from "@sproutsocial/seeds-react-banner";
4
+ import { Box } from "@sproutsocial/seeds-react-box";
5
+ import { Card } from "./";
6
+ import { Icon } from "@sproutsocial/seeds-react-icon";
7
+ import { Link } from "@sproutsocial/seeds-react-link";
8
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
9
+ import type { EnumIllustrationNames } from "@sproutsocial/seeds-illustrations";
10
+ import { SpotIllustration } from "@sproutsocial/seeds-react-spot-illustration";
11
+ import { Text } from "@sproutsocial/seeds-react-text";
12
+ import styled from "styled-components";
13
+ import { CardHeader, CardContent, CardFooter, CardLink } from "./subComponents";
14
+ import type { Meta, StoryObj } from "@storybook/react";
15
+
16
+ const meta: Meta<typeof Card> = {
17
+ title: "Components/Card",
18
+ component: Card,
19
+ argTypes: {
20
+ elevation: {
21
+ options: ["low", "medium", "high", undefined],
22
+ control: { type: "select" },
23
+ },
24
+ disabled: {
25
+ control: "boolean",
26
+ },
27
+ selected: {
28
+ control: "boolean",
29
+ },
30
+ },
31
+ args: {
32
+ elevation: undefined,
33
+ disabled: false,
34
+ selected: false,
35
+ },
36
+ };
37
+
38
+ export default meta;
39
+
40
+ type Story = StoryObj<typeof Card>;
41
+
42
+ export const AsLink: Story = {
43
+ render: (args) => (
44
+ <Card width={1 / 4} {...args}>
45
+ <Box display="flex" justifyContent="center" alignItems="center" my={400}>
46
+ <SpotIllustration
47
+ height="200px"
48
+ name="calendar-reporting"
49
+ aria-hidden
50
+ />
51
+ </Box>
52
+ <Text.SmallByline>Reporting</Text.SmallByline>
53
+ <CardLink external affordance>
54
+ Visit calendar report
55
+ </CardLink>
56
+ </Card>
57
+ ),
58
+ args: {
59
+ role: "link",
60
+ href: "https://seeds.sproutsocial.com/",
61
+ },
62
+ };
63
+
64
+ export const AsButton: Story = {
65
+ render: () => {
66
+ const [message, setMessage] = useState(false);
67
+
68
+ const _onClick = () => {
69
+ setMessage(true);
70
+ };
71
+
72
+ useEffect(() => {
73
+ const showMessage = setTimeout(() => {
74
+ setMessage(false);
75
+ }, 4000);
76
+
77
+ return function cleanup() {
78
+ clearTimeout(showMessage);
79
+ };
80
+ }, [message]);
81
+
82
+ return (
83
+ <>
84
+ {message ? (
85
+ <Box mb={400}>
86
+ <Banner text="Your click was successful." />
87
+ </Box>
88
+ ) : (
89
+ <Box mb={400}>
90
+ <Banner
91
+ type="warning"
92
+ text={
93
+ <Box
94
+ width={1}
95
+ display="flex"
96
+ alignItems="center"
97
+ justifyContent="space-between"
98
+ >
99
+ <Text>
100
+ Avoid nesting interactive content inside a Card with
101
+ role='button'
102
+ </Text>
103
+ <Text>
104
+ <Link href="https://html.spec.whatwg.org/multipage/dom.html#interactive-content">
105
+ Learn more
106
+ <Icon
107
+ ml={200}
108
+ size="mini"
109
+ name="arrow-right-up-solid"
110
+ role={undefined}
111
+ svgProps={{
112
+ role: "img",
113
+ "aria-label": "opens in a new tab",
114
+ }}
115
+ />
116
+ </Link>
117
+ </Text>
118
+ </Box>
119
+ }
120
+ />
121
+ </Box>
122
+ )}
123
+ <Card role="button" onClick={_onClick} width={1 / 4}>
124
+ <Box mx="auto" mb={400}>
125
+ <PartnerLogo py={450} partnerName="facebook" size="jumbo" />
126
+ </Box>
127
+ <Text.SmallByline>Profile connect</Text.SmallByline>
128
+ <Text.Headline mb={300}>Facebook</Text.Headline>
129
+ <Text.BodyCopy>Click to connect your facebook account.</Text.BodyCopy>
130
+ </Card>
131
+ </>
132
+ );
133
+ },
134
+ };
135
+
136
+ export const AsPresentation: Story = {
137
+ render: (args) => (
138
+ <Card width={1 / 4} {...args}>
139
+ <Text.SmallByline>Analytics</Text.SmallByline>
140
+ <Text.Headline mb={300}>Google Analytics</Text.Headline>
141
+ <Box mx="auto" my={400}>
142
+ <PartnerLogo py={450} partnerName="google-analytics" size="jumbo" />
143
+ </Box>
144
+ <Text.BodyCopy mb={400}>
145
+ Learn more about how we track analytics.
146
+ </Text.BodyCopy>
147
+ <Link fontSize={300} href="https://google.com/" external>
148
+ Visit google
149
+ <Icon
150
+ mb={100}
151
+ ml={300}
152
+ size="mini"
153
+ name="arrow-right-outline"
154
+ role={undefined}
155
+ svgProps={{ role: "img", "aria-label": "opens in a new tab" }}
156
+ />
157
+ </Link>
158
+ </Card>
159
+ ),
160
+ args: {
161
+ role: "presentation",
162
+ },
163
+ };
164
+
165
+ export const AsButtonDisabled: Story = {
166
+ render: () => {
167
+ const [message, setMessage] = useState(false);
168
+
169
+ const _onClick = () => {
170
+ setMessage(true);
171
+ };
172
+
173
+ useEffect(() => {
174
+ const showMessage = setTimeout(() => {
175
+ setMessage(false);
176
+ }, 4000);
177
+
178
+ return function cleanup() {
179
+ clearTimeout(showMessage);
180
+ };
181
+ }, [message]);
182
+
183
+ return (
184
+ <>
185
+ {message ? (
186
+ <Box mb={400}>
187
+ <Banner text="Your click was successful." />
188
+ </Box>
189
+ ) : (
190
+ <Box mb={400}>
191
+ <Banner
192
+ type="warning"
193
+ text={
194
+ <Box
195
+ width={1}
196
+ display="flex"
197
+ alignItems="center"
198
+ justifyContent="space-between"
199
+ >
200
+ <Text>
201
+ Avoid nesting interactive content inside a Card with
202
+ role='button'
203
+ </Text>
204
+ <Text>
205
+ <Link href="https://html.spec.whatwg.org/multipage/dom.html#interactive-content">
206
+ Learn more
207
+ <Icon
208
+ ml={200}
209
+ size="mini"
210
+ name="arrow-right-up-solid"
211
+ role={undefined}
212
+ svgProps={{
213
+ role: "img",
214
+ "aria-label": "opens in a new tab",
215
+ }}
216
+ />
217
+ </Link>
218
+ </Text>
219
+ </Box>
220
+ }
221
+ />
222
+ </Box>
223
+ )}
224
+ <Card role="button" disabled={true} onClick={_onClick} width={1 / 4}>
225
+ <Box mx="auto" mb={400}>
226
+ <SpotIllustration height="200px" name="coffee-cup" />
227
+ </Box>
228
+ <Text.SmallByline>SEEDS WEB</Text.SmallByline>
229
+ <Text.Headline mb={300}>Card component</Text.Headline>
230
+ <Text.BodyCopy>
231
+ The card component is a styled primitive container.
232
+ </Text.BodyCopy>
233
+ </Card>
234
+ </>
235
+ );
236
+ },
237
+ };
238
+
239
+ export const ConfigurableShadow: Story = {
240
+ render: (args) => (
241
+ <Card width={1 / 4} {...args}>
242
+ <Text.SmallByline>SEEDS WEB</Text.SmallByline>
243
+ <Text.Headline mb={300}>Card component</Text.Headline>
244
+ <Box mx="auto" my={400}>
245
+ <SpotIllustration height="200px" name="coffee-cup" />
246
+ </Box>
247
+ <Text.BodyCopy mb={400}>
248
+ The card component is a styled primitive container.
249
+ </Text.BodyCopy>
250
+ <Link fontSize={300} href="https://google.com/" external>
251
+ Use now
252
+ <Icon
253
+ mb={100}
254
+ ml={300}
255
+ size="mini"
256
+ name="arrow-right-up-solid"
257
+ role={undefined}
258
+ svgProps={{ role: "img", "aria-label": "opens in a new tab" }}
259
+ />
260
+ </Link>
261
+ </Card>
262
+ ),
263
+ args: {
264
+ role: "presentation",
265
+ elevation: "high",
266
+ },
267
+ };
268
+
269
+ export const SelectableComposition: Story = {
270
+ render: () => {
271
+ const [selected, setSelected] = useState(false);
272
+ const toggle = () => setSelected(!selected);
273
+
274
+ return (
275
+ <Card role="checkbox" selected={selected} onClick={toggle} width={1 / 3}>
276
+ <CardContent my={450} mx="auto">
277
+ <PartnerLogo partnerName="tiktok" size="jumbo" />
278
+ </CardContent>
279
+ <CardFooter flexDirection="column">
280
+ <Text.SmallSubHeadline>TikTok</Text.SmallSubHeadline>
281
+ <Text.SmallBodyCopy>
282
+ Select TikTok to start sharing content today.
283
+ </Text.SmallBodyCopy>
284
+ </CardFooter>
285
+ </Card>
286
+ );
287
+ },
288
+ };
289
+
290
+ export const CompositionUtilities: Story = {
291
+ render: () => (
292
+ <Card role="presentation">
293
+ <CardHeader>
294
+ <Avatar appearance="leaf" size="24px" name="Card Header" />
295
+ <Text.SubHeadline ml={300}>This is a card header</Text.SubHeadline>
296
+ </CardHeader>
297
+ <CardContent>
298
+ <Text.SmallBodyCopy as="p">
299
+ We can use the CardContent component to render cool content as seen
300
+ here:
301
+ </Text.SmallBodyCopy>
302
+ <Text.SmallBodyCopy as="p">
303
+ "Hi, Nard Dawg. I'm Lou Peachum!"
304
+ </Text.SmallBodyCopy>
305
+ </CardContent>
306
+ <CardFooter>
307
+ <Link external href="https://google.com">
308
+ Action in CardFooter
309
+ <Icon
310
+ ml={200}
311
+ size="mini"
312
+ name="arrow-right-up-solid"
313
+ role={undefined}
314
+ svgProps={{ role: "img", "aria-label": "opens in a new tab" }}
315
+ />
316
+ </Link>
317
+ </CardFooter>
318
+ </Card>
319
+ ),
320
+ };
321
+
322
+ export const Cards: Story = {
323
+ render: () => {
324
+ const cards: {
325
+ subhead: string;
326
+ reportName: string;
327
+ image: EnumIllustrationNames;
328
+ href: string;
329
+ }[] = [
330
+ {
331
+ subhead: "Reporting",
332
+ reportName: "Profile performance",
333
+ image: "reporting",
334
+ href: "https://app.sproutsocial.com/reports/group_report",
335
+ },
336
+ {
337
+ subhead: "Reporting",
338
+ reportName: "Post performance",
339
+ image: "reporting-folder",
340
+ href: "https://app.sproutsocial.com/reports/post_performance",
341
+ },
342
+ {
343
+ subhead: "Reporting",
344
+ reportName: "Tag performance",
345
+ image: "analytics-offering",
346
+ href: "https://app.sproutsocial.com/reports/tag_performance",
347
+ },
348
+ {
349
+ subhead: "Reporting",
350
+ reportName: "Paid performance",
351
+ image: "listening-tour",
352
+ href: "https://app.sproutsocial.com/reports/cross_network_campaign_performance",
353
+ },
354
+ ];
355
+
356
+ const StyledList = styled.ul`
357
+ list-style: none;
358
+ display: flex;
359
+ flex-wrap: wrap;
360
+ `;
361
+
362
+ return (
363
+ <StyledList>
364
+ {cards.map((card) => (
365
+ <li key={card.href}>
366
+ <Card role="link" href={card.href} width="275px" mr={400} mb={400}>
367
+ <Box
368
+ display="flex"
369
+ justifyContent="center"
370
+ alignItems="center"
371
+ my={400}
372
+ >
373
+ <SpotIllustration
374
+ height="200px"
375
+ name={card.image}
376
+ aria-hidden
377
+ />
378
+ </Box>
379
+ <Text.SmallByline>{card.subhead}</Text.SmallByline>
380
+ <CardLink external affordance>
381
+ Visit {card.reportName} report
382
+ </CardLink>
383
+ </Card>
384
+ </li>
385
+ ))}
386
+ </StyledList>
387
+ );
388
+ },
389
+ };
package/src/Card.tsx ADDED
@@ -0,0 +1,73 @@
1
+ import React, { useRef, useState } from "react";
2
+ import { StyledCard } from "./styles";
3
+ import type { TypeCardProps, TypeCardContext } from "./CardTypes";
4
+ import { SubComponentContext, onKeyDown } from "./utils";
5
+ import { SelectedIcon } from "./subComponents";
6
+
7
+ /**
8
+ * @link https://seeds.sproutsocial.com/components/card/
9
+ *
10
+ * Avoid nesting interactive content inside a Card with role='button'.
11
+ *
12
+ * Interactive content: "a", "audio", "button", "embed", "iframe", "img", "input", "label", "select", "textarea", "video"
13
+ * @see https://html.spec.whatwg.org/multipage/dom.html#interactive-content
14
+ *
15
+ * @example
16
+ * <Card role="button" onClick={_onClick}>
17
+ * <Button>Click me</Button>
18
+ * </Card>
19
+ */
20
+
21
+ const Card = ({
22
+ children,
23
+ disabled = false,
24
+ elevation,
25
+ href,
26
+ onClick,
27
+ role = "presentation",
28
+ selected,
29
+ ...rest
30
+ }: TypeCardProps) => {
31
+ const [hasSubComponent, setHasSubComponent] = useState<boolean>(false);
32
+ const containerRef = useRef<HTMLDivElement>(null);
33
+ const linkRef = useRef<HTMLAnchorElement>(null);
34
+ const isRoleLink = role === "link";
35
+ const checkedConditions = role === "checkbox" ? selected : undefined;
36
+
37
+ const cardContext: TypeCardContext = {
38
+ setHasSubComponent: setHasSubComponent,
39
+ href: href,
40
+ linkRef: linkRef,
41
+ };
42
+
43
+ const handleClickConditions: React.MouseEventHandler = (e) =>
44
+ isRoleLink ? linkRef.current?.click() : onClick?.(e);
45
+
46
+ const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) =>
47
+ onKeyDown({ e, href, onClick, ref: containerRef, role });
48
+
49
+ return (
50
+ <StyledCard
51
+ tabIndex={isRoleLink ? -1 : 0}
52
+ role={isRoleLink ? undefined : role}
53
+ onClick={handleClickConditions}
54
+ onKeyDown={handleKeyDown}
55
+ $elevation={elevation}
56
+ ref={containerRef}
57
+ $selected={selected}
58
+ aria-checked={checkedConditions}
59
+ $disabled={disabled}
60
+ aria-disabled={disabled && disabled}
61
+ $compositionalComponents={hasSubComponent}
62
+ $isRoleLink={isRoleLink}
63
+ {...rest}
64
+ >
65
+ <SelectedIcon $selected={selected} />
66
+ <SubComponentContext.Provider value={cardContext}>
67
+ {children}
68
+ </SubComponentContext.Provider>
69
+ </StyledCard>
70
+ );
71
+ };
72
+
73
+ export default Card;
@@ -0,0 +1,104 @@
1
+ import type { TypeIconProps } from "@sproutsocial/seeds-react-icon";
2
+ import * as React from "react";
3
+ import type { TypeStyledComponentsCommonProps } from "@sproutsocial/seeds-react-system-props";
4
+ import type {
5
+ TypeBorderSystemProps,
6
+ TypeColorSystemProps,
7
+ TypeFlexboxSystemProps,
8
+ TypeGridSystemProps,
9
+ TypeLayoutSystemProps,
10
+ TypePositionSystemProps,
11
+ TypeSpaceSystemProps,
12
+ TypeTypographySystemProps,
13
+ } from "@sproutsocial/seeds-react-system-props";
14
+
15
+ export interface TypeSharedCardSystemProps
16
+ extends Omit<React.ComponentPropsWithoutRef<"div">, "color">,
17
+ TypeStyledComponentsCommonProps,
18
+ TypeBorderSystemProps,
19
+ TypeColorSystemProps,
20
+ TypeFlexboxSystemProps,
21
+ TypeGridSystemProps,
22
+ TypeLayoutSystemProps,
23
+ TypePositionSystemProps,
24
+ TypeSpaceSystemProps {}
25
+
26
+ // consumer facing props that affect the styles of the component. We need to define these first so the user doesn't see our transient naming conventions.
27
+ export interface TypeCardStyleProps {
28
+ elevation?: "low" | "medium" | "high";
29
+ disabled?: boolean;
30
+ compositionalComponents?: boolean;
31
+ selected?: boolean;
32
+ isRoleLink?: boolean;
33
+ }
34
+
35
+ // Since we only want to manage the style props in one place(above), we'll use this generic to prepend the properties of TypeCardStyleProps with $.
36
+ export type TypeStyleTransientProps<T> = {
37
+ [K in Extract<keyof T, string> as `$${K}`]: T[K];
38
+ };
39
+
40
+ export type TypeCardStyleTransientProps =
41
+ TypeStyleTransientProps<TypeCardStyleProps>;
42
+
43
+ export interface TypeStyledCard
44
+ extends TypeSharedCardSystemProps,
45
+ TypeCardStyleTransientProps {}
46
+
47
+ export interface TypeCardStyles
48
+ extends TypeSharedCardSystemProps,
49
+ Omit<TypeCardStyleProps, "compositionalComponents"> {}
50
+
51
+ type TypeOnClick = (event: React.MouseEvent | React.KeyboardEvent) => void;
52
+
53
+ export type TypeCardConditions =
54
+ | {
55
+ role: "link";
56
+ href: string;
57
+ onClick?: never;
58
+ }
59
+ | {
60
+ role: "button";
61
+ href?: never;
62
+ onClick: TypeOnClick;
63
+ }
64
+ | {
65
+ role: "checkbox";
66
+ href?: never;
67
+ onClick: TypeOnClick;
68
+ selected: boolean;
69
+ }
70
+ | {
71
+ role: "presentation";
72
+ href?: never;
73
+ /**
74
+ * **Warning:**
75
+ * `role='presentation'` is outside of the accessiblity tree. Using an `onClick` that performs a user action should likely be used
76
+ * with `role='button'` instead.
77
+ */
78
+ onClick?: TypeOnClick;
79
+ };
80
+
81
+ export type TypeCardProps = TypeCardConditions &
82
+ React.PropsWithChildren<TypeCardStyles>;
83
+
84
+ export interface TypeCardArea extends TypeSharedCardSystemProps {
85
+ $divider?: "top" | "bottom";
86
+ }
87
+
88
+ export interface TypeStyledSelectedIcon extends TypeIconProps {
89
+ $selected?: TypeCardStyleTransientProps["$selected"];
90
+ }
91
+
92
+ export interface TypeCardContext {
93
+ setHasSubComponent?: React.Dispatch<React.SetStateAction<boolean>>;
94
+ href?: string;
95
+ linkRef: React.RefObject<HTMLAnchorElement> | null;
96
+ }
97
+
98
+ export interface TypeCardLink
99
+ extends Omit<React.ComponentPropsWithoutRef<"a">, "color">,
100
+ TypeColorSystemProps,
101
+ TypeTypographySystemProps {
102
+ affordance?: boolean;
103
+ external?: boolean;
104
+ }