Skip to content

Commit 667496f

Browse files
LG-2720: Card => Polymorphic (#2395)
* make card props polymorphic * with changeset * with changes to poly * do we need to require an href * Update README.md * another attempt * lint errors * changeset * changeset again * rm unused dep * WIP * mroe wip * tests passing * validate and fix * rm box from story --------- Co-authored-by: Adam Thompson <[email protected]> Co-authored-by: Adam Thompson <[email protected]>
1 parent 5fb40f5 commit 667496f

File tree

16 files changed

+101
-54
lines changed

16 files changed

+101
-54
lines changed

Diff for: .changeset/beige-teachers-leave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/card': major
3+
---
4+
5+
Updates Card component to leverage `@leafygreen-ui/polymorphic` rather than deprecated Box component for handling polymorphic behavior

Diff for: .changeset/curly-hairs-lick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/polymorphic': patch
3+
---
4+
5+
Adds more type safety around nested Polymorphic components accepting refs

Diff for: .changeset/green-hounds-fold.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@lg-chat/rich-links': minor
3+
---
4+
5+
Updates types to support updated Card component

Diff for: chat/rich-links/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@leafygreen-ui/leafygreen-provider": "^3.1.12",
2222
"@leafygreen-ui/lib": "^12.0.0",
2323
"@leafygreen-ui/palette": "^4.0.10",
24+
"@leafygreen-ui/polymorphic": "^2.0.0",
2425
"@leafygreen-ui/tokens": "^2.6.0",
2526
"@leafygreen-ui/typography": "^18.4.0"
2627
},

Diff for: chat/rich-links/src/RichLink/RichLink.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { forwardRef } from 'react';
33
import Card from '@leafygreen-ui/card';
44
import { cx } from '@leafygreen-ui/emotion';
55
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
6-
import { HTMLElementProps } from '@leafygreen-ui/lib';
6+
import { PolymorphicAs } from '@leafygreen-ui/polymorphic';
77
import { Body } from '@leafygreen-ui/typography';
88

