From b6614f1f54165bc295709fa2e7f1c50892163fc3 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 22 Jun 2021 14:11:18 +0200 Subject: [PATCH] feat(components): Make `` and `` polymorphic (#1132) --- .changeset/weak-months-bathe.md | 16 ++++ docs/content/components/link.mdx | 9 +- docs/content/components/text.mdx | 16 ++-- packages/components/package.json | 1 + packages/components/src/Box/Box.test.tsx | 2 +- packages/components/src/Box/Box.tsx | 11 ++- packages/components/src/Link/Link.test.tsx | 64 +++++++++---- packages/components/src/Link/Link.tsx | 41 +++++--- packages/components/src/Text/Text.test.tsx | 75 ++++++++------- packages/components/src/Text/Text.tsx | 76 ++++++++------- packages/types/src/index.ts | 2 +- yarn.lock | 103 +++++++++++++++++++++ 12 files changed, 303 insertions(+), 113 deletions(-) create mode 100644 .changeset/weak-months-bathe.md diff --git a/.changeset/weak-months-bathe.md b/.changeset/weak-months-bathe.md new file mode 100644 index 0000000000..3c95a8bd04 --- /dev/null +++ b/.changeset/weak-months-bathe.md @@ -0,0 +1,16 @@ +--- +"@marigold/components": patch +"@marigold/types": patch +--- + +feat(compoents): Make `` and `` polymorphic + +**``** +- the `as` prop supports arbitrary inputs +- supporst ref +- supports style props (text-align, color, cursor, user-select) + +**``** +- the `as` prop supports arbitrary inputs +- does not support `ref`! +- improved accessibility (react-aria) diff --git a/docs/content/components/link.mdx b/docs/content/components/link.mdx index fa035b13a4..dbfda12108 100644 --- a/docs/content/components/link.mdx +++ b/docs/content/components/link.mdx @@ -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 diff --git a/docs/content/components/text.mdx b/docs/content/components/text.mdx index 4e3b1a6d44..91e38e1ce6 100644 --- a/docs/content/components/text.mdx +++ b/docs/content/components/text.mdx @@ -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 @@ -58,5 +62,5 @@ import { Text } from '@marigold/components'; ``` ```tsx -Pink color - awesome +Pink is awesome! ``` diff --git a/packages/components/package.json b/packages/components/package.json index ea82d0e84d..df69a736b7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -8,6 +8,7 @@ "license": "MIT", "dependencies": { "@marigold/system": "workspace:*", + "@react-aria/link": "^3.1.3", "react-keyed-flatten-children": "1.3.0" }, "peerDependencies": { diff --git a/packages/components/src/Box/Box.test.tsx b/packages/components/src/Box/Box.test.tsx index 642ea93703..ddfe80048e 100644 --- a/packages/components/src/Box/Box.test.tsx +++ b/packages/components/src/Box/Box.test.tsx @@ -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( diff --git a/packages/components/src/Box/Box.tsx b/packages/components/src/Box/Box.tsx index 864919ce7f..05d8409602 100644 --- a/packages/components/src/Box/Box.tsx +++ b/packages/components/src/Box/Box.tsx @@ -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[]; @@ -53,7 +56,9 @@ export type BoxProps = { transition?: ResponsiveStyleValue; }; -export const Box: PolymorphicComponentWithRef = forwardRef( +export type BoxProps = PolymorphicPropsWithRef; + +export const Box: PolymorphicComponentWithRef = forwardRef( ( { variant, diff --git a/packages/components/src/Link/Link.test.tsx b/packages/components/src/Link/Link.test.tsx index ac01882dfc..8bbb58bed4 100644 --- a/packages/components/src/Link/Link.test.tsx +++ b/packages/components/src/Link/Link.test.tsx @@ -4,8 +4,8 @@ import { ThemeProvider } from '@marigold/system'; import { Link } from './Link'; const theme = { - link: { - normal: { + text: { + link: { fontFamily: 'Inter', }, second: { @@ -14,54 +14,80 @@ const theme = { }, }; -test('supports default variant and themeSection', () => { +test('uses `text.link` as default variant', () => { render( - - Link - + Link ); - 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( - + Link ); - const link = screen.getByTitle(/link/); + const link = screen.getByText(/Link/); expect(link).toHaveStyle(`font-family: Oswald`); }); -test('renders correct HTML element', () => { +test('renders a element by default', () => { render( - - Link - + Link ); - 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( - - link + + Link ); - 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 } + >(() => I am a Router Link!); + + render( + + + Link + + + ); + + const link = screen.getByText('I am a Router Link!'); + expect(link).toBeTruthy(); +}); + +test('a link can be disabled via aria attributes', () => { + render( + + + Link + + + ); + const link = screen.getByText(/Link/); + expect(link.getAttribute('aria-disabled')).toEqual('true'); +}); diff --git a/packages/components/src/Link/Link.tsx b/packages/components/src/Link/Link.tsx index 13abe0726b..77a635b380 100755 --- a/packages/components/src/Link/Link.tsx +++ b/packages/components/src/Link/Link.tsx @@ -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 = ({ - variant = 'normal', +export type LinkOwnProps = { disabled?: boolean } & TextOwnProps; +export type LinkProps = PolymorphicProps; + +export const Link = (({ + as = 'a', + variant = 'link', children, + disabled, ...props -}) => ( - - {children} - -); +}: LinkProps) => { + const ref = useRef(); + const { linkProps } = useLink( + { + ...props, + elementType: typeof as === 'string' ? as : 'span', + isDisabled: disabled, + }, + ref + ); + + return ( + + {children} + + ); +}) as PolymorphicComponent; diff --git a/packages/components/src/Text/Text.test.tsx b/packages/components/src/Text/Text.test.tsx index a62972f720..834b8fbc8a 100644 --- a/packages/components/src/Text/Text.test.tsx +++ b/packages/components/src/Text/Text.test.tsx @@ -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', @@ -14,7 +21,7 @@ const theme = { }, }; -test('accepts default variant', () => { +test('uses `text.body` as default variant', () => { render( text @@ -25,70 +32,68 @@ test('accepts default variant', () => { expect(text).toHaveStyle(`font-family: Oswald Regular`); }); -test('accepts default ', () => { +test('allows to change variants via `variant` prop (with "text" prefix)', () => { render( - text + text ); const text = screen.getByText(/text/); - expect(text instanceof HTMLSpanElement).toBeTruthy(); + expect(text).toHaveStyle(`font-family: Inter`); }); -test('accepts as

', () => { +test('renders a element by default', () => { render( - text + text ); 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( - text + text ); 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( - - text - + This is the Text! ); - 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 ( - - {children} - - ); - }; - - const { getByText } = render( - - text - +test('forwards ref', () => { + const ref = React.createRef(); + render( + + button + ); - const testelem = getByText('text'); - const text = getComputedStyle(testelem); - expect(text.fontSize).toEqual('8px'); + expect(ref.current instanceof HTMLButtonElement).toBeTruthy(); }); diff --git a/packages/components/src/Text/Text.tsx b/packages/components/src/Text/Text.tsx index aebeca0fcf..5c191aacef 100644 --- a/packages/components/src/Text/Text.tsx +++ b/packages/components/src/Text/Text.tsx @@ -1,35 +1,49 @@ -import React from 'react'; -import { useStyles } from '@marigold/system'; -import { ComponentPropsWithRef } from '@marigold/types'; -import { Box, BoxProps } from '../Box'; +import React, { forwardRef } from 'react'; +import { ResponsiveStyleValue, useStyles } from '@marigold/system'; +import { + PolymorphicComponentWithRef, + PolymorphicPropsWithRef, +} from '@marigold/types'; -export type TextProps = { - className?: string; - as?: 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; - variant?: string; - textColor?: string; -} & ComponentPropsWithRef<'span'> & - BoxProps; +import { Box, BoxOwnProps } from '../Box'; -export const Text: React.FC = ({ - as = 'span', - variant = 'body', - textColor = 'inherit', - className, - children, - ...props -}) => { - const classNames = useStyles({ - variant: `text.${variant}`, - css: { - color: textColor, - }, - className, - }); +export type TextOwnProps = { + align?: ResponsiveStyleValue; + color?: ResponsiveStyleValue; + cursor?: ResponsiveStyleValue; + outline?: ResponsiveStyleValue; + userSelect?: ResponsiveStyleValue; +} & BoxOwnProps; - return ( - - {children} - +export type TextProps = PolymorphicPropsWithRef; + +export const Text: PolymorphicComponentWithRef = + forwardRef( + ( + { + as = 'span', + variant = 'body', + children, + className, + align, + color, + cursor, + outline, + userSelect, + ...props + }, + ref + ) => { + const cn = useStyles({ + className, + variant: `text.${variant}`, + css: { textAlign: align, color, cursor, outline, userSelect }, + }); + + return ( + + {children} + + ); + } ); -}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e5dcac9860..718ed06106 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -105,4 +105,4 @@ export interface PolymorphicComponentWithRef ( props: PolymorphicPropsWithRef ): React.ReactElement

| null; -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9a6efd178f..5312b0eae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3364,6 +3364,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.6.2": + version: 7.14.6 + resolution: "@babel/runtime@npm:7.14.6" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: 927ffed7871f2ed29f967a8dad7a72aa10662f93b6735a89d664a161fa4dc2074b8947ca159a8a0a49cec9a71c8de473d7c2b22d3de0ee4d7dd06d24a7f98018 + languageName: node + linkType: hard + "@babel/standalone@npm:^7.14.0": version: 7.14.5 resolution: "@babel/standalone@npm:7.14.5" @@ -5243,6 +5252,7 @@ __metadata: dependencies: "@marigold/system": "workspace:*" "@marigold/types": "workspace:*" + "@react-aria/link": ^3.1.3 react-keyed-flatten-children: 1.3.0 peerDependencies: react: ^16.x || ^17.0.0 @@ -5572,6 +5582,92 @@ __metadata: languageName: node linkType: hard +"@react-aria/interactions@npm:^3.4.0": + version: 3.5.0 + resolution: "@react-aria/interactions@npm:3.5.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/utils": ^3.8.1 + "@react-types/shared": ^3.7.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 1e6652c2e5cbaf8c71b5da59c069e4ca07f21682601e720700acaceb3393f69f9f9b5adb0bb1c34eeb8a4d574a3e02cdd809bd80a1229c6739a2a3f3e0759521 + languageName: node + linkType: hard + +"@react-aria/link@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-aria/link@npm:3.1.3" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/interactions": ^3.4.0 + "@react-aria/utils": ^3.8.0 + "@react-types/link": ^3.1.2 + "@react-types/shared": ^3.6.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 48e6528db4d74c84fbfd7e4dbb241b82e759c3e40fe4a92cdebb6ccd60b63ecb130c65b286319acc8ad87c84e95849a6af3d35f3c5815e8fd91004bc07b59744 + languageName: node + linkType: hard + +"@react-aria/ssr@npm:^3.0.2": + version: 3.0.2 + resolution: "@react-aria/ssr@npm:3.0.2" + dependencies: + "@babel/runtime": ^7.6.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: ef3d2c52d794fd0dbc471cff639139d42a8e707d4b0fd16f831bd7d1ebb2385e6aa472b8a67f3661e93fb5b0f0445037ac3ce3f79424d97e506c0e8a15cbe538 + languageName: node + linkType: hard + +"@react-aria/utils@npm:^3.8.0, @react-aria/utils@npm:^3.8.1": + version: 3.8.1 + resolution: "@react-aria/utils@npm:3.8.1" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/ssr": ^3.0.2 + "@react-stately/utils": ^3.2.1 + "@react-types/shared": ^3.7.0 + clsx: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 4b7aa7e545c9fa2283c61510107f8e0bc705d9fa1b579fbc076df2052712b1452968aefaa57f2e3f3a90d9590011a245bbe554feaa33effe193c7a4d6741ac57 + languageName: node + linkType: hard + +"@react-stately/utils@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-stately/utils@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.6.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 349011c2746aa59e4ec352e2a684cea1621bc16e6d21c795df19d32199fc06c15737630cafec93eecfe878ee1cb6e39912972e118b94c820f19e0a6e193e09bf + languageName: node + linkType: hard + +"@react-types/link@npm:^3.1.2": + version: 3.1.2 + resolution: "@react-types/link@npm:3.1.2" + dependencies: + "@react-aria/interactions": ^3.4.0 + "@react-types/shared": ^3.6.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: c961f09c845ba011dd55b4488a9f91fe9197edd42fc49b648a77eee95351bd67def64bd809caf18394b9fcc2516c358219ea5b434bb3f7eca79e522e5e90a6b5 + languageName: node + linkType: hard + +"@react-types/shared@npm:^3.6.0, @react-types/shared@npm:^3.7.0": + version: 3.7.0 + resolution: "@react-types/shared@npm:3.7.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: b404ea22b11da7212026036134af8b314fc8a58de4bd08d2b5fcc1055d16be4d5cd627cfa3a5afc83f48a68b38c07a0f8041d9fd739c4e5a082c5e32b8bec045 + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.1.0": version: 5.3.0 resolution: "@rollup/plugin-babel@npm:5.3.0" @@ -11098,6 +11194,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.1.1": + version: 1.1.1 + resolution: "clsx@npm:1.1.1" + checksum: ff052650329773b9b245177305fc4c4dc3129f7b2be84af4f58dc5defa99538c61d4207be7419405a5f8f3d92007c954f4daba5a7b74e563d5de71c28c830063 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0"