Skip to content

Commit 5ccefdd

Browse files
chenesanbpas247
authored andcommitted
Added v4 support for layout utility
1 parent 651ae24 commit 5ccefdd

File tree

3 files changed

+355
-0
lines changed

3 files changed

+355
-0
lines changed

src/Layout.js

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import classNames from 'classnames';
2+
import React from 'react';
3+
import PropTypes from 'prop-types';
4+
import elementType from 'prop-types-extra/lib/elementType';
5+
6+
const camelCaseToHyphen = (str) =>
7+
str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
8+
9+
const flexAlignPropTypes = PropTypes.oneOf([
10+
'start',
11+
'end',
12+
'center',
13+
'baseline',
14+
'stretch',
15+
]);
16+
17+
const displayPropTypes = PropTypes.oneOf([
18+
'none',
19+
'inline',
20+
'inline-block',
21+
'block',
22+
'table',
23+
'table-cell',
24+
'table-row',
25+
'flex',
26+
'inline-flex',
27+
]);
28+
29+
const flexDirectionPropTypes = PropTypes.oneOf([
30+
'row',
31+
'row-reverse',
32+
'column',
33+
'column-reverse',
34+
]);
35+
36+
const flexWrapPropTypes = PropTypes.oneOf(['nowrap', 'wrap', 'wrap-reverse']);
37+
38+
const justifyContentPropTypes = PropTypes.oneOf([
39+
'start',
40+
'end',
41+
'center',
42+
'between',
43+
'around',
44+
]);
45+
46+
const spaceUtilSizePropTypes = PropTypes.oneOfType([
47+
PropTypes.number,
48+
PropTypes.oneOf(['auto']),
49+
]);
50+
51+
const spaceUtilPropTypes = PropTypes.oneOfType([
52+
spaceUtilSizePropTypes,
53+
PropTypes.shape({
54+
/**
55+
*
56+
* the key is the side of spacing utility,
57+
* t - for classes that set margin-top or padding-top
58+
* b - for classes that set margin-bottom or padding-bottom
59+
* l - for classes that set margin-left or padding-left
60+
* r - for classes that set margin-right or padding-right
61+
* x - for classes that set both *-left and *-right
62+
* y - for classes that set both *-top and *-bottom
63+
* all - for classes that set a margin or padding on all 4 sides of the element
64+
* (because we cannot set "" as key, use "all" to represent blank side here)
65+
* see the boostrap doc
66+
* https://getbootstrap.com/docs/4.0/utilities/spacing/#how-it-works
67+
*
68+
*/
69+
t: spaceUtilSizePropTypes,
70+
b: spaceUtilSizePropTypes,
71+
l: spaceUtilSizePropTypes,
72+
r: spaceUtilSizePropTypes,
73+
x: spaceUtilSizePropTypes,
74+
y: spaceUtilSizePropTypes,
75+
all: spaceUtilSizePropTypes,
76+
}),
77+
]);
78+
79+
const breakpointPropTypes = PropTypes.shape({
80+
/**
81+
*
82+
* display utility,
83+
* see the boostrap doc
84+
* https://getbootstrap.com/docs/4.0/utilities/display/
85+
*
86+
*/
87+
88+
display: displayPropTypes,
89+
90+
/**
91+
*
92+
* flex utilities,
93+
* see the boostrap doc
94+
* https://getbootstrap.com/docs/4.0/utilities/flex/
95+
*
96+
*/
97+
98+
alignContent: flexAlignPropTypes,
99+
alignItem: flexAlignPropTypes,
100+
alignSelf: flexAlignPropTypes,
101+
flexDirection: flexDirectionPropTypes,
102+
flexWrap: flexWrapPropTypes,
103+
justifyContent: justifyContentPropTypes,
104+
order: PropTypes.number,
105+
106+
/**
107+
*
108+
* spacing utilities,
109+
* see the boostrap doc
110+
* https://getbootstrap.com/docs/4.0/utilities/spacing/
111+
* can be number , "auto", or object,
112+
* if passing number or "auto" it will set spacing on all 4 sides of the element
113+
*
114+
*/
115+
116+
m: spaceUtilPropTypes,
117+
p: spaceUtilPropTypes,
118+
});
119+
120+
class Layout extends React.Component {
121+
/**
122+
*
123+
* Combination component of bootstrap V4 layout utility
124+
* (display / flex / spacing / visibility)
125+
* https://getbootstrap.com/docs/4.0/layout/utilities-for-layout/
126+
*
127+
*/
128+
129+
static propTypes = {
130+
/**
131+
*
132+
* for Extra small devices Phones (<576px)
133+
*
134+
*/
135+
xs: breakpointPropTypes,
136+
137+
/**
138+
*
139+
* for Small devices Tablets (≥576px)
140+
*
141+
*/
142+
sm: breakpointPropTypes,
143+
144+
/**
145+
*
146+
* for Medium devices Desktops (≥768px)
147+
*
148+
*/
149+
md: breakpointPropTypes,
150+
151+
/**
152+
*
153+
* for Large devices Desktops (≥992px)
154+
*
155+
*/
156+
lg: breakpointPropTypes,
157+
158+
/**
159+
*
160+
* for Large devices Desktops (≥1200px)
161+
*
162+
*/
163+
xl: breakpointPropTypes,
164+
165+
/**
166+
*
167+
* custom class name
168+
*
169+
*/
170+
className: PropTypes.string,
171+
172+
/**
173+
*
174+
* add `d-print-{display}` className on the element
175+
*
176+
*/
177+
print: displayPropTypes,
178+
/**
179+
*
180+
* add 'visible' or 'invisible' className on the element
181+
*
182+
*/
183+
visible: PropTypes.bool,
184+
componentClass: elementType,
185+
};
186+
187+
static defaultProps = {
188+
className: '',
189+
componentClass: 'div',
190+
};
191+
192+
buildVisibleClassName = (visible) => {
193+
if (typeof visible !== 'boolean') {
194+
return '';
195+
}
196+
return visible ? 'visible' : 'invisible';
197+
};
198+
199+
buildPrintClassName = (print) => {
200+
if (!print) {
201+
return '';
202+
}
203+
return `d-print-${print}`;
204+
};
205+
206+
buildLayoutClassName = (breakpoints) => {
207+
const classNameList = Object.keys(breakpoints).map((bp) =>
208+
this.buildBreakpointClassName(bp, breakpoints[bp]),
209+
);
210+
return classNames(classNameList);
211+
};
212+
213+
buildBreakpointClassName = (breakpoint, breakpointProps) => {
214+
if (typeof breakpointProps !== 'object') {
215+
return '';
216+
}
217+
const bpAbbrev = breakpoint === 'xs' ? '' : `${breakpoint}-`;
218+
const classNameList = Object.keys(breakpointProps).map((propName) => {
219+
const value = breakpointProps[propName];
220+
if (propName === 'm' || propName === 'p') {
221+
return this.buildSpacingClassName({ propName, value, bpAbbrev });
222+
}
223+
const suffix = `-${bpAbbrev}${value}`;
224+
let prefix = '';
225+
if (propName === 'display') {
226+
prefix = 'd';
227+
} else if (propName === 'flexDirection' || propName === 'flexWrap') {
228+
prefix = 'flex';
229+
} else {
230+
prefix = camelCaseToHyphen(propName);
231+
}
232+
return `${prefix}${suffix}`;
233+
});
234+
return classNames(classNameList);
235+
};
236+
237+
buildSpacingClassName = ({ propName, bpAbbrev, value }) => {
238+
if (typeof value === 'number' || value === 'auto') {
239+
const size = value;
240+
return `${propName}-${bpAbbrev}${size}`;
241+
}
242+
if (typeof value === 'object') {
243+
const classNameList = Object.keys(value).map((side) => {
244+
const size = value[side];
245+
const prefix = side === 'all' ? propName : `${propName}${side}`;
246+
return `${prefix}-${bpAbbrev}${size}`;
247+
});
248+
return classNames(classNameList);
249+
}
250+
return '';
251+
};
252+
253+
render() {
254+
const {
255+
className: customClassName,
256+
componentClass,
257+
print,
258+
visible,
259+
xs,
260+
sm,
261+
md,
262+
lg,
263+
xl,
264+
...props
265+
} = this.props;
266+
const className = classNames(
267+
this.buildVisibleClassName(visible),
268+
this.buildPrintClassName(print),
269+
this.buildLayoutClassName({ xs, sm, md, lg, xl }),
270+
customClassName,
271+
);
272+
const Component = componentClass;
273+
return <Component className={className} {...props} />;
274+
}
275+
}
276+
277+
export default Layout;

