Skip to content

Commit

Permalink
Media
Browse files Browse the repository at this point in the history
- add lodash isEmpty, isPlainObject
- update README
- add additional test coverage
- add "or" media query concatentation
  • Loading branch information
heyjul3s committed Nov 5, 2020
1 parent 8138d29 commit 36b764b
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 37 deletions.
117 changes: 106 additions & 11 deletions packages/media/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@artifak/media`

Although Artifak helps you to write less media queries, there are also many other types of media queries available now which focuses more on querying devices and accessibilty. The Artifak Media library essentially helps with that and is a styled-component mixin to help you write media queries for your styled-components. Docs are also available at [Artifak Media](https://www.artifak.dev/?content=media).
Although Artifak helps you to write less media queries, there are also many other types of media queries available now which focuses more on querying devices and accessibilty. The Artifak Media library essentially helps with that and is a styled-component mixin to help you write media queries for your styled-components. _Note that the CSS media query operator **"not"** is not supported for the time being._ Docs are also available at [Artifak Media](https://www.artifak.dev/?content=media).

## Installation

Expand All @@ -20,7 +20,23 @@ npm install @artifak/media

### media

Simply import **media** and the media query rule that you desire to use and include it in your styled component. Below is a simple rule that would equate to **@media screen and (min-width: 30em)**
Simply import **media** and the media query rule that you desire to use and include it in your styled component.

At its most basic usage, you can specify for example a min-width rule like so:

```ts
import { media } from '@artifak/media';

const MyArticle = styled.article`
${media({ width: '>= 30em' })`
background: orange;
`}
`;
```

#### Querying Media With "And"

For concatenation using the **"and"** keyword, below is a simple rule that would equate to **"@media screen and (min-width: 30em)"**. Basically, just append the rules to the query object to execute a media query with **"and"** keywords.

```ts
import { media, screen } from '@artifak/media';
Expand All @@ -32,34 +48,113 @@ const MyArticle = styled.article`
`;
```

You can also specify min and max widths like so:
#### Querying Media With "Or"

For **"or"** statements, simply provide another query object as argument. Below will equate to: **"@media screen and (min-width: 30em), (orientation: landscape)"**.

```ts
import { media, screen, landscape } from '@artifak/media';

const MyArticle = styled.article`
${media({ screen, width: '>= 30em' }, { landscape })`
background: orange;
`}
`;
```

#### Querying Media With Min, Max or Single Value

Keys to queries that offer min/max values are

- width
- height
- ratio (which is for **aspect-ratio**)
- res (which is for **resolution**)

For min values, use the **">="** operator.

```ts
import { media, screen } from '@artifak/media';

const MyArticle = styled.article`
${media({ screen, width: '30em >= width <= 50em' })`
${media({ screen, width: '>= 30em' })`
background: orange;
`}
`;
```

For max values, use the **"<="** operator.

```ts
import { media, screen } from '@artifak/media';

const MyArticle = styled.article`
${media({ screen, width: '<= 30em' })`
background: orange;
`}
`;
```

Here is another example with other queries you can include:
For both **min** and **max** values as per below, is an example for min and max widths.

