Skip to content

Commit

Permalink
feat(MediaQuery): convert to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed May 26, 2022
1 parent 2f8e27c commit 1a23e4f
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 75 deletions.
1 change: 1 addition & 0 deletions packages/dnb-eufemia/src/shared/Context.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const prepareContext = (props = {}) => {
return context.translation
},
locale: null,
breakpoints: null,
locales,
skeleton: null,
// All eufemia components because of Typescript:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import PropTypes from 'prop-types' //
import { isTrue } from './component-helper'
import Context from './Context'
import {
Expand All @@ -8,30 +7,22 @@ import {
onMediaQueryChange,
isMatchMediaSupported,
} from './MediaQueryUtils'
import type {
MediaQueryProps,
MediaQueryState,
MediaQueryListener,
} from './MediaQueryUtils'

export type { MediaQueryProps }

export { onMediaQueryChange }

export default class MediaQuery extends React.PureComponent {
export default class MediaQuery extends React.PureComponent<
MediaQueryProps,
MediaQueryState
> {
static contextType = Context
static propTypes = {
matchOnSSR: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
when: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
PropTypes.object,
]),
not: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
query: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.node,
}

static defaultProps = {
matchOnSSR: null,
when: null,
not: null,
query: null,
children: null,
}
listener: MediaQueryListener

state = {
match: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
import { isTrue, toKebabCase, warn } from './component-helper'
import { IS_IE11 } from './helpers'

export const defaultBreakpoints = {
export type MediaQuerySizes =
| 'small'
| 'medium'
| 'large'
| 'x-large'
| 'xx-large'
export type MediaQueryBreakpoints = Partial<
Record<MediaQuerySizes, string>
>

export const defaultBreakpoints: MediaQueryBreakpoints = {
small: '40em',
medium: '50em',
large: '60em',
'x-large': '72em',
'xx-large': '80em',
}

export type MediaQueryCondition =
| {
min?: number | string | MediaQuerySizes
max?: number | string | MediaQuerySizes
screen?: boolean
minWidth?: number | string | MediaQuerySizes
maxWidth?: number | string | MediaQuerySizes
orientation?: string
handheld?: boolean
not?: boolean
all?: boolean
monochrome?: boolean
aspectRatio?: string
}
| string

export type MediaQueryProperties = {
/**
* A MediaQuery as a string similar to the CSS API, but without `@media`.
*/
query?: MediaQueryCondition

/**
* Define a list of sizes to match, given as an object `{ min: 'small', max: 'medium' }` or as an array `[{ min: 'small', max: 'medium' }, { min: 'medium', max: 'large' }]`.
*/
when?: MediaQueryCondition | Array<MediaQueryCondition>

/**
* Reverts the defined queries as a whole.
*/
not?: boolean

/**
* For debugging
*/
log?: boolean
} & MediaQueryCondition

export type MediaQueryListener = () => void

export type MediaQueryProps = {
/**
* If set to true, it will match and return the given children during SSR.
*/
matchOnSSR?: boolean
children?: React.ReactNode
} & MediaQueryProperties

export type MediaQueryState = {
match?: boolean | null
mediaQueryList?: { matches: boolean }
}

/**
* Adds a listener to a given MediaQuery
*
Expand All @@ -20,10 +83,10 @@ export const defaultBreakpoints = {
* @returns function to remove listeners when called
*/
export function onMediaQueryChange(
property,
callback,
property: MediaQueryProperties | string,
callback?: (matches: boolean, mediaQueryList: MediaQueryList) => void,
{ runOnInit = false } = {}
) {
): MediaQueryListener {
let query = property
let when = null
let not = null
Expand All @@ -50,7 +113,7 @@ export function onMediaQueryChange(
*
* @returns boolean
*/
export const isMatchMediaSupported = () =>
export const isMatchMediaSupported = (): boolean =>
typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined'

/**
Expand All @@ -64,9 +127,9 @@ export const isMatchMediaSupported = () =>
* @returns MediaQueryList type
*/
export function makeMediaQueryList(
{ query, when, not = null, log = false } = {},
breakpoints = null
) {
{ query, when, not = null, log = false }: MediaQueryProperties = {},
breakpoints: MediaQueryBreakpoints = null
): MediaQueryList {
if (!isMatchMediaSupported()) {
return null
}
Expand All @@ -92,7 +155,10 @@ export function makeMediaQueryList(
* @param {function} callback callback function
* @returns function to remove listeners when called
*/
export function createMediaQueryListener(mediaQueryList, callback) {
export function createMediaQueryListener(
mediaQueryList: MediaQueryList,
callback: (matches: boolean, event) => void
): MediaQueryListener {
if (!mediaQueryList) {
warn('Invalid MediaQueryList was given')
return () => null
Expand Down Expand Up @@ -130,8 +196,8 @@ export function createMediaQueryListener(mediaQueryList, callback) {
* @returns media queries as a string
*/
export function buildQuery(
{ query = null, when = null, not = null } = {},
breakpoints
{ query = null, when = null, not = null }: MediaQueryProperties = {},
breakpoints?: MediaQueryBreakpoints
) {
if (when) {
if (typeof when === 'string') {
Expand Down Expand Up @@ -161,7 +227,7 @@ export function buildQuery(
}

if (isTrue(not)) {
query = reverseQuery(query)
query = reverseQuery(String(query))
}

return query || 'not'
Expand All @@ -172,7 +238,7 @@ export function buildQuery(
* @param {string} query media query to reverse with "not"
* @returns reversed query
*/
function reverseQuery(query) {
function reverseQuery(query: string) {
if (query.startsWith('not')) {
return query.replace(/^not +/, '')
}
Expand All @@ -188,7 +254,10 @@ function reverseQuery(query) {
* @param {*} queries media query definitions as an array that contains either strings with size types or an object with all the media query definitions
* @returns array of JavaScript based queries
*/
function combineQueries(queries, breakpoints = null) {
function combineQueries(
queries: Array<MediaQueryCondition>,
breakpoints: MediaQueryBreakpoints = null
) {
return queries
.reduce((listOfQueries, when, i, arr) => {
if (breakpoints) {
Expand Down Expand Up @@ -223,7 +292,7 @@ function combineQueries(queries, breakpoints = null) {
* If custom breakpoints are given, we order them by the value
* and return again an object as before
*/
function mergeBreakpoints(breakpoints) {
function mergeBreakpoints(breakpoints: MediaQueryBreakpoints) {
return Object.entries({
...defaultBreakpoints,
...breakpoints,
Expand All @@ -241,7 +310,10 @@ function mergeBreakpoints(breakpoints) {
* @param {array|object|string} query media query definitions
* @returns media query string
*/
export function convertToMediaQuery(query, breakpoints = null) {
export function convertToMediaQuery(
query: MediaQueryCondition | Array<MediaQueryCondition>,
breakpoints: MediaQueryBreakpoints = null
): string {
if (typeof query === 'string') {
return query
}
Expand All @@ -255,7 +327,7 @@ export function convertToMediaQuery(query, breakpoints = null) {
}

return acc
}, '')
}, '') as string
}

// Handling single media query
Expand All @@ -267,39 +339,45 @@ export function convertToMediaQuery(query, breakpoints = null) {
* @param {object} obj Object with PascalCase defined properties and respective values
* @returns media query string
*/
function objToMediaQuery(obj, breakpoints = null) {
function objToMediaQuery(
obj: MediaQueryCondition,
breakpoints: MediaQueryBreakpoints = null
): string {
let hasNot = false
let query = Object.keys(obj).reduce((acc, feature) => {
let value = obj[feature]
feature = toKebabCase(feature)

if (feature === 'not') {
hasNot = true
return acc
}
let query: string | Array<null> = Object.keys(obj).reduce(
(acc, feature) => {
let value = obj[feature]
feature = toKebabCase(feature)

if (feature === 'not') {
hasNot = true
return acc
}

if (feature === 'min' || feature === 'max') {
feature = `${feature}-width`
}
if (feature === 'min' || feature === 'max') {
feature = `${feature}-width`
}

// Add em to dimension features
if (typeof value === 'number' && /[height|width]$/.test(feature)) {
value = value + 'em'
}
// Add em to dimension features
if (typeof value === 'number' && /[height|width]$/.test(feature)) {
value = value + 'em'
}

if (value === true) {
acc.push(feature)
} else if (value === false) {
acc.push('not ' + feature)
} else {
value = getValueByFeature(value, breakpoints)
if (typeof value !== 'undefined') {
acc.push(`(${feature}: ${value})`)
if (value === true) {
acc.push(feature)
} else if (value === false) {
acc.push('not ' + feature)
} else {
value = getValueByFeature(value, breakpoints)
if (typeof value !== 'undefined') {
acc.push(`(${feature}: ${value})`)
}
}
}

return acc
}, [])
return acc
},
[]
)

if (Array.isArray(query)) {
query = query.length > 0 ? query.join(' and ') : query.join('')
Expand All @@ -319,7 +397,10 @@ function objToMediaQuery(obj, breakpoints = null) {
* @param {object} types breakpoints
* @returns corrected value
*/
function getValueByFeature(value, types = {}) {
function getValueByFeature(
value: string,
types: MediaQueryBreakpoints = null
) {
types = types || defaultBreakpoints
if (Object.prototype.hasOwnProperty.call(types, value)) {
value = types[value]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ import { mount } from '../../core/jest/jestSetup'
import MatchMediaMock from 'jest-matchmedia-mock'
import MediaQuery from '../MediaQuery'
import Provider from '../Provider'
import { isMatchMediaSupported } from '../MediaQueryUtils'
import { isMatchMediaSupported as _isMatchMediaSupported } from '../MediaQueryUtils'

jest.mock('../MediaQueryUtils', () => ({
...jest.requireActual('../MediaQueryUtils'),
isMatchMediaSupported: jest.fn(),
}))
const isMatchMediaSupported = _isMatchMediaSupported as jest.Mock

jest.mock('../MediaQueryUtils', () => {
const orig = jest.requireActual('../MediaQueryUtils')
return {
...orig,
isMatchMediaSupported: jest.fn(),
}
})

describe('MediaQuery', () => {
let matchMedia
let matchMedia: MatchMediaMock

beforeAll(() => {
matchMedia = new MatchMediaMock()
Expand Down Expand Up @@ -130,7 +135,7 @@ describe('MediaQuery', () => {
isMatchMediaSupported.mockReturnValue(false)

const Comp = mount(
<MediaQuery matchOnSSR when={{ someting: 'what-every' }}>
<MediaQuery matchOnSSR when={{ min: 'what-every' }}>
medium
</MediaQuery>
)
Expand Down
Loading

0 comments on commit 1a23e4f

Please sign in to comment.