Skip to content

Commit

Permalink
feat(resolution): Expand media query support
Browse files Browse the repository at this point in the history
- Lower the min width to account for iPhone 5 resolution
- Add support for OR logic within displayAliases
  - This is to support applying a media query when either the screen or the device
    with are within certain values

BREAKING CHANGE layout defaults change
  • Loading branch information
Oscar Bartra committed Feb 18, 2018
1 parent 3f8386c commit f19590e
Show file tree
Hide file tree
Showing 18 changed files with 538 additions and 355 deletions.
2 changes: 1 addition & 1 deletion src/cxs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import cxs from 'cxs/monolithic'

cxs.prefix('xnr_')
cxs.prefix('gym_')

export default cxs
44 changes: 30 additions & 14 deletions src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,40 @@ export default {
columns: 12, // number of columns used in the layout
displayAliases: {
// aliases used for the different display breakpoints:
small: {
// - "small" alias used when width is less than 600px
maxWidth: '599px',
},
medium: {
// - "medium" alias used when width is between 600px and 900px
minWidth: '600px',
maxWidth: '899px',
},
large: {
// - "large" alias used when width is equal or greater than 900px
minWidth: '900px',
},
small: [
{
// - "small" alias used when width is less than 600px
maxWidth: '599px',
},
{
maxDeviceWidth: '599px',
},
],
medium: [
{
// - "medium" alias used when width is between 600px and 900px
minWidth: '600px',
maxWidth: '899px',
},
{
minDeviceWidth: '600px',
maxDeviceWidth: '899px',
},
],
large: [
{
// - "large" alias used when width is equal or greater than 900px
minWidth: '900px',
},
{
minDeviceWidth: '900px',
},
],
},
fallbackDisplayKey: 'default', // key to use when a display alias is omitted or non matching
gutter: 3, // value (in base units) that separates columns horizontally
maxPageWidth: 153, // maximum page width (in base units) 153 * base (8px) = 1224px
minPageWidth: 50, // minimum page width (in base units) 50 * base (8px) = 400px
minPageWidth: 40, // minimum page width (in base units) 40 * base (8px) = 320px = iPhone5 screen width
pageMargin: {
// page margins (in base units) for each display breakpoint
small: 1,
Expand Down
4 changes: 2 additions & 2 deletions src/dev/dev.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ function aliasMarginQuery(query, padding) {
}
return {
leftMargin: {
[`@media screen and ${query}`]: {
[query]: {
width: `${padding}px`,
},
},
rightMargin: {
[`@media screen and ${query}`]: {
[query]: {
width: `${padding}px`,
},
},
Expand Down
5 changes: 0 additions & 5 deletions src/errors/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ const addError =
process.env.NODE_ENV === 'production' ? addProdError : addDevError
const errors = {}

addError(
errors,
'INVALIDMEDIAKEY',
`Specified query is invalid. Only the following keys are allowed: "minWidth", "maxWidth", "minHeight", "maxHeight", "aspectRatio" and "orientation".`
)
addError(
errors,
'INVALIDSPACING',
Expand Down
4 changes: 2 additions & 2 deletions src/root/root.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ function addRootPadding(query, padding) {
function addChildPadding(query, padding) {
return {
root: {
[`@media screen and ${query}`]: smallRoot,
[query]: smallRoot,
},
child: {
[`@media screen and ${query}`]: {
[query]: {
flexShrink: 0,
width: `calc(100% + ${padding}px)`,
},
Expand Down
20 changes: 7 additions & 13 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,13 @@ export type SpacingAliases = {
+[spacingAlias: string]: number,
}

type ResolutionKeys =
| 'minWidth'
| 'maxWidth'
| 'minHeight'
| 'maxHeight'
| 'aspectRatio'
| 'orientation'

export type DisplayAliases = {
+[displayAlias: string]: {
+[ResolutionKeys]: string,
},
}
type DisplayProperties = {|
+[resolutionKey: string]: string,
|}

export type DisplayAliases = {|
+[displayAlias: string]: DisplayProperties | Array<DisplayProperties>,
|}

export type ConfigProviderContext = {
+gymnast?: {|
Expand Down
4 changes: 4 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const splitPattern = /(?:(?:\s+)?,(?:\s+)?|\s+)/
export const noop: Noop = () => null
export const times = (n: number) =>
new Array(n).fill(undefined).map((val, index) => index)
export const kebabCase = (str: string) =>
str
.replace(/^[A-Z]/, match => match.toLowerCase())
.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)

export function validateSpacingProps(props: SpacingProps) {
if (process.env.NODE_ENV === 'production') {
Expand Down
25 changes: 25 additions & 0 deletions src/utils/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getCSS,
getValue,
getValues,
kebabCase,
parseSpacing,
replaceSpacingAliases,
toCXS,
Expand Down Expand Up @@ -353,3 +354,27 @@ describe('accumulateOver', () => {
})
})
})

describe('kebabCase', () => {
it('should work with empty strings', () => {
expect(kebabCase('')).toEqual('')
})

it('should not modify strings without upper case characters', () => {
const sample = 'this-is-a-test1#'

expect(kebabCase(sample)).toEqual(sample)
})

it('should lower case upper case letters and add a preceding dash', () => {
const sample = 'thisWillHaveDashes'

expect(kebabCase(sample)).toEqual('this-will-have-dashes')
})

it('should not add an additional dash if the first letter is capitalize', () => {
const sample = 'Lowercase'

expect(kebabCase(sample)).toEqual('lowercase')
})
})
44 changes: 18 additions & 26 deletions src/withResolution/withResolution.logic.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// @flow
import type { DisplayValues, DisplayAliases } from '../types'
import { splitPattern } from '../utils'
import log from '../log'
import errors from '../errors'
import { splitPattern, kebabCase } from '../utils'
import defaults from '../defaults'

export type ShouldShow = { [string]: boolean }
Expand Down Expand Up @@ -59,31 +57,25 @@ export function getSingleResolutionProps({
return propsCopy
}

const queriesMap = {
minWidth: 'min-width',
maxWidth: 'max-width',
minHeight: 'min-height',
maxHeight: 'max-height',
aspectRatio: 'aspect-ratio',
orientation: 'orientation',
}

export function getMediaQuery(
range: string,
displayAliases: DisplayAliases
displayAliases: DisplayAliases,
prefix: string = '@media '
): string {
const response = []
Object.keys(displayAliases[range]).forEach(key => {
if (key in queriesMap) {
const value = displayAliases[range][key]

response.push(`(${queriesMap[key]}: ${value})`)
} else {
log.error(errors.INVALIDMEDIAKEY, `"${key}" used`)
}
})

return response.join(' and ')
const displayPropertiesArray =
displayAliases[range] instanceof Array
? displayAliases[range]
: [displayAliases[range]]
const response = displayPropertiesArray
.map(
displayProperties =>
`${prefix}${Object.keys(displayProperties)
.map(key => `(${kebabCase(key)}: ${displayProperties[key]})`)
.join(' and ')}`
)
.join(', ')

return response
}

export function getMediaQueries(
Expand All @@ -94,7 +86,7 @@ export function getMediaQueries(

return showArray
.filter(range => range in displayAliases)
.map(range => [range, getMediaQuery(range, displayAliases)])
.map(range => [range, getMediaQuery(range, displayAliases, '')])
.reduce((acc, [range, query]) => {
if (query) {
return {
Expand Down
71 changes: 68 additions & 3 deletions src/withResolution/withResolution.logic.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getMediaQueries,
getMediaQuery,
getSingleResolutionProps,
checkShouldShow,
} from './withResolution.logic'
Expand All @@ -16,6 +17,17 @@ describe('getMediaQueries', () => {
expect(out).toEqual({ test: '(min-width: 1px) and (max-width: 2px)' })
})

it('should return the same output when using array syntax', () => {
const test = {
minWidth: '1px',
maxWidth: '2px',
}
const out1 = getMediaQueries('test', { test })
const out2 = getMediaQueries('test', { test: [test] })

expect(out1).toEqual(out2)
})

it('should return a max value only when no min value is provided', () => {
const out = getMediaQueries('test', {
test: {
Expand All @@ -26,6 +38,21 @@ describe('getMediaQueries', () => {
expect(out).toEqual({ test: '(max-width: 2px)' })
})

it('should concatenate multiple queries with commas', () => {
const out = getMediaQueries('test', {
test: [
{
maxWidth: '2px',
},
{
minWidth: '1px',
},
],
})

expect(out).toEqual({ test: '(max-width: 2px), (min-width: 1px)' })
})

it('should return a min value only when no max value is provided', () => {
const out = getMediaQueries('test', {
test: {
Expand All @@ -36,14 +63,52 @@ describe('getMediaQueries', () => {
expect(out).toEqual({ test: '(min-width: 1px)' })
})

it('should return an empty string if an invalid value is passed', () => {
it('should kebab case keys', () => {
const out = getMediaQueries('test2', {
test: {
test2: {
MaxAspectRatio: '1/2',
},
})

expect(out).toEqual({ test2: '(max-aspect-ratio: 1/2)' })
})

it('should include invalid keys', () => {
const out = getMediaQueries('test2', {
test2: {
invalidValue: 'meow',
},
})

expect(out).toEqual({})
expect(out).toEqual({ test2: '(invalid-value: meow)' })
})
})

describe('getMediaQuery', () => {
let sampleDisplayAliases

beforeEach(() => {
sampleDisplayAliases = {
test: [{ something: '3px' }, { somethingElse: '4px' }],
}
})

it('should include the prefix on every response', () => {
const out = getMediaQuery('test', sampleDisplayAliases, 'prefix ')

expect(out).toBe('prefix (something: 3px), prefix (something-else: 4px)')
})

it('should default to "@media " prefix', () => {
const out = getMediaQuery('test', sampleDisplayAliases)

expect(out).toBe('@media (something: 3px), @media (something-else: 4px)')
})

it('should remove prefix when "" is set', () => {
const out = getMediaQuery('test', sampleDisplayAliases, '')

expect(out).toBe('(something: 3px), (something-else: 4px)')
})
})

Expand Down
2 changes: 1 addition & 1 deletion storybook/stories/configProvider/columns.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { number } from '@storybook/addon-knobs'
import { ConfigProvider, Grid } from 'gymnast'
import { colors } from '../../shared'

export default function() {
export default () => {
const columns = number('Columns', 10, { range: true, min: 1, max: 24 })
const items = number('Items', 20, { range: true, min: 1, max: 48 })

Expand Down
38 changes: 38 additions & 0 deletions storybook/stories/configProvider/displayAliases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @flow
import * as React from 'react'
import { ConfigProvider, Grid } from 'gymnast'
import { colors } from '../../shared'

export default () => {
const displayAliases = {
test: {
minWidth: '351px',
maxWidth: '600px',
},
test2: [
{
maxWidth: '350px',
},
{
minWidth: '601px',
},
],
}

return (
<ConfigProvider displayAliases={displayAliases}>
<Grid justify="center">
<Grid show="test" style={colors.colors1} margin="L" size={6}>
<Grid justify="center" style={colors.colors2} padding="L">
I am only visible between 150-600px
</Grid>
</Grid>
<Grid show="test2" style={colors.colors2} margin="L" size={6}>
<Grid justify="center" style={colors.colors1} padding="L">
I am visible all other times!
</Grid>
</Grid>
</Grid>
</ConfigProvider>
)
}
Loading

0 comments on commit f19590e

Please sign in to comment.