src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export { default as Figure } from './Figure';
9797
export { default as InputGroup } from './InputGroup';
9898
export type { InputGroupProps } from './InputGroup';
9999

100+
export { default as Layout } from './Layout';
101+
100102
export { default as ListGroup } from './ListGroup';
101103
export type { ListGroupProps } from './ListGroup';
102104

test/LayoutSpec.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
import { shallow } from 'enzyme';
3+
4+
import Layout from '../src/Layout';
5+
6+
describe('<Layout>', () => {
7+
it('Should render plain <div> when no prop', () => {
8+
const component = shallow(<Layout />);
9+
component.find('div').should.have.length(1);
10+
});
11+
it('Should not has class name when no prop', () => {
12+
const component = shallow(<Layout />);
13+
component.find('div').prop('className').should.equal('');
14+
});
15+
it('Should have `invisible` class name when visible={false}', () => {
16+
const component = shallow(<Layout visible={false} />);
17+
component.find('div').hasClass('invisible').should.equal(true);
18+
});
19+
it('Should have print display class name when we pass print prop', () => {
20+
const component = shallow(<Layout print="block" />);
21+
component.find('div').hasClass('d-print-block').should.equal(true);
22+
});
23+
it('Should have display utility class name with breakpoint prop', () => {
24+
const component = shallow(<Layout sm={{ display: 'inline-block' }} />);
25+
component.find('div').hasClass('d-sm-inline-block').should.equal(true);
26+
});
27+
it('Should not add breakpoint in class name with xs breakpoint prop', () => {
28+
const component = shallow(<Layout xs={{ display: 'inline-block' }} />);
29+
component.find('div').hasClass('d-inline-block').should.equal(true);
30+
});
31+
it('Should have flex direction utility with flexDirection in breakpoint prop', () => {
32+
const component = shallow(<Layout sm={{ flexDirection: 'row-reverse' }} />);
33+
component.find('div').hasClass('flex-sm-row-reverse').should.equal(true);
34+
});
35+
it('Should have align item utility with alignItem in breakpoint prop', () => {
36+
const component = shallow(<Layout sm={{ alignItem: 'center' }} />);
37+
component.find('div').hasClass('align-item-sm-center').should.equal(true);
38+
});
39+
it('Should have order utility with order number in breakpoint prop', () => {
40+
const component = shallow(<Layout sm={{ order: 3 }} />);
41+
component.find('div').hasClass('order-sm-3').should.equal(true);
42+
});
43+
it('Should have custom class name with className prop', () => {
44+
const customClass = 'customClass';
45+
const component = shallow(
46+
<Layout sm={{ order: 3 }} className={customClass} />,
47+
);
48+
component.find('div').hasClass(customClass).should.equal(true);
49+
});
50+
it('Should have margin spacing class name with number prop', () => {
51+
const component = shallow(<Layout xs={{ m: 3 }} />);
52+
component.find('div').hasClass('m-3').should.equal(true);
53+
});
54+
it('Should have margin spacing class name with auto prop', () => {
55+
const component = shallow(<Layout sm={{ m: 'auto' }} />);
56+
component.find('div').hasClass('m-sm-auto').should.equal(true);
57+
});
58+
it('Should have margin spacing class name with object prop', () => {
59+
const component = shallow(<Layout sm={{ m: { x: 1, l: 'auto' } }} />);
60+
component.find('div').hasClass('mx-sm-1').should.equal(true);
61+
component.find('div').hasClass('ml-sm-auto').should.equal(true);
62+
});
63+
it('Should have margin spacing class name with `all` prop', () => {
64+
const component = shallow(<Layout sm={{ p: { all: 1 } }} />);
65+
component.find('div').hasClass('p-sm-1').should.equal(true);
66+
});
67+
it('Should handle multiple breakpoint props', () => {
68+
const component = shallow(
69+
<Layout sm={{ m: { x: 1, l: 'auto' } }} xs={{ m: { x: 0, l: 1 } }} />,
70+
);
71+
component.find('div').hasClass('mx-sm-1').should.equal(true);
72+
component.find('div').hasClass('ml-sm-auto').should.equal(true);
73+
component.find('div').hasClass('mx-0').should.equal(true);
74+
component.find('div').hasClass('ml-1').should.equal(true);
75+
});
76+
});

0 commit comments

Comments
 (0)