99
import {
@@ -20,8 +20,6 @@ import {
2020
import { RichLinkBadge } from './RichLinkBadge';
2121
import { richLinkVariants } from './RichLinkVariants';
2222

23-
type DivProps = HTMLElementProps<'div', never>;
24-
2523
export const RichLink = forwardRef<HTMLAnchorElement, RichLinkProps>(
2624
({ darkMode: darkModeProp, ...props }, ref) => {
2725
const { darkMode, theme } = useDarkMode(darkModeProp);
@@ -55,17 +53,24 @@ export const RichLink = forwardRef<HTMLAnchorElement, RichLinkProps>(
5553

5654
const showImageBackground = (imageUrl?.length ?? -1) > 0;
5755

56+
const conditionalProps = href
57+
? {
58+
as: 'a' as PolymorphicAs,
59+
href,
60+
ref: ref,
61+
target: '_blank',
62+
...anchorProps,
63+
}
64+
: {};
65+
5866
return (
5967
<Card
6068
darkMode={darkMode}
61-
ref={ref}
6269
className={cx(baseStyles, themeStyles[theme], {
6370
[badgeAreaStyles]: showBadge,
6471
[imageBackgroundStyles(imageUrl ?? '')]: showImageBackground,
6572
})}
66-
as="a"
67-
// Cast to div props to get around Card's Box typing https://jira.mongodb.org/browse/LG-4259
68-
{...({ target: '_blank', href, ...anchorProps } as unknown as DivProps)}
73+
{...conditionalProps}
6974
>
7075
<Body className={richLinkTextClassName} darkMode={darkMode}>
7176
{children}

Diff for: chat/rich-links/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
{
2626
"path": "../../packages/icon"
2727
},
28+
{
29+
"path": "../../packages/polymorphic"
30+
},
2831
{
2932
"path": "../../packages/typography"
3033
},

Diff for: packages/card/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
"access": "public"
2323
},
2424
"dependencies": {
25-
"@leafygreen-ui/box": "^3.1.9",
2625
"@leafygreen-ui/emotion": "^4.0.8",
2726
"@leafygreen-ui/lib": "^13.3.0",
2827
"@leafygreen-ui/palette": "^4.0.9",
28+
"@leafygreen-ui/polymorphic": "^1.3.7",
2929
"@leafygreen-ui/tokens": "^2.5.2",
3030
"polished": "^4.2.2"
3131
},

Diff for: packages/card/src/Card.stories.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ import React from 'react';
33
import { storybookArgTypes, StoryMetaType } from '@lg-tools/storybook-utils';
44
import { StoryFn } from '@storybook/react';
55

6-
import { BoxProps } from '@leafygreen-ui/box';
7-
86
import Card, { CardProps } from '.';
97

108
const loremIpsum = `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy children ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.`;
119

12-
const meta: StoryMetaType<typeof Card, BoxProps> = {
10+
const meta: StoryMetaType<typeof Card> = {
1311
title: 'Components/Card',
1412
component: Card,
1513
parameters: {

Diff for: packages/card/src/Card/Card.spec.tsx

+25-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import React from 'react';
22
import { render } from '@testing-library/react';
33
import { axe } from 'jest-axe';
44

5-
import { Card, ContentStyle } from '.';
5+
import { InferredPolymorphicProps } from '@leafygreen-ui/polymorphic';
6+
7+
import { Card, CardProps, ContentStyle } from '.';
68

79
const defaultClassName = 'card-className';
810
const defaultChildren = 'this is my card component';
@@ -13,24 +15,20 @@ function isVisuallyClickable(element: HTMLElement): boolean {
1315
);
1416
}
1517

16-
interface PartialCardProps {
17-
children?: React.ReactNode;
18-
className?: string;
19-
href?: string;
20-
onClick?: React.MouseEventHandler;
21-
as?: 'section';
22-
contentStyle?: ContentStyle;
23-
}
18+
type DivLikeProps = InferredPolymorphicProps<'div', CardProps>;
19+
20+
type AnchorLikeProps = InferredPolymorphicProps<'a', CardProps>;
21+
22+
type CardRenderProps = DivLikeProps | AnchorLikeProps;
2423

2524
function renderCard({
2625
children = defaultChildren,
2726
className = defaultClassName,
2827
...rest
29-
}: PartialCardProps = {}) {
28+
}: CardRenderProps = {}) {
3029
const cardId = 'cardID';
3130

3231
const { container, getByTestId } = render(
33-
// @ts-expect-error
3432
<Card data-testid={cardId} className={className} {...rest}>
3533
{children}
3634
</Card>,
@@ -65,8 +63,8 @@ describe('packages/Card', () => {
6563
});
6664

6765
test(`renders component inside of a React Element/HTML tag based on as prop`, () => {
68-
const { renderedCard } = renderCard({ as: 'section' });
69-
expect(renderedCard.tagName.toLowerCase()).toBe('section');
66+
const utils = render(<Card data-testid="section" as="section" />);
67+
expect(utils.getByTestId('section').tagName.toLowerCase()).toBe('section');
7068
});
7169

7270
describe('content style', () => {
@@ -113,18 +111,29 @@ describe('packages/Card', () => {
113111
});
114112
});
115113

114+
function AnchorLike(props: JSX.IntrinsicElements['a']) {
115+
return <a {...props}>content</a>;
116+
}
117+
116118
/* eslint-disable jest/no-disabled-tests, jest/expect-expect*/
117119
describe.skip('Types behave as expected', () => {
118120
test('Allows no props', () => {
119121
<Card />;
120122
});
121123
test('Accepts `as` prop', () => {
122-
<Card as="p" />;
123-
<Card as={() => <></>} />;
124+
<>
125+
<Card as="p" />;
126+
<Card as={(_props: JSX.IntrinsicElements['div']) => <></>} />;
127+
<Card as="section" />
128+
</>;
124129
});
125130

126131
test('Accepts `href` prop', () => {
127-
<Card href="http://mongodb.design" />;
132+
<>
133+
<Card as={AnchorLike} href="string" />;
134+
<Card href="string" />
135+
<Card as="a" href="http://mongodb.design" />;
136+
</>;
128137
});
129138
});
130139
});

Diff for: packages/card/src/Card/Card.tsx

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33

4-
import Box, { BoxProps } from '@leafygreen-ui/box';
54
import { cx } from '@leafygreen-ui/emotion';
65
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
6+
import {
7+
InferredPolymorphic,
8+
PolymorphicAs,
9+
useInferredPolymorphic,
10+
} from '@leafygreen-ui/polymorphic';
711

812
import { colorSet, containerStyle } from './styles';
913
import { CardProps, ContentStyle } from './types';
1014

1115
/**
1216
* Cards are used to organize information into consumable chunks.
1317
*/
14-
export const Card = React.forwardRef(
18+
export const Card = InferredPolymorphic<CardProps, 'div'>(
1519
(
1620
{
21+
as = 'div' as PolymorphicAs,
1722
className,
1823
contentStyle,
1924
darkMode: darkModeProp,
2025
...rest
21-
}: BoxProps<'div', CardProps>,
22-
forwardRef,
26+
},
27+
ref,
2328
) => {
29+
const { Component } = useInferredPolymorphic(as, rest, 'div');
30+
2431
if (
2532
contentStyle === undefined &&
2633
(('onClick' in rest && rest.onClick !== undefined) ||
@@ -32,8 +39,8 @@ export const Card = React.forwardRef(
3239
const { theme } = useDarkMode(darkModeProp);
3340

3441
return (
35-
<Box
36-
// @ts-expect-error
42+
<Component
43+
ref={ref}
3744
className={cx(
3845
containerStyle,
3946
colorSet[theme].containerStyle,
@@ -43,7 +50,6 @@ export const Card = React.forwardRef(
4350
},
4451
className,
4552
)}
46-
ref={forwardRef}
4753
{...rest}
4854
/>
4955
);

Diff for: packages/card/src/Card/types.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib';
1+
import { DarkModeProps } from '@leafygreen-ui/lib';
22

33
export const ContentStyle = {
44
None: 'none',
@@ -7,7 +7,7 @@ export const ContentStyle = {
77

88
export type ContentStyle = (typeof ContentStyle)[keyof typeof ContentStyle];
99

10-
export interface CardProps extends DarkModeProps, HTMLElementProps<'div'> {
10+
export interface CardProps extends DarkModeProps {
1111
/**
1212
* Determines whether the Card should be styled as clickable.
1313
*
@@ -16,4 +16,10 @@ export interface CardProps extends DarkModeProps, HTMLElementProps<'div'> {
1616
* @default 'clickable' | 'none'
1717
*/
1818
contentStyle?: ContentStyle;
19+
20+
/**
21+
* Title for the Card component.
22+
*
23+
*/
24+
title?: string;
1925
}

Diff for: packages/card/tsconfig.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
],
1616
"exclude": ["**/*.spec.*", "**/*.stories.*"],
1717
"references": [
18-
{
19-
"path": "../box"
20-
},
18+
2119
{
2220
"path": "../emotion"
2321
},
@@ -27,6 +25,9 @@
2725
{
2826
"path": "../palette"
2927
},
28+
{
29+
"path": "../polymorphic"
30+
},
3031
{
3132
"path": "../tokens"
3233
},

Diff for: packages/expandable-card/src/ExpandableCard/ExpandableCard.types.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, { ReactNode } from 'react';
22

3-
import { CardProps } from '@leafygreen-ui/card';
4-
import { DarkModeProps } from '@leafygreen-ui/lib';
3+
import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib';
54

65
/**
76
* Types
87
*/
98
export interface ExpandableCardProps
10-
extends Omit<CardProps, 'contentStyle' | 'title'>,
11-
DarkModeProps {
9+
extends DarkModeProps,
10+
Omit<HTMLElementProps<'div'>, 'title'> {
1211
/**
1312
* The title of the card
1413
*/

Diff for: packages/polymorphic/README.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,16 @@ Ensure the custom props are wrapped in `InferredPolymorphicProps`, and use the `
8585
Make sure to pass both `as` and a `rest` object (that may contain `href`) into the hook.
8686

8787
```tsx
88-
export const MyInferredComponent = InferredPolymorphic<
89-
InferredPolymorphicProps<MyProps>
90-
>(({ as, ...rest }) => {
91-
const { Component, ref } = useInferredPolymorphic(as, rest);
92-
return (
93-
<Component ref={ref} {...rest}>
94-
{title}
95-
</Component>
96-
);
97-
});
88+
export const MyInferredComponent = InferredPolymorphic<MyProps>(
89+
({ as, ...rest }) => {
90+
const { Component, ref } = useInferredPolymorphic(as, rest);
91+
return (
92+
<Component ref={ref} {...rest}>
93+
{title}
94+
</Component>
95+
);
96+
},
97+
);
9898

9999
//
100100

Diff for: packages/polymorphic/src/Polymorphic/Polymorphic.types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export interface AsProp<T extends PolymorphicAs> {
3838
export type PolymorphicRef<T extends PolymorphicAs> =
3939
| ComponentPropsWithRef<T>['ref']
4040
| null;
41-
4241
/**
4342
* Union of prop types potentially re-defined in React.ComponentProps
4443
*/

Diff for: yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3773,6 +3773,11 @@
37733773
lodash "^4.17.21"
37743774
prop-types "^15.7.2"
37753775

3776+
"@leafygreen-ui/polymorphic@^1.3.7":
3777+
version "1.3.7"
3778+
resolved "https://registry.yarnpkg.com/@leafygreen-ui/polymorphic/-/polymorphic-1.3.7.tgz#befd1ae4a7e764ff3356a1bbfc63bb08b8a99198"
3779+
integrity sha512-Tr2TmpS0YFJ3hGNbVWQpeseJRo4kTrVumVlZ4aF4hId1JYDzF0TU5JJO40v+brhbgnKsyBu7+Rvz6ExY1NcKew==
3780+
37763781
"@leafygreen-ui/typography@^18.4.0":
37773782
version "18.4.0"
37783783
resolved "https://registry.yarnpkg.com/@leafygreen-ui/typography/-/typography-18.4.0.tgz#d157bc2fa64eb72f6d0baf86385575e60454c522"

0 commit comments

Comments
 (0)