Skip to content

Commit 600b11d

Browse files
Brian Stonestonebk
Brian Stone
authored andcommitted
feat: add MediaObject component
1 parent 8940cf1 commit 600b11d

File tree

6 files changed

+335
-0
lines changed

6 files changed

+335
-0
lines changed

specs/MediaObject.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
MediaObject
2+
===========
3+
4+
A layout utility for aligning a media figure next to content.
5+
6+
Inspiration
7+
-----------
8+
9+
* https://getbootstrap.com/docs/4.0/layout/media-object/
10+
* https://lightningdesignsystem.com/utilities/media-objects/
11+
* https://philipwalton.github.io/solved-by-flexbox/demos/media-object/
12+
* http://www.stubbornella.org/content/2010/06/25/the-media-object-saves-hundreds-of-lines-of-code
13+
14+
Discovery
15+
---------
16+
17+
#### Are there similar components already in the library? If yes, what are they and can they be modified instead to capture this use-case?
18+
19+
* No
20+
21+
#### Who is the primary consumer for this component? Does it belong in the core API?
22+
23+
* Core component
24+
25+
#### Does the component behave any differently on desktop than it does on mobile? Is there any touch specific behavior?
26+
27+
* No
28+
29+
#### How does the component behave with respect to other components?
30+
31+
* Should support any content for the body and media. Should allow nesting of other `MediaObject` components.
32+
33+
#### Are there any existing libraries that can potentially be leveraged?
34+
35+
* No
36+
37+
#### Are there any performance considerations?
38+
39+
* No
40+
41+
#### [Theming] Is there any animation, transition, or other forms of motion involved?
42+
43+
* No
44+
45+
#### [Theming] Are there any z-index considerations?
46+
47+
* No
48+
49+
50+
API Specifications
51+
------------------
52+
53+
### Props
54+
55+
| Name | Type | Default | Description |
56+
| ---- | ---- | ------- | ----------- |
57+
| media | `PropTypes.node` | REQUIRED | The media content. |
58+
| children | `PropTypes.node` | REQUIRED | The body content. |
59+
| align | `PropTypes.oneOf(['top', 'bottom', 'center'])` | `"top"` | How to align the media content with respect to the text. |
60+
| reverse | `PropTypes.bool` | `false` | By default, the media will come before the body. Set this to reverse the order of media and body. |
61+
| gutter | | | Used for defining spacing between the media and children content. |
62+
63+
### Example Usage
64+
65+
```jsx
66+
<MediaObject
67+
media={<img src="image.png" />}
68+
align="center"
69+
reverse
70+
>
71+
<p>This is some text<p>
72+
</MediaObject>
73+
```
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import styled from 'styled-components';
4+
import getTheme from '../../theme/getTheme';
5+
6+
// This wrapper is used for nothing other than filtering props that are passed to it so they don't
7+
// bleed through to the DOM
8+
// (styled-components does not filter props when wrapping a third-party component).
9+
const StyledMediaObject = styled.div``;
10+
11+
/**
12+
* A layout utility for aligning a media figure next to content.
13+
*
14+
* ##### Inspiration
15+
* * https://getbootstrap.com/docs/4.0/layout/media-object/
16+
* * https://lightningdesignsystem.com/utilities/media-objects/
17+
* * https://philipwalton.github.io/solved-by-flexbox/demos/media-object/
18+
* * http://www.stubbornella.org/content/2010/06/25/the-media-object-saves-hundreds-of-lines-of-code
19+
*/
20+
const MediaObject = styled(props => {
21+
const { renderLayout } = props;
22+
23+
let content = renderLayout;
24+
if (typeof renderLayout === 'function') {
25+
content = renderLayout(props);
26+
}
27+
28+
return <StyledMediaObject {...props}>{content}</StyledMediaObject>;
29+
})`
30+
${getTheme('MediaObject')}
31+
`;
32+
33+
MediaObject.Body = styled.div`
34+
${getTheme('MediaObjectBody')}
35+
`;
36+
MediaObject.Media = styled.div`
37+
${getTheme('MediaObjectMedia')}
38+
`;
39+
40+
MediaObject.propTypes = {
41+
/**
42+
* How to align the media content with respect to the children content.
43+
*/
44+
align: PropTypes.oneOf(['top', 'center', 'bottom']),
45+
/**
46+
* The body content.
47+
*/
48+
children: PropTypes.node,
49+
/**
50+
* The spacing between media and children content.
51+
*/
52+
// eslint-disable-next-line zillow/react/forbid-prop-types
53+
gutter: PropTypes.any,
54+
/**
55+
* The media content.
56+
*/
57+
media: PropTypes.node,
58+
/**
59+
* You can use `renderLayout` to modify the default composition of the component.
60+
* Pass your own node using the `<MediaObject.Body>` and `<MediaObject.Media>` wrappers,
61+
* or pass a render function that receives enhanced props as the only argument.
62+
*/
63+
renderLayout: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
64+
/**
65+
* By default, the media will come before the body.
66+
* Set this to reverse the order of media and body.
67+
*/
68+
reverse: PropTypes.bool,
69+
};
70+
71+
MediaObject.defaultProps = {
72+
align: 'top',
73+
// eslint-disable-next-line zillow/react/prop-types
74+
renderLayout: ({ media, children }) => (
75+
<React.Fragment>
76+
{media && <MediaObject.Media>{media}</MediaObject.Media>}
77+
{children && <MediaObject.Body>{children}</MediaObject.Body>}
78+
</React.Fragment>
79+
),
80+
reverse: false,
81+
};
82+
83+
/** @component */
84+
export default MediaObject;
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
```jsx
2+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />}>
3+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
4+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
5+
amet placerat. Morbi rhoncus dictum elementum.
6+
</MediaObject>
7+
```
8+
9+
Center aligned.
10+
11+
```jsx
12+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />} align="center">
13+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
14+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
15+
amet placerat. Morbi rhoncus dictum elementum.
16+
</MediaObject>
17+
```
18+
19+
Bottom aligned.
20+
21+
```jsx
22+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />} align="bottom">
23+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
24+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
25+
amet placerat. Morbi rhoncus dictum elementum.
26+
</MediaObject>
27+
```
28+
29+
Reversed media and content.
30+
31+
```jsx
32+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />} reverse>
33+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
34+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
35+
amet placerat. Morbi rhoncus dictum elementum.
36+
</MediaObject>
37+
```
38+
39+
Nested media objects.
40+
41+
```jsx
42+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />}>
43+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
44+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
45+
amet placerat. Morbi rhoncus dictum elementum.
46+
47+
<MediaObject media={<div style={{ height: '64px', width: '64px', background: '#777' }} />}>
48+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce ornare lorem sit amet quam mattis,
49+
ac fringilla est commodo. Vestibulum rhoncus congue tempus. Vivamus cursus scelerisque nulla sit
50+
amet placerat. Morbi rhoncus dictum elementum.
51+
</MediaObject>
52+
</MediaObject>
53+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import TestRenderer from 'react-test-renderer';
3+
import { ThemeProvider, css, isStyledComponent } from 'styled-components';
4+
import { MediaObject } from '../../../index';
5+
import { createTestTheme } from '../../../test/util';
6+
7+
describe('<MediaObject>', () => {
8+
it('can render without theme', () => {
9+
const testRenderer = TestRenderer.create(<MediaObject media="media">body</MediaObject>);
10+
expect(testRenderer.toJSON()).toMatchSnapshot();
11+
});
12+
13+
it('can render with theme', () => {
14+
const theme = createTestTheme({
15+
MediaObject: css`
16+
color: red;
17+
`,
18+
});
19+
const testRenderer = TestRenderer.create(
20+
<ThemeProvider theme={theme}>
21+
<MediaObject media="media">body</MediaObject>
22+
</ThemeProvider>
23+
);
24+
const tree = testRenderer.toJSON();
25+
expect(tree).toHaveStyleRule('color', 'red');
26+
expect(tree).toMatchSnapshot();
27+
});
28+
29+
it('is a styled-component', () => {
30+
expect(isStyledComponent(MediaObject)).toBe(true);
31+
});
32+
33+
it('does not pass unrecognized react props through to DOM', () => {
34+
const testRenderer = TestRenderer.create(
35+
<MediaObject media="media" data-foobar="baz" fooBar="baz">
36+
body
37+
</MediaObject>
38+
);
39+
const testInstance = testRenderer.root;
40+
const mediaObject = testInstance.findByType('div');
41+
expect(mediaObject.props['data-foobar']).toBe('baz');
42+
expect(mediaObject.props.fooBar).toBeUndefined();
43+
});
44+
45+
it('customizes the layout with renderLayout render function', () => {
46+
const CustomLayout = () => <p>custom layout</p>;
47+
const testRenderer = TestRenderer.create(
48+
<MediaObject renderLayout={() => <CustomLayout />} />
49+
);
50+
const testInstance = testRenderer.root;
51+
expect(() => testInstance.findByType(CustomLayout)).not.toThrow();
52+
expect(testRenderer.toJSON()).toMatchSnapshot();
53+
});
54+
55+
it('customizes the layout with renderLayout node', () => {
56+
const CustomLayout = () => <p>custom layout</p>;
57+
const testRenderer = TestRenderer.create(<MediaObject renderLayout={<CustomLayout />} />);
58+
const testInstance = testRenderer.root;
59+
expect(() => testInstance.findByType(CustomLayout)).not.toThrow();
60+
expect(testRenderer.toJSON()).toMatchSnapshot();
61+
});
62+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<MediaObject> can render with theme 1`] = `
4+
.c0 {
5+
color: red;
6+
}
7+
8+
<div
9+
className="c0 MediaObject__StyledMediaObject-sc-12gs3hz-0 "
10+
media="media"
11+
>
12+
<div
13+
className="MediaObject__Media-sc-12gs3hz-3 "
14+
>
15+
media
16+
</div>
17+
<div
18+
className="MediaObject__Body-sc-12gs3hz-2 "
19+
>
20+
body
21+
</div>
22+
</div>
23+
`;
24+
25+
exports[`<MediaObject> can render without theme 1`] = `
26+
<div
27+
className="MediaObject-sc-12gs3hz-1 MediaObject__StyledMediaObject-sc-12gs3hz-0 "
28+
media="media"
29+
>
30+
<div
31+
className="MediaObject__Media-sc-12gs3hz-3 "
32+
>
33+
media
34+
</div>
35+
<div
36+
className="MediaObject__Body-sc-12gs3hz-2 "
37+
>
38+
body
39+
</div>
40+
</div>
41+
`;
42+
43+
exports[`<MediaObject> customizes the layout with renderLayout node 1`] = `
44+
<div
45+
className="MediaObject__StyledMediaObject-sc-12gs3hz-0 "
46+
>
47+
<p>
48+
custom layout
49+
</p>
50+
</div>
51+
`;
52+
53+
exports[`<MediaObject> customizes the layout with renderLayout render function 1`] = `
54+
<div
55+
className="MediaObject__StyledMediaObject-sc-12gs3hz-0 "
56+
>
57+
<p>
58+
custom layout
59+
</p>
60+
</div>
61+
`;

src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { default as AnimatedList } from './components/AnimatedList/AnimatedList';
22
export { default as Button } from './components/Button/Button';
33
export { default as CloseButton } from './components/CloseButton/CloseButton';
4+
export { default as MediaObject } from './components/MediaObject/MediaObject';
45
export { default as Toast } from './components/Toast/Toast';
56
export { default as ToastProvider, withToast } from './components/Toast/ToastProvider';
67
export { default as withNamespace } from './theme/withNamespace';
8+
export { default as getTheme } from './theme/getTheme';

0 commit comments

Comments
 (0)