Skip to content

Commit 1ee34bf

Browse files
committed
feat: add computed breakpoints
1 parent 65b4745 commit 1ee34bf

File tree

15 files changed

+202
-58
lines changed

15 files changed

+202
-58
lines changed

Diff for: .github/workflows/node.js.yml

+7
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ jobs:
2121

2222
steps:
2323
- uses: actions/checkout@v2
24+
2425
- name: Use Node.js ${{ matrix.node-version }}
2526
uses: actions/setup-node@v1
2627
with:
2728
node-version: ${{ matrix.node-version }}
29+
2830
- run: npm install -g lerna start-server-and-test
2931
- run: lerna bootstrap
3032
- run: lerna run --scope vue-screen build --stream
3133
- run: lerna run --scope vue-screen test --stream
3234
- run: start-server-and-test "lerna run --scope vue-screen-examples serve --stream" http://localhost:8081 "lerna run --scope vue-screen-e2e cy:run --stream"
35+
36+
- name: Coverage
37+
uses: codecov/codecov-action@v1
38+
with:
39+
token: ${{ secrets.CODECOV_TOKEN }}

Diff for: .gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ dist
33
!packages/docs/src/.vitepress/dist
44
.nuxt
55
packages/e2e/cypress/screenshots
6-
.DS_Store
6+
.DS_Store
7+
coverage

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
[![Build Status](https://github.com/reegodev/vue-screen/workflows/Node.js%20CI/badge.svg)](https://github.com/reegodev/vue-screen/actions)
33
[![npm version](https://img.shields.io/npm/v/vue-screen/next)](https://www.npmjs.com/package/vue-screen)
44
[![npm downloads](https://img.shields.io/npm/dm/vue-screen)](https://www.npmjs.com/package/vue-screen)
5+
[![codecov](https://codecov.io/gh/reegodev/vue-screen/branch/master/graph/badge.svg?token=KTHOOUSHFJ)](https://codecov.io/gh/reegodev/vue-screen)
56

67
<br>
78

Diff for: packages/docs/src/.vitepress/config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ if (process.env.NODE_ENV === 'production') {
1616
}
1717

1818
module.exports = {
19-
base: '/vue-screen/',
19+
// base: '/vue-screen/',
2020
lang: 'en-US',
2121
title: 'Vue-Screen',
2222
description: '"Reactive screen size and media query states for Vue components. Integrates with most UI frameworks out of the box.',

Diff for: packages/docs/src/guide/configuration/composition-api.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,50 @@ The value can either be a string literal, with one of the supported UI framework
5757
or an object that specifies a custom grid:
5858
5959
```ts
60-
Record<string, number | string>
60+
Record<string, number | string | ComputedBreakpoint>
6161
```
6262
6363
For example:
6464
```js
65+
import { useGrid } from 'vue-screen'
66+
6567
useGrid({
6668
phone: '340px',
6769
tablet: 768,
6870
desktop: '32em',
6971
})
7072
```
73+
<br>
74+
75+
#### Computed breakpoints
76+
77+
Aside from using direct breakpoint values, you can also specify breakpoints that depend on other breakpoints, for example:
78+
79+
```js
80+
import { useGrid } from 'vue-screen'
7181

82+
useGrid({
83+
phone: '340px',
84+
tablet: 768,
85+
desktop: '32em',
86+
tabletAndDown: grid => !grid.desktop,
87+
})
88+
```
89+
90+
In this example, the breakpoint `tabletAndDown` will be recalculated everytime `desktop` breakpoint changes.
91+
92+
<br>
93+
94+
#### Extending default UI frameworks breakpoints
95+
96+
Sometimes it's useful to add new breakpoints to the default configuration of one of the supported UI frameworks.<br>
97+
You can do that by using the `extendGrid` helper:
98+
99+
```js
100+
import { useGrid, extendGrid } from 'vue-screen'
101+
102+
useGrid(extendGrid('tailwind', {
103+
mdAndDown: grid => !grid.lg
104+
}))
105+
```
72106

Diff for: packages/docs/src/guide/upgrading.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ editLink: true
66

77
## Configuration
88
v2 has brought a few breaking changes on the configuration.
9-
To see how to configure the libary please refer to the [configuration section](/vue-screen/guide/configuration/composition-api).<br>
10-
Callbacks have been removed and there is no plan on adding them back at the moment.
9+
To see how to configure the library please refer to the [configuration section](/vue-screen/guide/configuration/composition-api).<br>
1110

1211
## API
1312

Diff for: packages/docs/src/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ editLink: true
33
title: Reactive screen size and media query states for VueJs
44
---
55
::: warning
6-
Version 2.0 only supports Vue 3 and is in early alpha version. There might still be API changes before final release.<br> v1 docs are available [here](https://github.com/reegodev/vue-screen/tree/v1.5.3#vuescreen)
6+
Version 2.0 only supports Vue 3 and is still in alpha.<br> v1 docs are available [here](https://github.com/reegodev/vue-screen/tree/v1.5.3#vuescreen)
77
:::
88
<br>
99
<div style="text-align: center">

Diff for: packages/lib/__tests__/useGrid.ts

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
createGridObject,
33
createConfigFromLiteral,
4-
useGrid
4+
useGrid,
5+
extendGrid,
6+
getCurrentBreakpoint
57
} from '../src/useGrid'
68

79
import { GridDefinitionLiteral } from '../src/types/grid'
@@ -38,7 +40,7 @@ describe('useGrid', () => {
3840
expect(Object.values(gridObject).every(value => value === false)).toBe(true)
3941
})
4042

41-
it('creates a reactive grid object', async () => {
43+
it('creates a reactive grid object from a literal', async () => {
4244
const grid = useGrid('bulma')
4345

4446
expect(grid.tablet).toBe(false)
@@ -51,4 +53,62 @@ describe('useGrid', () => {
5153
expect(grid.tablet).toBe(true)
5254
})
5355

56+
it('creates a reactive grid object from a custom config', async () => {
57+
const grid = useGrid({
58+
a: 0,
59+
b: 1,
60+
c: 2,
61+
})
62+
63+
expect(grid.a).toBe(false)
64+
65+
await new Promise((resolve) => {
66+
grid.a = true
67+
watchEffect(() => resolve(grid))
68+
})
69+
70+
expect(grid.a).toBe(true)
71+
})
72+
73+
it('extends a grid literal config', async () => {
74+
const grid = useGrid(extendGrid('bulma', {
75+
tabletAndBelow: grid => !grid.desktop
76+
}))
77+
78+
const allBreakpoints = Object.keys(grids['bulma']).concat(['breakpoint', 'tabletAndBelow'])
79+
expect(Object.keys(grid).every(key => allBreakpoints.includes(key))).toBe(true)
80+
})
81+
82+
})
83+
84+
describe('getCurrentBreakpoint', () => {
85+
it('returns an empty value if no breakpoint is active', () => {
86+
const config = {
87+
a: 0,
88+
b: 1,
89+
c: 2
90+
}
91+
const gridObject = {
92+
a: false,
93+
b: false,
94+
c: false,
95+
}
96+
97+
expect(getCurrentBreakpoint(config, gridObject)).toBe('')
98+
})
99+
100+
it('returns the currently active breakpoint', () => {
101+
const config = {
102+
a: 0,
103+
b: 1,
104+
c: 2
105+
}
106+
const gridObject = {
107+
a: false,
108+
b: true,
109+
c: true,
110+
}
111+
112+
expect(getCurrentBreakpoint(config, gridObject)).toBe('c')
113+
})
54114
})

Diff for: packages/lib/__tests__/useScreen.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('useScreen', () => {
3333
touch: false,
3434
} as ScreenConfig
3535

36-
const screen = useScreen(config)
36+
const screen = useScreen(config, 300)
3737

3838
expect(screen).toStrictEqual({
3939
resolution: `${config.width}x${config.height}`,

Diff for: packages/lib/jest.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ module.exports = {
1010
"^.+\\.(ts|tsx)$": "ts-jest"
1111
},
1212
"testEnvironment": "node",
13+
collectCoverage: true,
14+
coveragePathIgnorePatterns: ['src/utils.ts', 'src/grids'],
15+
coverageReporters: ['lcov', 'html', 'text'],
1316
}

Diff for: packages/lib/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useScreen } from './useScreen'
2-
import { useGrid } from './useGrid'
2+
import { useGrid, extendGrid } from './useGrid'
33
import { plugin } from './plugin'
44
import grids from './grids'
55

66
export {
77
useScreen,
88
useGrid,
9+
extendGrid,
910
grids,
1011
}
1112

Diff for: packages/lib/src/types/grid.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
SemanticUi,
1414
} from '../grids'
1515

16-
export type Custom = Record<string, number | string>
16+
export type ComputedBreakpoint = (grid: GridObject<Custom>) => boolean
17+
18+
export type Custom = Record<string, number | string | ComputedBreakpoint>
1719
export type CustomObject = Record<string, boolean>
1820

1921
export type SupportedGridType =

Diff for: packages/lib/src/useGrid.ts

+79-46
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
GridDefinitionCustomObject,
99
GridObjectLiteral,
1010
SupportedGridType,
11-
CustomObject
11+
CustomObject,
12+
ComputedBreakpoint
1213
} from './types/grid'
1314
import grids from './grids'
1415
import { reactive, onUnmounted, getCurrentInstance } from 'vue'
15-
import { inBrowser } from './utils'
16+
import { inBrowser, debounce } from './utils'
1617

1718
export const DEFAULT_GRID_FRAMEWORK = 'tailwind'
1819

@@ -33,54 +34,81 @@ export const createConfigFromLiteral = (literal: GridDefinitionLiteral): GridTyp
3334
return grids[literal]
3435
}
3536

36-
export const getCurrentBreakpoint = (object: CustomObject): string => {
37-
const current = Object.keys(object).filter(key => !['breakpoint'].includes(key)).reverse().find(key => object[key])
37+
export const getCurrentBreakpoint = (config: Custom, object: CustomObject): string => {
38+
const current = Object.keys(object)
39+
.filter(key => {
40+
return !['breakpoint'].includes(key) && typeof config[key] !== 'function'
41+
})
42+
.reverse()
43+
.find(key => object[key])
3844
return current || ''
3945
}
4046

47+
/* istanbul ignore next */
48+
export const updateComputedProperties = (config: Custom, object: CustomObject): void => {
49+
Object.keys(config)
50+
.filter((breakpoint) => {
51+
return typeof config[breakpoint] === 'function'
52+
})
53+
.forEach((breakpoint) => {
54+
const fn = config[breakpoint] as ComputedBreakpoint
55+
object[breakpoint] = fn.call(null, object)
56+
})
57+
}
58+
59+
const debouncedUpdateComputedProperties = debounce(updateComputedProperties, 100)
60+
61+
/* istanbul ignore next */
4162
export const createMediaQueries = (config: Custom, object: CustomObject & { breakpoint: keyof CustomObject }): void => {
42-
Object.keys(config).forEach((breakpoint) => {
43-
let width = config[breakpoint]
44-
45-
if (typeof width === 'number') {
46-
width = `${width}px`
47-
} else {
48-
width = width.toString()
49-
}
50-
51-
const onChange = (event: MediaQueryListEvent) => {
52-
object[breakpoint] = event.matches
53-
object.breakpoint = getCurrentBreakpoint(object)
54-
}
55-
56-
const query = window.matchMedia(`(min-width: ${width})`)
57-
if ('addEventListener' in query) {
58-
query.addEventListener('change', onChange);
59-
} else {
60-
// https://github.com/reegodev/vue-screen/issues/30
61-
// query.addListener is not deprecated for iOS 12
62-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63-
(query as any).addListener(onChange)
64-
}
65-
object[breakpoint] = query.matches
66-
object.breakpoint = getCurrentBreakpoint(object)
67-
68-
// Do not leak memory by keeping event listeners active.
69-
// This appears to work as expected, using useGrid() inside components
70-
// triggers this hook when they are destroyed.
71-
if (getCurrentInstance()) {
72-
onUnmounted(() => {
73-
if ('removeEventListener' in query) {
74-
query.removeEventListener('change', onChange);
75-
} else {
76-
// https://github.com/reegodev/vue-screen/issues/30
77-
// query.removeListener is not deprecated for iOS 12
78-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79-
(query as any).removeListener(onChange)
80-
}
81-
})
82-
}
83-
})
63+
Object.keys(config)
64+
.filter((breakpoint) => {
65+
return typeof config[breakpoint] !== 'function'
66+
})
67+
.forEach((breakpoint) => {
68+
let width = config[breakpoint]
69+
70+
if (typeof width === 'number') {
71+
width = `${width}px`
72+
} else {
73+
width = width.toString()
74+
}
75+
76+
const onChange = (event: MediaQueryListEvent) => {
77+
object[breakpoint] = event.matches
78+
object.breakpoint = getCurrentBreakpoint(config, object)
79+
debouncedUpdateComputedProperties(config, object)
80+
}
81+
82+
const query = window.matchMedia(`(min-width: ${width})`)
83+
if ('addEventListener' in query) {
84+
query.addEventListener('change', onChange);
85+
} else {
86+
// https://github.com/reegodev/vue-screen/issues/30
87+
// query.addListener is not deprecated for iOS 12
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
(query as any).addListener(onChange)
90+
}
91+
object[breakpoint] = query.matches
92+
object.breakpoint = getCurrentBreakpoint(config, object)
93+
94+
// Do not leak memory by keeping event listeners active.
95+
// This appears to work as expected, using useGrid() inside components
96+
// triggers this hook when they are destroyed.
97+
if (getCurrentInstance()) {
98+
onUnmounted(() => {
99+
if ('removeEventListener' in query) {
100+
query.removeEventListener('change', onChange);
101+
} else {
102+
// https://github.com/reegodev/vue-screen/issues/30
103+
// query.removeListener is not deprecated for iOS 12
104+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105+
(query as any).removeListener(onChange)
106+
}
107+
})
108+
}
109+
})
110+
111+
updateComputedProperties(config, object)
84112
}
85113

86114
export function useGrid<T extends GridDefinitionLiteral> (gridConfig: T): GridObjectLiteral<T>
@@ -98,9 +126,14 @@ export function useGrid (
98126

99127
const gridObject = reactive(createGridObject(config))
100128

129+
/* istanbul ignore if */
101130
if (inBrowser) {
102131
createMediaQueries(config as Custom, gridObject)
103132
}
104133

105134
return gridObject
106135
}
136+
137+
export const extendGrid = <T extends GridDefinitionLiteral>(literalConfig: T, extraProperties: GridDefinitionCustomObject): GridDefinitionCustomObject => {
138+
return Object.assign({}, createConfigFromLiteral(literalConfig), extraProperties)
139+
}

0 commit comments

Comments
 (0)