Skip to content

Commit

Permalink
feat(Grid): Add grid support for multiple device visibility breakpoin…
Browse files Browse the repository at this point in the history
…ts (#1347)

* feat(Grid): Add grid support for multiple device visibility breakpoints

* style(tests): fix description [ci skip]

* feat(Grid): Add grid support for multiple device visibility breakpoints

* feat(Grid): Add grid support for multiple device visibility breakpoints

* breaking(Grid): Add grid support for multiple device visibility breakpoints

* breaking(Grid): Add grid support for multiple device visibility breakpoints

* feat(Grid): Add grid support for multiple device visibility breakpoints
  • Loading branch information
layershifter authored and levithomason committed Mar 18, 2017
1 parent 6cc4884 commit 4ca4d03
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { Grid, Segment } from 'semantic-ui-react'

const GridExampleDeviceVisibility = () => (
const GridExampleOnly = () => (
<Grid>
<Grid.Row columns={2} only='large screen'>
<Grid.Column>
Expand Down Expand Up @@ -55,7 +55,7 @@ const GridExampleDeviceVisibility = () => (
<Segment>Computer</Segment>
</Grid.Column>
</Grid.Row>
<Grid.Row only='tablet'>
<Grid.Row columns={3} only='tablet'>
<Grid.Column>
<Segment>Tablet</Segment>
</Grid.Column>
Expand All @@ -69,4 +69,4 @@ const GridExampleDeviceVisibility = () => (
</Grid>
)

export default GridExampleDeviceVisibility
export default GridExampleOnly
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { Grid, Segment } from 'semantic-ui-react'

const GridExampleOnlyMultiple = () => (
<Grid>
<Grid.Row columns={2} only='mobile tablet'>
<Grid.Column>
<Segment>Mobile</Segment>
</Grid.Column>
<Grid.Column>
<Segment>Tablet</Segment>
</Grid.Column>
</Grid.Row>

<Grid.Row columns={2} only='tablet computer'>
<Grid.Column>
<Segment>Tablet</Segment>
</Grid.Column>
<Grid.Column>
<Segment>Computer</Segment>
</Grid.Column>
</Grid.Row>

<Grid.Row columns={2} only='large screen widescreen'>
<Grid.Column>
<Segment>Large Screen</Segment>
</Grid.Column>
<Grid.Column>
<Segment>Widescreen</Segment>
</Grid.Column>
</Grid.Row>
</Grid>
)

export default GridExampleOnlyMultiple
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ const GridResponsiveVariationsExamples = () => (
<ComponentExample
title='Device Visibility'
description='A columns or row can appear only for a specific device, or screen sizes.'
examplePath='collections/Grid/ResponsiveVariations/GridExampleDeviceVisibility'
examplePath='collections/Grid/ResponsiveVariations/GridExampleOnly'
/>
<ComponentExample examplePath='collections/Grid/ResponsiveVariations/GridExampleOnlyMultiple' />

<ComponentExample
title='Responsive Width'
Expand Down
2 changes: 1 addition & 1 deletion src/collections/Grid/GridColumn.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SemanticWIDTHS
} from '../..';

export type GridOnlyProp = 'computer' | 'large screen' | 'mobile' | 'tablet mobile' | 'tablet' | 'widescreen';
export type GridOnlyProp = string | 'computer' | 'largeScreen' | 'mobile' | 'tablet mobile' | 'tablet' | 'widescreen';

interface GridColumnProps {
[key: string]: any;
Expand Down
7 changes: 4 additions & 3 deletions src/collections/Grid/GridColumn.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
META,
SUI,
useKeyOnly,
useOnlyProp,
useTextAlignProp,
useValueAndKey,
useVerticalAlignProp,
Expand Down Expand Up @@ -38,9 +39,9 @@ function GridColumn(props) {
const classes = cx(
color,
useKeyOnly(stretched, 'stretched'),
useOnlyProp(only, 'only'),
useTextAlignProp(textAlign),
useValueAndKey(floated, 'floated'),
useValueAndKey(only, 'only'),
useVerticalAlignProp(verticalAlign),
useWidthProp(computer, 'wide computer'),
useWidthProp(largeScreen, 'wide large screen'),
Expand Down Expand Up @@ -88,8 +89,8 @@ GridColumn.propTypes = {
/** A column can specify a width for a mobile device. */
mobile: PropTypes.oneOf(SUI.WIDTHS),

/** A column can appear only for a specific device, or screen sizes. */
only: PropTypes.oneOf(['computer', 'large screen', 'mobile', 'tablet mobile', 'tablet', 'widescreen']),
/** A row can appear only for a specific device, or screen sizes. */
only: customPropTypes.onlyProp(SUI.VISIBILITY),

/** A column can stretch its contents to take up the entire grid or row height. */
stretched: PropTypes.bool,
Expand Down
5 changes: 3 additions & 2 deletions src/collections/Grid/GridRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
META,
SUI,
useKeyOnly,
useOnlyProp,
useTextAlignProp,
useValueAndKey,
useVerticalAlignProp,
Expand Down Expand Up @@ -37,8 +38,8 @@ function GridRow(props) {
useKeyOnly(centered, 'centered'),
useKeyOnly(divided, 'divided'),
useKeyOnly(stretched, 'stretched'),
useOnlyProp(only),
useTextAlignProp(textAlign),
useValueAndKey(only, 'only'),
useValueAndKey(reversed, 'reversed'),
useVerticalAlignProp(verticalAlign),
useWidthProp(columns, 'column', true),
Expand Down Expand Up @@ -80,7 +81,7 @@ GridRow.propTypes = {
divided: PropTypes.bool,

/** A row can appear only for a specific device, or screen sizes. */
only: PropTypes.oneOf(['computer', 'large screen', 'mobile', 'tablet mobile', 'tablet', 'widescreen']),
only: customPropTypes.onlyProp(SUI.VISIBILITY),

/** A row can specify that its columns should reverse order at different device sizes. */
reversed: PropTypes.oneOf([
Expand Down
2 changes: 2 additions & 0 deletions src/lib/SUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const SIZES = ['mini', 'tiny', 'small', 'medium', 'large', 'big', 'huge',
export const TEXT_ALIGNMENTS = ['left', 'center', 'right', 'justified']
export const VERTICAL_ALIGNMENTS = ['bottom', 'middle', 'top']

export const VISIBILITY = ['mobile', 'tablet', 'computer', 'large screen', 'widescreen']

export const WIDTHS = [
..._.keys(numberToWordMap),
..._.keys(numberToWordMap).map(Number),
Expand Down
76 changes: 49 additions & 27 deletions src/lib/classNameBuilders.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { numberToWord } from './numberToWord'

/*
* There are 4 prop patterns used to build up the className for a component.
* There are 3 prop patterns used to build up the className for a component.
* Each utility here is meant for use in a classnames() argument.
*
* There is no util for valueOnly() because it would simply return val.
* Use the prop value inline instead.
* <Label size='big' />
* <div class="ui big label"></div>
*/
import { numberToWord } from './numberToWord'

/**
* Props where only the prop key is used in the className.
Expand Down Expand Up @@ -51,36 +52,25 @@ export const useKeyOrValueAndKey = (val, key) => val && (val === true ? key : `$
//

/**
* Create "X", "X wide" and "equal width" classNames.
* "X" is a numberToWord value and "wide" is configurable.
* @param {*} val The prop value
* @param {string} [widthClass=''] The class
* @param {boolean} [canEqual=false] Flag that indicates possibility of "equal" value
* The "only" prop implements control of visibility classes for Grid subcomponents.
*
* @example
* <Grid columns='equal' />
* <div class="ui equal width grid"></div>
*
* <Form widths='equal' />
* <div class="ui equal width form"></div>
*
* <FieldGroup widths='equal' />
* <div class="equal width fields"></div>
* @param {*} val The value of the "only" prop
*
* @example
* <Grid columns={4} />
* <div class="ui four column grid"></div>
* <Grid.Row only='mobile' />
* <Grid.Row only='mobile tablet' />
* <div class="mobile only row"></div>
* <div class="mobile only tablet only row"></div>
*/
export const useWidthProp = (val, widthClass = '', canEqual = false) => {
if (canEqual && val === 'equal') {
return 'equal width'
}
const valType = typeof val
if ((valType === 'string' || valType === 'number') && widthClass) {
return `${numberToWord(val)} ${widthClass}`
}
return numberToWord(val)
export const useOnlyProp = val => {
if (!val || val === true) return null

return val.replace('large screen', 'large-screen')
.split(' ')
.map(prop => `${prop.replace('-', ' ')} only`)
.join(' ')
}

/**
* The "textAlign" prop follows the useValueAndKey except when the value is "justified'.
* In this case, only the class "justified" is used, ignoring the "aligned" class.
Expand All @@ -106,3 +96,35 @@ export const useTextAlignProp = val => val === 'justified' ? 'justified' : useVa
* <div class="ui middle aligned grid"></div>
*/
export const useVerticalAlignProp = val => useValueAndKey(val, 'aligned')

/**
* Create "X", "X wide" and "equal width" classNames.
* "X" is a numberToWord value and "wide" is configurable.
* @param {*} val The prop value
* @param {string} [widthClass=''] The class
* @param {boolean} [canEqual=false] Flag that indicates possibility of "equal" value
*
* @example
* <Grid columns='equal' />
* <div class="ui equal width grid"></div>
*
* <Form widths='equal' />
* <div class="ui equal width form"></div>
*
* <FieldGroup widths='equal' />
* <div class="equal width fields"></div>
*
* @example
* <Grid columns={4} />
* <div class="ui four column grid"></div>
*/
export const useWidthProp = (val, widthClass = '', canEqual = false) => {
if (canEqual && val === 'equal') {
return 'equal width'
}
const valType = typeof val
if ((valType === 'string' || valType === 'number') && widthClass) {
return `${numberToWord(val)} ${widthClass}`
}
return numberToWord(val)
}
31 changes: 31 additions & 0 deletions src/lib/customPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,37 @@ export const demand = (requiredProps) => {
}
}

/**
* Ensure an only prop contains a string with only possible values.
* @param {string[]} possible An array of possible values to prop.
*/
export const onlyProp = possible => {
return (props, propName, componentName) => {
if (!Array.isArray(possible)) {
throw new Error([
'Invalid argument supplied to some, expected an instance of array.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}

const propValue = props[propName]

// skip if prop is undefined
if (_.isNil(propValue) || propValue === false) return

const values = propValue
.replace('large screen', 'large-screen')
.split(' ')
.map(val => _.trim(val).replace('-', ' '))
const invalid = _.difference(values, possible)

// fail only if there are invalid values
if (invalid.length > 0) {
return new Error(`\`${propName}\` prop in \`${componentName}\` has invalid values: \`${invalid.join('`, `')}\`.`)
}
}
}

/**
* Ensure a component can render as a node passed as a prop value in place of children.
*/
Expand Down
6 changes: 4 additions & 2 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ export * as childrenUtils from './childrenUtils'

export {
useKeyOnly,
useValueAndKey,
useKeyOrValueAndKey,
useWidthProp,
useValueAndKey,

useOnlyProp,
useTextAlignProp,
useVerticalAlignProp,
useWidthProp,
} from './classNameBuilders'

export * as customPropTypes from './customPropTypes'
Expand Down
4 changes: 1 addition & 3 deletions test/specs/collections/Grid/GridColumn-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('GridColumn', () => {
common.isConformant(GridColumn)
common.rendersChildren(GridColumn)

common.implementsOnlyProp(GridColumn)
common.implementsTextAlignProp(GridColumn)
common.implementsVerticalAlignProp(GridColumn)

Expand Down Expand Up @@ -41,9 +42,6 @@ describe('GridColumn', () => {
})

common.propKeyAndValueToClassName(GridColumn, 'floated', SUI.FLOATS)
common.propKeyAndValueToClassName(GridColumn, 'only', [
'computer', 'large screen', 'mobile', 'tablet mobile', 'tablet', 'widescreen',
])

common.propKeyOnlyToClassName(GridColumn, 'stretched')

Expand Down
4 changes: 1 addition & 3 deletions test/specs/collections/Grid/GridRow-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ describe('GridRow', () => {
common.isConformant(GridRow)
common.rendersChildren(GridRow)

common.implementsOnlyProp(GridRow)
common.implementsTextAlignProp(GridRow)
common.implementsVerticalAlignProp(GridRow)
common.implementsWidthProp(GridRow, SUI.WIDTHS, {
propKey: 'columns',
widthClass: 'column',
})

common.propKeyAndValueToClassName(GridRow, 'only', [
'computer', 'large screen', 'mobile', 'tablet mobile', 'tablet', 'widescreen',
])
common.propKeyAndValueToClassName(GridRow, 'reversed', [
['computer', 'computer vertically', 'mobile', 'mobile vertically', 'tablet', 'tablet vertically'],
])
Expand Down
29 changes: 29 additions & 0 deletions test/specs/commonTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,35 @@ export const implementsImageProp = (Component, options = {}) => {
})
}

/**
* Assert that a Component correctly implements the "only" prop.
* @param {React.Component|Function} Component The component to test.
*/
export const implementsOnlyProp = Component => {
const { assertRequired } = commonTestHelpers('propKeyAndValueToClassName', Component)
const propValues = SUI.VISIBILITY

describe('only (common)', () => {
assertRequired(Component, 'a `Component`')

_noDefaultClassNameFromProp(Component, 'only', propValues)
_noClassNameFromBoolProps(Component, 'only', propValues)

propValues.forEach(propVal => {
it(`adds "${propVal} only" to className`, () => {
shallow(createElement(Component, { only: propVal })).should.have.className(`${propVal} only`)
})
})

it('adds all possible values to className', () => {
const className = propValues.map(prop => `${prop} only`).join(' ')
const propValue = propValues.join(' ')

shallow(createElement(Component, { only: propValue })).should.have.className(className)
})
})
}

/**
* Assert that a Component correctly implements the "textAlign" prop.
* @param {React.Component|Function} Component The component to test.
Expand Down

0 comments on commit 4ca4d03

Please sign in to comment.