```ts
import { media, all, landscape, displayFullScreen } from '@artifak/media';
import { media, screen } from '@artifak/media';

const MyArticle = styled.article`
${media({ all, landscape, displayFullScreen })`
${media({ screen, width: '30em >= width <= 50em' })`
background: orange;
`}
`;
```

The above will equate to: **@media all and (orientation: landscape) and (display-mode: fullscreen)**
If for some reason you only want a single value for these properties, it can be applied like so.

### Available Media Queries
```ts
import { media, screen } from '@artifak/media';

Below are a list of currently available media queries
const MyArticle = styled.article`
${media({
screen,
width: '30em',
height: '50em',
ratio: '1/3',
res: '72dpi'
})`
background: orange;
`}
`;
```

Due to some newer queries that do not have sufficient browser support, they are omitted from the library for the time being. These are:
## Media Queries

Apart from the common **width, height, aspect-ratio and resolution** queries, below are a list of currently available media queries. Due to some newer queries not having sufficient browser support, they are omitted from the library for the time being.

### Available Queries

| Property Name | Value |
| ----------------- | ------------------------------------- |
| all | all |
| screen | screen |
| onlyScreen | only screen |
| print | print |
| onlyPrint | only print |
| speech | speech |
| onlySpeech | only speech |
| hover | hover: hover |
| hoverNone | hover: none |
| anyHover | any-hover: hover |
| anyHoverNone | any-hover: none |
| pointer | pointer: pointer |
| pointerNone | pointer: none |
| anyPointer | any-pointer: pointer |
| anyPointerNone | any-pointer: none |
| displayFullScreen | display-mode: fullscreen |
| displayMinUI | display-mode: minimal-ui |
| displayStandalone | display-mode: standalone |
| displayBrowser | display-mode: browser |
| portrait | orientation: portrait |
| landscape | orientation: landscape |
| darkColorScheme | prefers-color-scheme: dark |
| lightColorScheme | prefers-color-scheme: light |
| reducedMotion | prefers-reduced-motion: reduce |
| reducedMotionAny | prefers-reduced-motion: no-preference |
80 changes: 72 additions & 8 deletions packages/media/__tests__/media.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// import { css } from 'styled-components';
import { media, formatQueryValue, createQueryValuesArray } from '../src/media';
import {
media,
formatQueryValue,
createQueryArray,
createQueryString,
filterQueryArgs
} from '../src/media';

describe('@artifak/media', () => {
describe('media - main function that runs all necessary operations to return a styled-component media query', () => {
Expand All @@ -14,29 +19,88 @@ describe('@artifak/media', () => {
})
).toBeDefined();
});

it('should handle multiple media query arg objects', () => {
expect(
media(
{ width: '30em >= width <= 50em', screen: 'screen' },
{ landscape: 'orientation: landscape' }
)
).toBeDefined();
});
});

describe('filterQueryArgs', () => {
it('should return an empty array when provided with falsy args', () => {
expect(filterQueryArgs(void 0)).toEqual([]);
expect(filterQueryArgs(null)).toEqual([]);
expect(filterQueryArgs([void 0, null])).toEqual([]);
});

it('should return an empty array when provided with empty query objects', () => {
expect(filterQueryArgs([{}])).toEqual([]);
});

it('should return an array with query objects when provided with non-empty query objects', () => {
expect(filterQueryArgs([{ width: '>= 30em' }])).toEqual([
{ width: '>= 30em' }
]);

expect(
filterQueryArgs([{ screen: 'screen' }, { width: '>= 30em' }])
).toEqual([{ screen: 'screen' }, { width: '>= 30em' }]);
});
});

describe('createQueryString - concatenates query string arrays with "OR" if applies', () => {
it('should return media query string with formatted value when provided a single prop in a single query object', () => {
expect(createQueryString([{ width: '>= 30em' }])).toEqual(
'(min-width: 30em)'
);
});

it('should return media query string concatenated with "AND" when given a single query object', () => {
expect(
createQueryString([
{
screen: 'screen',
width: '30em >= width <= 50em'
}
])
).toEqual('screen and (min-width: 30em) and (max-width: 50em)');
});

it('should return media query string concatenated with "OR" when given multiple query object', () => {
expect(
createQueryString([
{ screen: 'screen', width: '>= 30em' },
{ landscape: 'orientation: landscape' }
])
).toEqual('screen and (min-width: 30em), (orientation: landscape)');
});
});

describe('createQueryValuesArray - loops through object, processes media query rules and returns them in a string array ', () => {
describe('createQueryArray - processes media query rules and returns them in a string concatenated with "AND" if applies', () => {
it('should return an empty array when no args are passed', () => {
expect(createQueryValuesArray()).toEqual([]);
expect(createQueryArray()).toEqual('');
});

it('should return an array with a media type screen string and any-hover rule string', () => {
expect(
createQueryValuesArray({
createQueryArray({
screen: 'screen',
anyHover: 'any-hover: hover'
})
).toEqual(['screen', '(any-hover: hover)']);
).toEqual('screen and (any-hover: hover)');
});

it('should return an array with a media type screen string and any-hover rule string', () => {
expect(
createQueryValuesArray({
createQueryArray({
screen: 'screen',
width: '30em >= width <= 50em'
})
).toEqual(['screen', '(min-width: 30em)', '(max-width: 50em)']);
).toEqual('screen and (min-width: 30em) and (max-width: 50em)');
});
});

Expand Down
4 changes: 4 additions & 0 deletions packages/media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@
"react": ">=16.11.0",
"react-dom": ">=16.11.0",
"styled-components": ">=4.4.1"
},
"dependencies": {
"lodash.isempty": "^4.4.0",
"lodash.isplainobject": "^4.0.6"
}
}
49 changes: 31 additions & 18 deletions packages/media/src/media.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import { css } from 'styled-components';
import { css, ThemedCssFunction } from 'styled-components';
import isEmpty from 'lodash.isempty';
import isPlainObject from 'lodash.isplainobject';
import { mediaBounds, boundaryTypes } from './mediaBoundaries';
import { isNonEmptyString } from './utils';
import { Media } from './typings';

export function media(queries: Partial<Media>) {
return (...styles) =>
!!queries && Object.keys(queries).length
/* eslint-disable @typescript-eslint/no-explicit-any */
export function media(...queries: Partial<Media>[]): ThemedCssFunction<any> {
const args = filterQueryArgs(queries);

return (first, ...interpolations) =>
args.length
? css`
@media ${createQueryValuesArray(queries).join(' and ')} {
${css(...styles)}
@media ${createQueryString(queries)} {
${css(first, ...interpolations)}
}
`
: css(...styles);
: css(first, ...interpolations);
}

export function createQueryValuesArray(queries: Partial<Media> = {}): string[] {
return Object.keys(queries).reduce((acc, key) => {
if (boundaryTypes.hasOwnProperty(key)) {
return acc.concat(mediaBounds({ [key]: queries[key] }));
}
export function filterQueryArgs(queries: Partial<Media>[]): Partial<Media>[] {
return !!queries
? queries.filter(query => isPlainObject(query) && !isEmpty(query))
: [];
}

return acc.concat(formatQueryValue(queries[key]));
}, []);
export function createQueryString(queries: Partial<Media>[]): string {
return queries.map(query => createQueryArray(query)).join(', ');
}

export function createQueryArray(queries: Partial<Media> = {}): string {
return Object.keys(queries)
.reduce(
(acc, key) =>
boundaryTypes.hasOwnProperty(key)
? acc.concat(mediaBounds({ [key]: queries[key] }))
: acc.concat(formatQueryValue(queries[key])),
[]
)
.join(' and ');
}

export function formatQueryValue(value: string): string {
Expand All @@ -31,7 +48,3 @@ export function formatQueryValue(value: string): string {

return !!value && value.includes(':') ? `(${value})` : value;
}

// export function joinQueries(queries): string {
// return `@media ${createQueryValuesArray(queries).join(' and ')} {`;
// }
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10271,6 +10271,11 @@ lodash.ismatch@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=

lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=

lodash.kebabcase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
Expand Down

0 comments on commit 36b764b

Please sign in to comment.