Skip to content

Commit

Permalink
feat(components): Make <Text> and <Link> polymorphic (#1132)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebald authored Jun 22, 2021
1 parent 0313787 commit b6614f1
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 113 deletions.
16 changes: 16 additions & 0 deletions .changeset/weak-months-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@marigold/components": patch
"@marigold/types": patch
---

feat(compoents): Make `<Text>` and `<Link>` polymorphic

**`<Text>`**
- the `as` prop supports arbitrary inputs
- supporst ref
- supports style props (text-align, color, cursor, user-select)

**`<Link>`**
- the `as` prop supports arbitrary inputs
- does not support `ref`!
- improved accessibility (react-aria)
9 changes: 5 additions & 4 deletions docs/content/components/link.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ Element to style links.

## Properties

| Property | Type | Default |
| :------------------- | :------- | :------ |
| `variant` (optional) | `string` | `link` |
| `href` | `string` | |
| Property | Type | Default |
| :-------------------- | :-------------------- | :------ |
| `as` (optional) | string \| `Component` | `a` |
| `variant` (optional) | string \| string[] | `link` |
| `disabled` (optional) | boolean | |

## Import

Expand Down
16 changes: 10 additions & 6 deletions docs/content/components/text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ By adding the `as` prop you can alternatively render a `p` element.

## Properties

| Property | Type | Default |
| :--------------------- | :---------------- | :-------- |
| `as` (optional) | `span`, `p` | `span` |
| `variant` (optional) | `body`, `heading` | `body` |
| `textColor` (optional) | string | `inherit` |
| Property | Type | Default |
| :---------------------- | :-------------------- | :------ |
| `as` (optional) | string \| `Component` | `span` |
| `variant` (optional) | string \| string[] | `body` |
| `align` (optional) | string | |
| `color` (optional) | string | |
| `cursor` (optional) | string | |
| `outline` (optional) | string | |
| `userSelect` (optional) | string | |

## Import

Expand Down Expand Up @@ -58,5 +62,5 @@ import { Text } from '@marigold/components';
```

```tsx
<Text textColor="hotpink">Pink color - awesome</Text>
<Text color="hotpink">Pink is awesome!</Text>
```
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"license": "MIT",
"dependencies": {
"@marigold/system": "workspace:*",
"@react-aria/link": "^3.1.3",
"react-keyed-flatten-children": "1.3.0"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/Box/Box.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ test.each([
[{ opacity: 1 }, 'opacity: 0.5'],
[{ overflow: 'hidden' }, 'overflow: hidden'],
[{ transition: 1 }, 'transition: 1s opacity'],
])('test %o', (...args) => {
])('test style prop %o', (...args) => {
const props = args.shift();

render(
Expand Down
11 changes: 8 additions & 3 deletions packages/components/src/Box/Box.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createElement, forwardRef } from 'react';
import { ResponsiveStyleValue, useStyles } from '@marigold/system';
import { PolymorphicComponentWithRef } from '@marigold/types';
import {
PolymorphicPropsWithRef,
PolymorphicComponentWithRef,
} from '@marigold/types';

export type BoxProps = {
export type BoxOwnProps = {
className?: string;
variant?: string | string[];

Expand Down Expand Up @@ -53,7 +56,9 @@ export type BoxProps = {
transition?: ResponsiveStyleValue<number | string>;
};

export const Box: PolymorphicComponentWithRef<BoxProps, 'div'> = forwardRef(
export type BoxProps = PolymorphicPropsWithRef<BoxOwnProps, 'div'>;

export const Box: PolymorphicComponentWithRef<BoxOwnProps, 'div'> = forwardRef(
(
{
variant,
Expand Down
64 changes: 45 additions & 19 deletions packages/components/src/Link/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ThemeProvider } from '@marigold/system';
import { Link } from './Link';

const theme = {
link: {
normal: {
text: {
link: {
fontFamily: 'Inter',
},
second: {
Expand All @@ -14,54 +14,80 @@ const theme = {
},
};

test('supports default variant and themeSection', () => {
test('uses `text.link` as default variant', () => {
render(
<ThemeProvider theme={theme}>
<Link href="#!" title="link">
Link
</Link>
<Link href="#!">Link</Link>
</ThemeProvider>
);
const link = screen.getByTitle(/link/);
const link = screen.getByText(/Link/);

expect(link).toHaveStyle(`font-family: Inter`);
});

test('accepts other variant than default', () => {
test('allows to change variants via `variant` prop (with "text" prefix)', () => {
render(
<ThemeProvider theme={theme}>
<Link href="#!" title="link" variant="second">
<Link href="#!" variant="second">
Link
</Link>
</ThemeProvider>
);
const link = screen.getByTitle(/link/);
const link = screen.getByText(/Link/);

expect(link).toHaveStyle(`font-family: Oswald`);
});

test('renders correct HTML element', () => {
test('renders a <a> element by default', () => {
render(
<ThemeProvider theme={theme}>
<Link href="#!" title="link">
Link
</Link>
<Link href="#!">Link</Link>
</ThemeProvider>
);
const link = screen.getByTitle(/link/);
const link = screen.getByText(/Link/);

expect(link instanceof HTMLAnchorElement).toBeTruthy();
});

test('accepts custom styles prop className', () => {
test('accepts custom className', () => {
render(
<ThemeProvider theme={theme}>
<Link href="#!" className="custom-class-name" title="link">
link
<Link href="#!" className="custom-class-name">
Link
</Link>
</ThemeProvider>
);
const link = screen.getByTitle(/link/);
const link = screen.getByText(/Link/);

expect(link.className).toMatch('custom-class-name');
});

test('accepts other routing components', () => {
const RouterLink = React.forwardRef<
HTMLSpanElement,
{ to: string; children?: React.ReactNode }
>(() => <span>I am a Router Link!</span>);

render(
<ThemeProvider theme={theme}>
<Link as={RouterLink} to="/Home">
Link
</Link>
</ThemeProvider>
);

const link = screen.getByText('I am a Router Link!');
expect(link).toBeTruthy();
});

test('a link can be disabled via aria attributes', () => {
render(
<ThemeProvider theme={theme}>
<Link href="#!" disabled={true}>
Link
</Link>
</ThemeProvider>
);
const link = screen.getByText(/Link/);
expect(link.getAttribute('aria-disabled')).toEqual('true');
});
41 changes: 28 additions & 13 deletions packages/components/src/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import React from 'react';
import { ComponentProps } from '@marigold/types';
import { Box } from '../Box';
import React, { useRef } from 'react';
import { useLink } from '@react-aria/link';
import { PolymorphicComponent, PolymorphicProps } from '@marigold/types';

export type LinkProps = {
variant?: string;
} & ComponentProps<'a'>;
import { Text, TextOwnProps } from '../Text';

export const Link: React.FC<LinkProps> = ({
variant = 'normal',
export type LinkOwnProps = { disabled?: boolean } & TextOwnProps;
export type LinkProps = PolymorphicProps<LinkOwnProps, 'a'>;

export const Link = (({
as = 'a',
variant = 'link',
children,
disabled,
...props
}) => (
<Box {...props} as="a" variant={`link.${variant}`}>
{children}
</Box>
);
}: LinkProps) => {
const ref = useRef<any>();
const { linkProps } = useLink(
{
...props,
elementType: typeof as === 'string' ? as : 'span',
isDisabled: disabled,
},
ref
);

return (
<Text {...props} {...linkProps} as={as} variant={variant} ref={ref}>
{children}
</Text>
);
}) as PolymorphicComponent<LinkOwnProps, 'a'>;
75 changes: 40 additions & 35 deletions packages/components/src/Text/Text.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '@marigold/system';

import { Text } from './Text';
import { ThemeProvider, useStyles } from '@marigold/system';

const theme = {
colors: {
primary: 'hotpink',
black: '#000',
white: '#FFF',
blue: '#2980b9',
},
text: {
body: {
fontFamily: 'Oswald Regular',
Expand All @@ -14,7 +21,7 @@ const theme = {
},
};

test('accepts default variant', () => {
test('uses `text.body` as default variant', () => {
render(
<ThemeProvider theme={theme}>
<Text>text</Text>
Expand All @@ -25,70 +32,68 @@ test('accepts default variant', () => {
expect(text).toHaveStyle(`font-family: Oswald Regular`);
});

test('accepts default <span>', () => {
test('allows to change variants via `variant` prop (with "text" prefix)', () => {
render(
<ThemeProvider theme={theme}>
<Text>text</Text>
<Text variant="heading">text</Text>
</ThemeProvider>
);
const text = screen.getByText(/text/);

expect(text instanceof HTMLSpanElement).toBeTruthy();
expect(text).toHaveStyle(`font-family: Inter`);
});

test('accepts as <p>', () => {
test('renders a <span> element by default', () => {
render(
<ThemeProvider theme={theme}>
<Text as="p">text</Text>
<Text>text</Text>
</ThemeProvider>
);
const text = screen.getByText(/text/);

expect(text instanceof HTMLParagraphElement).toBeTruthy();
expect(text instanceof HTMLSpanElement).toBeTruthy();
});

test('variant works', () => {
test('allows to control the rendered element via the `as` prop', () => {
render(
<ThemeProvider theme={theme}>
<Text variant="body">text</Text>
<Text as="p">text</Text>
</ThemeProvider>
);
const text = screen.getByText(/text/);

expect(text).toHaveStyle(`font-family: Oswald Regular`);
expect(text instanceof HTMLParagraphElement).toBeTruthy();
});

test('accepts other variant than default', () => {
test.each([
[{ color: 'primary' }, 'color: hotpink'],
[{ color: 'blue' }, 'color: #2980b9'],
[{ align: 'center' }, 'text-align: center'],
[{ cursor: 'pointer' }, 'cursor: pointer'],
[{ outline: 'dashed red' }, 'outline: dashed red'],
[{ userSelect: 'none' }, 'user-select: none'],
])('test style prop %o', (...args) => {
const props = args.shift();

render(
<ThemeProvider theme={theme}>
<Text variant="heading" textColor="#000">
text
</Text>
<Text {...props}>This is the Text!</Text>
</ThemeProvider>
);
const text = screen.getByText(/text/);

expect(text).toHaveStyle(`color: rgb(0,0,0)`);
expect(text).toHaveStyle(`font-family: Inter`);
const box = screen.getByText('This is the Text!');
args.forEach((style: any) => {
expect(box).toHaveStyle(style);
});
});

test('accepts custom styles prop className', () => {
const TestTextComponent: React.FC = ({ children, ...props }) => {
const classNames = useStyles({ css: { fontSize: '8px' } });
return (
<Text className={classNames} {...props}>
{children}
</Text>
);
};

const { getByText } = render(
<ThemeProvider theme={theme}>
<TestTextComponent>text</TestTextComponent>
</ThemeProvider>
test('forwards ref', () => {
const ref = React.createRef<HTMLButtonElement>();
render(
<Text as="button" ref={ref}>
button
</Text>
);
const testelem = getByText('text');
const text = getComputedStyle(testelem);

expect(text.fontSize).toEqual('8px');
expect(ref.current instanceof HTMLButtonElement).toBeTruthy();
});
Loading

1 comment on commit b6614f1

@vercel
Copy link

@vercel vercel bot commented on b6614f1 Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.