Skip to content

Commit

Permalink
feat: Add default styling to <Element> (#1508)
Browse files Browse the repository at this point in the history
Co-authored-by: Timo Zehnle <59875255+ti10le@users.noreply.github.com>
  • Loading branch information
sebald and ti10le authored Dec 2, 2021
1 parent d962603 commit a1ef210
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 104 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-dolphins-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@marigold/system": patch
---

feat: Add default styling to `<Element>`
229 changes: 127 additions & 102 deletions packages/system/src/Element.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,73 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from './useTheme';
import { Element } from './Element';
import { normalize } from './normalize';

const theme = {
colors: {
primary: 'black',
secondary: 'hotpink',
},
fontSizes: {
body: 16,
small: 12,
large: 24,
},
space: {
none: 0,
small: 4,
medium: 8,
large: 16,
},
text: {
body: {
fontSize: 1,
color: 'black',
marginTop: '2px',
fontSize: 'body',
color: 'primary',
},
heading: {
fontSize: 3,
color: 'primary',
fontSize: 'large',
color: 'secondary',
},
padding: {
paddingTop: '2px',
whitespace: {
p: 'medium',
},
},
variant: {
spacing: {
m: 'large',
p: 'large',
},
},
};

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

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

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

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

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

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

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

expect(element.getAttribute('id')).toEqual('element-id');
expect(element.getAttribute('disabled')).toMatch('');
Expand All @@ -65,119 +86,123 @@ test('forwards ref', () => {
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);
test('apply normalized styles', () => {
render(<Element>Test</Element>);
const element = screen.getByText('Test');
const { base } = normalize;

expect(style.marginTop).toEqual('0px'); // 0px come from base
// Smoketest
expect(element).toHaveStyle(`box-sizing: ${base.boxSizing}`);
expect(element).toHaveStyle(`margin: ${base.margin}px`);
expect(element).toHaveStyle(`min-width: ${base.minWidth}`);
});

test('variant styles second', () => {
const TestComponent: React.FC<{ variant?: 'body' }> = ({
variant = 'body',
children,
...props
}) => {
return (
<Element as="p" variant={`text.${variant}`} {...props}>
{children}
</Element>
);
};
test('base normalization is always applied', () => {
render(<Element as="button">Test</Element>);
const element = screen.getByText('Test');
const { base } = normalize;

expect(element).toHaveStyle(`box-sizing: ${base.boxSizing}`);
expect(element).toHaveStyle(`margin: ${base.margin}px`);
expect(element).toHaveStyle(`min-width: ${base.minWidth}`);
});

test('apply normalized styles based on element', () => {
render(<Element as="h1">Test</Element>);
const element = screen.getByText('Test');
const { h1 } = normalize;

const { getByText } = render(
expect(element).toHaveStyle(`overflow-wrap: ${h1.overflowWrap}`);
});

test('accepts default styling via "__baseCSS" prop', () => {
render(<Element __baseCSS={{ color: 'hotpink' }}>Test</Element>);
const element = screen.getByText('Test');

expect(element).toHaveStyle('color: hotpink');
});

test('default styling overrides normalization', () => {
render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
<Element __baseCSS={{ m: 'medium' }}>Test</Element>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);
const element = screen.getByText('Test');

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
expect(element).toHaveStyle(`margin: ${theme.space.medium}px`);
});

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(
test('variants are applied correctly', () => {
render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
<Element variant="text.body">Test</Element>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);
const element = screen.getByText('Test');

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
expect(element).toHaveStyle(`font-size: ${theme.fontSizes.body}px`);
expect(element).toHaveStyle(`color: ${theme.colors.primary}`);
});

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}
test('accept an array of variants', () => {
render(
<ThemeProvider theme={theme}>
<Element as="p" variant={['text.heading', 'text.whitespace']}>
Test
</Element>
);
};
</ThemeProvider>
);
const element = screen.getByText('Test');

const { getByText } = render(
expect(element).toHaveStyle(`font-size: ${theme.fontSizes.large}px`);
expect(element).toHaveStyle(`color: ${theme.colors.secondary}`);
expect(element).toHaveStyle(`padding: ${theme.space.medium}px`);
});

test('variants override normalization and default styles', () => {
render(
<ThemeProvider theme={theme}>
<TestComponent>Text</TestComponent>
<Element __baseCSS={{ p: 'small' }} variant="variant.spacing">
Test
</Element>
</ThemeProvider>
);
const testelem = getByText('Text');
const style = getComputedStyle(testelem);
const element = screen.getByText('Test');

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
expect(element).toHaveStyle(`margin: ${theme.space.large}px`);
expect(element).toHaveStyle(`padding: ${theme.space.large}px`);
});

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>
);
};
test('apply custom styling via css prop', () => {
render(
<ThemeProvider theme={theme}>
<Element css={{ color: 'secondary', padding: '1rem' }}>Test</Element>
</ThemeProvider>
);
const element = screen.getByText('Test');

expect(element).toHaveStyle(`padding: 1rem`);
expect(element).toHaveStyle(`color: ${theme.colors.secondary}`);
});

const { getByText } = render(
test('custom styling overrides normalization, defaults and variants', () => {
render(
<ThemeProvider theme={theme}>
<TestComponent>Link</TestComponent>
<Element
__baseCSS={{ p: 'small' }}
variant="text.body"
css={{ fontSize: 'large', m: 'small', p: 'large' }}
>
Test
</Element>
</ThemeProvider>
);
const testelem = getByText('Link');
const style = getComputedStyle(testelem);
const element = screen.getByText('Test');

expect(element).toHaveStyle(`margin: ${theme.space.small}px`); // overrides normalization
expect(element).toHaveStyle(`padding: ${theme.space.large}px`); // overrides default
expect(element).toHaveStyle(`font-size: ${theme.fontSizes.large}px`); // overrides variant

expect(style.boxSizing).toEqual('border-box'); // from base
expect(style.textDecoration).toEqual('none'); // from a
expect(element).not.toHaveStyle(`color: ${theme.colors.primary}px`); // variant part that is not overriden
});
11 changes: 10 additions & 1 deletion packages/system/src/Element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import { useTheme } from './useTheme';
export type ElementOwnProps = {
css?: CSSObject;
variant?: string | string[];
/**
* Use to set base styles for the component
* @private - **use with caution ... please!**
*/
__baseCSS?: CSSObject;
};

export type ElementProps = PolymorphicPropsWithRef<ElementOwnProps, 'div'>;
Expand All @@ -24,7 +29,10 @@ const isNotEmpty = (val: any) =>

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

/**
Expand All @@ -41,6 +49,7 @@ export const Element: PolymorphicComponentWithRef<ElementOwnProps, 'div'> =
...{
css: [
getNormalizedStyles(as),
css(__baseCSS),
...variants.map(v => css(v)),
css(styles),
].filter(isNotEmpty),
Expand Down
2 changes: 1 addition & 1 deletion packages/system/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const media = {
...base,
display: 'block',
maxWidth: '100%',
};
} as const;

const button = {
...base,
Expand Down

0 comments on commit a1ef210

Please sign in to comment.