Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Mar 18, 2017
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