Skip to content

Commit

Permalink
create Element component + normalize file (#1436)
Browse files Browse the repository at this point in the history
* create Element component + normalize file

* changing Element

* wip Element

* final Element component changes

* Create slimy-bikes-wash.md

* remove useless test and rewrite comment
  • Loading branch information
ti10le authored Oct 29, 2021
1 parent 69aa6be commit c4ae5c5
Show file tree
Hide file tree
Showing 6 changed files with 442 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/slimy-bikes-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@marigold/system": patch
---

create Element component + normalize file
203 changes: 203 additions & 0 deletions packages/system/src/Element.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { useStyles } from './useStyles';
import { ThemeProvider } from './useTheme';
import { Element } from './Element';

const theme = {
text: {
body: {
fontSize: 1,
color: 'black',
marginTop: '2px',
},
heading: {
fontSize: 3,
color: 'primary',
},
padding: {
paddingTop: '2px',
},
},
};

test('renders a <div> by default', () => {
render(<Element>Text</Element>);
const testelem = screen.getByText('Text');

expect(testelem instanceof HTMLDivElement).toBeTruthy();
});

test('supports as prop', () => {
render(<Element as="p">Text</Element>);
const testelem = screen.getByText('Text');

expect(testelem instanceof HTMLParagraphElement).toBeTruthy();
});

test('supports HTML className attribute', () => {
render(<Element className="my-custom-class">Text</Element>);
const element = screen.getByText('Text');

expect(element.getAttribute('class')).toMatch('my-custom-class');
});

test('passes down HTML attributes', () => {
render(
<Element className="my-custom-class" id="element-id" disabled>
Text
</Element>
);
const element = screen.getByText('Text');

expect(element.getAttribute('id')).toEqual('element-id');
expect(element.getAttribute('disabled')).toMatch('');
expect(element.getAttribute('class')).toMatch('my-custom-class');
});

test('forwards ref', () => {
const ref = React.createRef<HTMLButtonElement>();
render(
<Element as="button" ref={ref}>
button
</Element>
);

expect(ref.current instanceof HTMLButtonElement).toBeTruthy();
});

test('base styles first', () => {
const { getByText } = render(<Element as="p">Text</Element>);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);

expect(style.marginTop).toEqual('0px'); // 0px come from base
});

test('variant styles second', () => {
const TestComponent: React.FC<{ variant?: 'body' }> = ({
variant = 'body',
children,
...props
}) => {
return (
<Element as="p" variant={`text.${variant}`} {...props}>
{children}
</Element>
);
};

const { getByText } = render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);

expect(style.marginTop).not.toEqual('0px'); // 0px come from base
expect(style.marginBottom).toEqual('0px'); // 0px still come from base
expect(style.marginTop).toEqual('2px'); // 2px come from variant
});

test('array of variant styles', () => {
const TestComponent: React.FC<{ variant?: 'body' }> = ({
variant = 'body',
children,
...props
}) => {
return (
<Element as="p" variant={[`text.${variant}`, `text.padding`]} {...props}>
{children}
</Element>
);
};

const { getByText } = render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);

expect(style.marginTop).not.toEqual('0px'); // 0px come from base
expect(style.marginBottom).toEqual('0px'); // 0px still come from base
expect(style.marginTop).toEqual('2px'); // 2px marginTop come from variant
expect(style.paddingTop).toEqual('2px'); // 2px paddingTop come from variant
});

test('custom styles with css prop third', () => {
const TestComponent: React.FC<{ variant?: 'body' }> = ({
variant = 'body',
children,
...props
}) => {
return (
<Element
as="p"
variant={`text.${variant}`}
css={{ marginTop: '4px' }}
{...props}
>
{children}
</Element>
);
};

const { getByText } = render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);

expect(style.marginTop).not.toEqual('0px'); // do not apply 0px from base
expect(style.marginTop).not.toEqual('2px'); // do not apply 2px from variant
expect(style.marginTop).toEqual('4px'); // apply 4px from custom styles
});

