Skip to content

Commit

Permalink
Adds the "query" utility function
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Apr 12, 2020
1 parent ba295c7 commit e327095
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 77 deletions.
1 change: 1 addition & 0 deletions packages/atomic-layout-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ export { default as warn } from './utils/functions/warn'
export { default as compose } from './utils/functions/compose'
export { default as throttle } from './utils/functions/throttle'
export { default as transformNumeric } from './utils/math/transformNumeric'
export { default as memoizeWith } from './utils/functions/memoizeWith'
2 changes: 2 additions & 0 deletions packages/atomic-layout-emotion/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export {
useResponsiveProps,
useResponsiveComponent,
useResponsiveQuery,
/* Utils */
query,
} from '../../atomic-layout/src'
82 changes: 5 additions & 77 deletions packages/atomic-layout/src/hooks/useResponsiveQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { useState, useMemo } from 'react'
import {
Breakpoint,
Layout,
openBreakpoint,
closeBreakpoint,
mergeAreaRecords,
} from '@atomic-layout/core'
import { createMediaQuery } from './useMediaQuery'
import { useState } from 'react'
import { Breakpoint } from '@atomic-layout/core'
import useViewportChange from './useViewportChange'
import { query } from '../utils/query'

export type BreakpointRef = Breakpoint | string

Expand All @@ -32,59 +26,6 @@ export interface ResponsiveQueryParams {
except?: boolean
}

export const resolveBreakpoint = (breakpointRef: BreakpointRef): Breakpoint => {
return typeof breakpointRef === 'string'
? Layout.breakpoints[breakpointRef]
: breakpointRef
}

const getBreakpoints = (
exactBreakpoint: BreakpointRef,
from: BreakpointRef,
to: BreakpointRef,
except: boolean,
) => {
// Explicit breakpoint
if (exactBreakpoint) {
return resolveBreakpoint(exactBreakpoint)
}

const minBreakpoint = resolveBreakpoint(from)
const maxBreakpoint = resolveBreakpoint(to)

// Bell, __/--\__
if (minBreakpoint && maxBreakpoint && !except) {
const mergedAreaRecord = mergeAreaRecords(
{
behavior: 'down',
breakpoint: maxBreakpoint,
},
{
behavior: 'up',
breakpoint: minBreakpoint,
},
false,
)

return mergedAreaRecord.breakpoint
}

// Notch, --\__/--
if (minBreakpoint && maxBreakpoint && except) {
return [closeBreakpoint(minBreakpoint), openBreakpoint(maxBreakpoint)]
}

// High-pass, __/--
if (minBreakpoint && !maxBreakpoint) {
return openBreakpoint(minBreakpoint)
}

// Low-pass, --\__
if (!minBreakpoint && maxBreakpoint) {
return closeBreakpoint(maxBreakpoint)
}
}

/**
* Returns a boolean indicating that the current viewport matches the given responsive query.
* @example
Expand All @@ -96,23 +37,10 @@ export default function useResponsiveQuery(
initialMatches: boolean = false,
): boolean {
const [matches, setMatches] = useState(initialMatches)

const { for: exactBreakpoint, from, to, except } = params
const breakpointsList = useMemo(() => {
const breakpoints = getBreakpoints(exactBreakpoint, from, to, except)

if (!breakpoints) {
return []
}

return [].concat(breakpoints).map(createMediaQuery)
}, [exactBreakpoint, from, to, except])
const mediaQuery = query(params)

useViewportChange(() => {
const hasMatchingQuery = breakpointsList.some((mediaQuery) => {
return matchMedia(mediaQuery).matches
})

const { matches: hasMatchingQuery } = matchMedia(mediaQuery)
setMatches(hasMatchingQuery)
})

Expand Down
3 changes: 3 additions & 0 deletions packages/atomic-layout/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ export { default as useResponsiveValue } from './hooks/useResponsiveValue'
export { default as useResponsiveProps } from './hooks/useResponsiveProps'
export { default as useResponsiveComponent } from './hooks/useResponsiveComponent'
export { default as useResponsiveQuery } from './hooks/useResponsiveQuery'

/* Utils */
export { query } from './utils/query'
89 changes: 89 additions & 0 deletions packages/atomic-layout/src/utils/getBreakpointsByQuery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { getBreakpointsByQuery } from './getBreakpointsByQuery'

describe('getBreakpointsByQuery', () => {
describe('given an exact breakpoint (for)', () => {
let result: ReturnType<typeof getBreakpointsByQuery>

beforeAll(() => {
result = getBreakpointsByQuery({ for: 'md' })
})

it('should return a single enclosed breakpoint', () => {
expect(result).toEqual([
{
minWidth: '768px',
maxWidth: '991px',
},
])
})
})

describe('given a high-pass breakpoint range (from)', () => {
let result: ReturnType<typeof getBreakpointsByQuery>

beforeAll(() => {
result = getBreakpointsByQuery({ from: 'sm' })
})

it('should return breakpoints for that high-pass range', () => {
expect(result).toEqual([
{
minWidth: '576px',
},
])
})
})

describe('given a low-pass breakpoint range (to)', () => {
let result: ReturnType<typeof getBreakpointsByQuery>

beforeAll(() => {
result = getBreakpointsByQuery({ to: 'md' })
})

it('should return breakpoints for that low-pass range', () => {
expect(result).toEqual([
{
maxWidth: '767px',
},
])
})
})

describe.only('given a bell breakpoint range (from/to)', () => {
let result: ReturnType<typeof getBreakpointsByQuery>

beforeAll(() => {
result = getBreakpointsByQuery({ from: 'sm', to: 'lg' })
})

it('should return breakpoints for that inclusive range', () => {
expect(result).toEqual([
{
minWidth: '576px',
maxWidth: 'calc(992px - 1px)',
},
])
})
})

describe('given a notch breakpoint range (except/from/to)', () => {
let result: ReturnType<typeof getBreakpointsByQuery>

beforeAll(() => {
result = getBreakpointsByQuery({ except: true, from: 'sm', to: 'lg' })
})

it('should return breakpoints for that exclusive range', () => {
expect(result).toEqual([
{
maxWidth: '575px',
},
{
maxWidth: undefined,
minWidth: '992px',
},
])
})
})
})
73 changes: 73 additions & 0 deletions packages/atomic-layout/src/utils/getBreakpointsByQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
mergeAreaRecords,
Breakpoint,
Layout,
openBreakpoint,
closeBreakpoint,
} from '@atomic-layout/core'
import {
ResponsiveQueryParams,
BreakpointRef,
} from '../hooks/useResponsiveQuery'