test("don't apply the same reset multiple times", () => {
const Button = ({ className }: { className?: string }) => {
const classNames = useStyles({ element: 'button', className });
return (
<Element as="button" title="button" className={classNames}>
Click me!
</Element>
);
};
const Wrapper = () => <Button />;

render(<Wrapper />);
const button = screen.getByTitle('button');
const classNames = button.className.split(' ').filter(i => i.length);

// Test if applied classnames are unique
expect(classNames.length).toEqual([...new Set(classNames)].length);
});

test('normalize tag name <a>', () => {
const TestComponent: React.FC<{ variant?: 'body' }> = ({
variant = 'body',
children,
...props
}) => {
return (
<Element as="a" variant={`text.${variant}`} {...props}>
{children}
</Element>
);
};

const { getByText } = render(
<ThemeProvider theme={theme}>
<TestComponent>Link</TestComponent>
</ThemeProvider>
);
const testelem = getByText('Link');
const style = getComputedStyle(testelem);

expect(style.boxSizing).toEqual('border-box'); // from base
expect(style.textDecoration).toEqual('none'); // from a
});
59 changes: 59 additions & 0 deletions packages/system/src/Element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { jsx } from '@emotion/react';
import { forwardRef } from 'react';
import {
PolymorphicPropsWithRef,
PolymorphicComponentWithRef,
} from '@marigold/types';

import { getNormalizedStyles } from './normalize';
import { CSSObject } from './types';
import { useTheme } from './useTheme';

export type ElementOwnProps = {
css?: CSSObject;
variant?: string | string[];
};

export type ElementProps = PolymorphicPropsWithRef<ElementOwnProps, 'div'>;

/**
* Function expression to check if there is any falsy value or empty object
*/
const isNotEmpty = (val: any) =>
!(val && Object.keys(val).length === 0 && val.constructor === Object);

/**
* Get the normalized base styles
*/
const baseStyles = getNormalizedStyles('base');

export const Element: PolymorphicComponentWithRef<ElementOwnProps, 'div'> =
forwardRef(
({ as = 'div', css: styles = {}, variant, children, ...props }, ref) => {
const { css } = useTheme();

/**
* Transform variant input for `@theme-ui/css`
*/
const variants = Array.isArray(variant)
? variant.map(v => ({ variant: v }))
: [{ variant }];

return jsx(
as,
{
...props,
...{
css: [
baseStyles,
getNormalizedStyles(as),
...variants.map(v => css(v)),
css(styles),
].filter(isNotEmpty),
},
ref,
},
children
);
}
);
3 changes: 2 additions & 1 deletion packages/system/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './Element';
export * from './cache';
export * from './types';
export * from './useClassname';
export * from './useStyles';
export * from './useTheme';
export * from './useTheme';
42 changes: 42 additions & 0 deletions packages/system/src/normalize.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getNormalizedStyles } from './normalize';

test('get base styles', () => {
const baseStyles = getNormalizedStyles('base');
expect(baseStyles).toEqual({
boxSizing: 'border-box',
margin: 0,
padding: 0,
minWidth: 0,
fontSize: '100%',
fontFamily: 'inherit',
verticalAlign: 'baseline',
WebkitTapHighlightColor: 'transparent',
});
});

test('get reset style by element', () => {
const baseStyles = getNormalizedStyles('a');
expect(baseStyles).toEqual({
textDecoration: 'none',
touchAction: 'manipulation',
});
});

test('getNormalizedStyles returns base if input is not a string', () => {
const baseStyles = getNormalizedStyles(undefined);
expect(baseStyles).toEqual({
boxSizing: 'border-box',
margin: 0,
padding: 0,
minWidth: 0,
fontSize: '100%',
fontFamily: 'inherit',
verticalAlign: 'baseline',
WebkitTapHighlightColor: 'transparent',
});
});

test('getNormalizedStyles returns empty object if input is unknown', () => {
const baseStyles = getNormalizedStyles('p');
expect(baseStyles).toEqual({});
});
Loading

0 comments on commit c4ae5c5

Please sign in to comment.