export const resolveBreakpoint = (breakpointRef: BreakpointRef): Breakpoint => {
return typeof breakpointRef === 'string'
? Layout.breakpoints[breakpointRef]
: breakpointRef
}

/**
* Returns a list of breakpoints based on a responsive query.
* @example
* getBreakpointsByQuery({ from: 'md' })
* // [{ minWidth: 768 }]
* getBreakpointsByQuery({ from: 'sm', to: 'lg' })
* // [{ minWidth: 576 }, { maxWidth: 1199 }]
*/
export const getBreakpointsByQuery = (
params: ResponsiveQueryParams,
): Breakpoint[] => {
const { for: exactBreakpoint, from, to, except } = params

// Explicit breakpoint
if (exactBreakpoint) {
return [resolveBreakpoint(exactBreakpoint)]
}

const minBreakpoint = resolveBreakpoint(from)
const maxBreakpoint = resolveBreakpoint(to)

// Bell, __/--\__
if (minBreakpoint && maxBreakpoint && !except) {
const mergedAreaRecord = mergeAreaRecords(
{
behavior: 'down',
breakpoint: maxBreakpoint,
},
{
behavior: 'up',
breakpoint: minBreakpoint,
},
false,
)

return [mergedAreaRecord.breakpoint]
}

// Notch, --\__/--
if (minBreakpoint && maxBreakpoint && except) {
return [closeBreakpoint(minBreakpoint), openBreakpoint(maxBreakpoint)]
}

// High-pass, __/--
if (minBreakpoint && !maxBreakpoint) {
return [openBreakpoint(minBreakpoint)]
}

// Low-pass, --\__
if (!minBreakpoint && maxBreakpoint) {
return [closeBreakpoint(maxBreakpoint)]
}

return []
}
65 changes: 65 additions & 0 deletions packages/atomic-layout/src/utils/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { query } from './query'

describe('query', () => {
describe('given an exact breakpoint (for)', () => {
let result: ReturnType<typeof query>

beforeAll(() => {
result = query({ for: 'md' })
})

it('should return an enclosed media query for the given breakpoint', () => {
expect(result).toEqual('(min-width:768px) and (max-width:991px)')
})
})

describe('given a high-pass breakpoint range (from)', () => {
let result: ReturnType<typeof query>

beforeAll(() => {
result = query({ from: 'md' })
})

it('should return an enclosed media query for the given range', () => {
expect(result).toEqual('(min-width:768px)')
})
})

describe('given a low-pass breakpoint range (to)', () => {
let result: ReturnType<typeof query>

beforeAll(() => {
result = query({ to: 'lg' })
})

it('should return an enclosed media query for the given range', () => {
expect(result).toEqual('(max-width:991px)')
})
})

describe('given a bell breakpoint range (from/to)', () => {
let result: ReturnType<typeof query>

beforeAll(() => {
result = query({ from: 'sm', to: 'lg' })
})

it('should return an enclosed media query for the given range', () => {
expect(result).toEqual(
'(min-width:576px) and (max-width:calc(992px - 1px))',
)
})
})

describe('given a notch breakpoint range (except/from/to)', () => {
let result: ReturnType<typeof query>

beforeAll(() => {
result = query({ except: true, from: 'sm', to: 'lg' })
})

it('should return an enclosed media query for the given range', () => {
expect(result).toEqual('(max-width:575px),(min-width:992px)')
})
})
})
30 changes: 30 additions & 0 deletions packages/atomic-layout/src/utils/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { memoizeWith } from '@atomic-layout/core'
import { createMediaQuery } from '../hooks/useMediaQuery'
import { ResponsiveQueryParams } from '../hooks/useResponsiveQuery'
import { getBreakpointsByQuery } from './getBreakpointsByQuery'

const createQuery = (params: ResponsiveQueryParams): string => {
const breakpoints = getBreakpointsByQuery(params)
return breakpoints.map(createMediaQuery).join(params.except ? ',' : ' ')
}

/**
* Converts a responsive query into a @media query string.
* @example
* query({ from: 'md' })
* // (min-width: 768px)
* query({ from: 'sm', to: 'lg' })
* // (min-width: 576px) and (max-width: 1199px)
* query({ for: 'md' })
* // (min-width: 768px) and (max-width: 991px)
* query({ except: true, from: 'sm', to: 'lg' })
* // (max-width: 575px), (min-width: 992px)
*/
export const query = memoizeWith<typeof createQuery>((params) => {
return Object.entries(params)
.filter(([, value]) => value != null)
.reduce((acc, [key, value]) => {
return acc.concat(`${key}=${value.toString()}`)
}, [])
.join()
})(createQuery)

0 comments on commit e327095

Please sign in to comment.