diff --git a/CHANGELOG.md b/CHANGELOG.md index f490b989..15b1a3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [10.4.0](https://github.com/streamich/react-use/compare/v10.3.1...v10.4.0) (2019-08-02) + + +### Features + +* add useMountedState hook ([9081b99](https://github.com/streamich/react-use/commit/9081b99)) + +## [10.3.1](https://github.com/streamich/react-use/compare/v10.3.0...v10.3.1) (2019-08-02) + + +### Bug Fixes + +* **storybook:** fix useKeyboardJs import path ([b7481f6](https://github.com/streamich/react-use/commit/b7481f6)) +* **useKeyboardJs:** fix argument type error ([8c820ce](https://github.com/streamich/react-use/commit/8c820ce)) +* allow string list in useKeyboardJs hook ([aecbd0b](https://github.com/streamich/react-use/commit/aecbd0b)) + # [10.3.0](https://github.com/streamich/react-use/compare/v10.2.0...v10.3.0) (2019-07-26) diff --git a/README.md b/README.md index 0a36269d..9e97e95b 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,12 @@ - [`useTitle`](./docs/useTitle.md) — sets title of the page. - [`usePermission`](./docs/usePermission.md) — query permission status for browser APIs.
-
+
- [**Lifecycles**](./docs/Lifecycles.md) - [`useEffectOnce`](./docs/useEffectOnce.md) — a modified [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect) hook that only runs once. - [`useEvent`](./docs/useEvent.md) — subscribe to events. - [`useLifecycles`](./docs/useLifecycles.md) — calls `mount` and `unmount` callbacks. - - [`useRefMounted`](./docs/useRefMounted.md) — tracks if component is mounted. + - [`useMountedState`](./docs/useMountedState.md) and [`useRefMounted`](./docs/useRefMounted.md) — track if component is mounted. - [`usePromise`](./docs/usePromise.md) — resolves promise only while component is mounted. - [`useLogger`](./docs/useLogger.md) — logs in console as component goes through life-cycles. - [`useMount`](./docs/useMount.md) — calls `mount` callbacks. diff --git a/docs/useKeyboardJs.md b/docs/useKeyboardJs.md index 6dc09ef9..3df9d49d 100644 --- a/docs/useKeyboardJs.md +++ b/docs/useKeyboardJs.md @@ -37,5 +37,5 @@ yarn add keyboardjs ## Reference ```js -useKeyboardJs(combination: string): [isPressed: boolean, event?: KeyboardEvent] +useKeyboardJs(combination: string | string[]): [isPressed: boolean, event?: KeyboardEvent] ``` diff --git a/docs/useMountedState.md b/docs/useMountedState.md new file mode 100644 index 00000000..ae6ecace --- /dev/null +++ b/docs/useMountedState.md @@ -0,0 +1,25 @@ +# `useMountedState` + +Lifecycle hook providing ability to check component's mount state. +Gives a function that will return `true` if component mounted and `false` otherwise. + +## Usage + +```jsx +import * as React from 'react'; +import {useMountedState} from 'react-use'; + +const Demo = () => { + const isMounted = useMountedState(); + + React.useEffect(() => { + setTimeout(() => { + if (isMounted()) { + // ... + } else { + // ... + } + }, 1000); + }); +}; +``` diff --git a/package.json b/package.json index a3604150..b0473dfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-use", - "version": "10.3.0", + "version": "10.4.0", "description": "Collection of React Hooks", "main": "lib/index.js", "module": "esm/index.js", @@ -24,7 +24,7 @@ "clean": "rimraf lib storybook-static esm", "storybook": "start-storybook -p 6008", "storybook:build": "build-storybook", - "storybook:upload": "gh-pages -d storybook-static", + "storybook:upload": "gh-pages -d storybook-static --git \"$(which git)\"", "storybook:clean": "rimraf storybook-static", "release": "semantic-release" }, @@ -67,34 +67,35 @@ "@semantic-release/changelog": "3.0.4", "@semantic-release/git": "7.0.16", "@semantic-release/npm": "5.1.13", - "@storybook/addon-actions": "5.1.9", - "@storybook/addon-knobs": "5.1.9", - "@storybook/addon-notes": "5.1.9", - "@storybook/addon-options": "5.1.9", - "@storybook/react": "5.1.9", - "@types/jest": "24.0.15", + "@storybook/addon-actions": "5.1.10", + "@storybook/addon-knobs": "5.1.10", + "@storybook/addon-notes": "5.1.10", + "@storybook/addon-options": "5.1.10", + "@storybook/react": "5.1.10", + "@testing-library/react-hooks": "^1.1.0", + "@types/jest": "24.0.16", "@types/react": "16.8.23", "babel-core": "6.26.3", "babel-loader": "8.0.6", "babel-plugin-dynamic-import-node": "2.3.0", "fork-ts-checker-webpack-plugin": "1.4.3", - "gh-pages": "2.0.1", - "husky": "3.0.1", + "gh-pages": "2.1.0", + "husky": "3.0.2", "jest": "24.8.0", "keyboardjs": "2.5.1", "lint-staged": "9.2.1", - "markdown-loader": "5.0.0", + "markdown-loader": "5.1.0", "prettier": "1.17.1", "react": "16.8.6", "react-dom": "16.8.6", - "react-hooks-testing-library": "0.4.1", "react-spring": "8.0.27", + "react-test-renderer": "^16.8.6", "rebound": "0.1.0", "redux-logger": "3.0.6", "redux-thunk": "2.3.0", "rimraf": "2.6.3", "rxjs": "6.5.2", - "semantic-release": "15.13.18", + "semantic-release": "15.13.19", "ts-loader": "6.0.4", "ts-node": "8.3.0", "tslint": "5.18.0", diff --git a/src/__stories__/useKeyboardJs.story.tsx b/src/__stories__/useKeyboardJs.story.tsx index f9e83e9b..fd2f16cd 100644 --- a/src/__stories__/useKeyboardJs.story.tsx +++ b/src/__stories__/useKeyboardJs.story.tsx @@ -1,7 +1,7 @@ import { text, withKnobs } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useKeyboardJs } from '..'; +import useKeyboardJs from '../useKeyboardJs'; import { CenterStory } from './util/CenterStory'; import ShowDocs from './util/ShowDocs'; diff --git a/src/__stories__/useMountedState.story.tsx b/src/__stories__/useMountedState.story.tsx new file mode 100644 index 00000000..a4bcac43 --- /dev/null +++ b/src/__stories__/useMountedState.story.tsx @@ -0,0 +1,17 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useMountedState } from '..'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const isMounted = useMountedState(); + const [, updateState] = React.useState(); + + requestAnimationFrame(updateState); + + return
This component is {isMounted() ? 'MOUNTED' : 'NOT MOUNTED'}
; +}; + +storiesOf('Lifecycle|useMountedState', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/__tests__/createMemo.test.ts b/src/__tests__/createMemo.test.ts new file mode 100644 index 00000000..2acb5d94 --- /dev/null +++ b/src/__tests__/createMemo.test.ts @@ -0,0 +1,45 @@ +import { renderHook } from '@testing-library/react-hooks'; +import createMemo from '../createMemo'; + +const getDouble = jest.fn((n: number): number => n * 2); + +it('should init memo hook', () => { + const useMemoGetDouble = createMemo(getDouble); + + expect(useMemoGetDouble).toBeInstanceOf(Function); +}); + +describe('when using created memo hook', () => { + let useMemoGetDouble; + + beforeEach(() => { + useMemoGetDouble = createMemo(getDouble); + }); + + it.each([[1], [3], [5]])('should return same result as original function for argument %d', (val: number) => { + const { result } = renderHook(() => useMemoGetDouble(val)); + expect(result.current).toBe(getDouble(val)); + }); + + it('should NOT call original function for same arguments', () => { + let initialValue = 5; + expect(getDouble).not.toHaveBeenCalled(); + + // it's called first time calculating for argument 5 + const { rerender } = renderHook(() => useMemoGetDouble(initialValue)); + expect(getDouble).toHaveBeenCalled(); + + getDouble.mockClear(); + + // it's NOT called second time calculating for argument 5 + rerender(); + expect(getDouble).not.toHaveBeenCalled(); + + getDouble.mockClear(); + + // it's called again calculating for different argument + initialValue = 7; + rerender(); + expect(getDouble).toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/createReducer.test.ts b/src/__tests__/createReducer.test.ts new file mode 100644 index 00000000..42ae47c6 --- /dev/null +++ b/src/__tests__/createReducer.test.ts @@ -0,0 +1,112 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import createReducer from '../createReducer'; +import logger from 'redux-logger'; +import thunk from 'redux-thunk'; + +it('should init reducer hook function', () => { + const useSomeReducer = createReducer(); + expect(useSomeReducer).toBeInstanceOf(Function); +}); + +/** + * This test suite implements the special demo in storybook that creates a + * reducer with thunk and logger for using a simple counter + */ +describe('when using created reducer hook', () => { + const initialCount = 1; + let originalLog; + let originalGroup; + const mockLog = jest.fn(); + const mockGroup = jest.fn(); + + function reducer(state, action) { + switch (action.type) { + case 'increment': + return { count: state.count + 1 }; + case 'decrement': + return { count: state.count - 1 }; + case 'reset': + return { count: action.payload }; + default: + throw new Error(); + } + } + + // Action creator to increment count, wait a second and then reset + const addAndReset = () => { + return dispatch => { + dispatch({ type: 'increment' }); + + setTimeout(() => { + dispatch({ type: 'reset', payload: initialCount }); + }, 1000); + }; + }; + + const setUp = () => { + const useThunkReducer = createReducer(thunk, logger); + return renderHook(() => useThunkReducer(reducer, { count: initialCount })); + }; + + beforeAll(() => { + originalLog = console.log; + originalGroup = console.group; + console.log = mockLog; + console.group = mockGroup; + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + console.log = originalLog; + console.group = originalGroup; + }); + + it('should init state and dispatcher', () => { + const { result } = setUp(); + const [state, dispatch] = result.current; + + expect(state).toEqual({ count: 1 }); + expect(dispatch).toBeInstanceOf(Function); + }); + + it.each` + actionType | expectedCount | payload + ${'increment'} | ${2} | ${undefined} + ${'decrement'} | ${0} | ${undefined} + ${'reset'} | ${1} | ${1} + `('should handle "$actionType" action', ({ actionType, expectedCount, payload }) => { + const { result } = setUp(); + const [, dispatch] = result.current; + expect(mockLog).not.toHaveBeenCalled(); + + act(() => { + dispatch({ type: actionType, payload }); + }); + + expect(result.current[0]).toEqual({ count: expectedCount }); + expect(mockLog).toHaveBeenCalled(); + }); + + it('should handle async action with several middlewares', () => { + const { result } = setUp(); + const [, dispatch] = result.current; + expect(mockLog).not.toHaveBeenCalled(); + + act(() => { + dispatch(addAndReset()); + }); + + expect(result.current[0]).toEqual({ count: 2 }); + expect(mockLog).toHaveBeenCalled(); + + // fast-forward until all timers have been executed + act(() => { + jest.runAllTimers(); + }); + + expect(result.current[0]).toEqual({ count: 1 }); + }); +}); diff --git a/src/__tests__/useAsync.test.tsx b/src/__tests__/useAsync.test.tsx index 3764e42a..4e5ca61f 100644 --- a/src/__tests__/useAsync.test.tsx +++ b/src/__tests__/useAsync.test.tsx @@ -1,9 +1,7 @@ +import { renderHook } from '@testing-library/react-hooks'; import { useCallback } from 'react'; -import { cleanup, renderHook } from 'react-hooks-testing-library'; import useAsync from '../useAsync'; -afterEach(cleanup); - // NOTE: these tests cause console errors. // maybe we should test in a real environment instead // of a fake one? @@ -17,7 +15,7 @@ describe('useAsync', () => { let callCount = 0; const resolver = async () => { - return new Promise((resolve, reject) => { + return new Promise(resolve => { callCount++; const wait = setTimeout(() => { @@ -58,7 +56,7 @@ describe('useAsync', () => { let callCount = 0; const rejection = async () => { - return new Promise((resolve, reject) => { + return new Promise((_, reject) => { callCount++; const wait = setTimeout(() => { diff --git a/src/__tests__/useAsyncFn.test.tsx b/src/__tests__/useAsyncFn.test.tsx index 8e3826b5..f9c7d238 100644 --- a/src/__tests__/useAsyncFn.test.tsx +++ b/src/__tests__/useAsyncFn.test.tsx @@ -5,11 +5,9 @@ // does not automatically invoke the function // and it can take arguments. -import { cleanup, renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import useAsyncFn, { AsyncState } from '../useAsyncFn'; -afterEach(cleanup); - type AdderFn = (a: number, b: number) => Promise; describe('useAsyncFn', () => { diff --git a/src/__tests__/useBoolean.test.ts b/src/__tests__/useBoolean.test.ts new file mode 100644 index 00000000..8f633e9e --- /dev/null +++ b/src/__tests__/useBoolean.test.ts @@ -0,0 +1,6 @@ +import useBoolean from '../useBoolean'; +import useToggle from '../useToggle'; + +it('should be an alias for useToggle ', () => { + expect(useBoolean).toBe(useToggle); +}); diff --git a/src/__tests__/useCounter.test.ts b/src/__tests__/useCounter.test.ts new file mode 100644 index 00000000..41cc1e22 --- /dev/null +++ b/src/__tests__/useCounter.test.ts @@ -0,0 +1,128 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useCounter from '../useCounter'; + +const setUp = (initialValue?: number) => renderHook(() => useCounter(initialValue)); + +it('should init counter and utils', () => { + const { result } = setUp(5); + + expect(result.current[0]).toBe(5); + expect(result.current[1]).toStrictEqual({ + inc: expect.any(Function), + dec: expect.any(Function), + get: expect.any(Function), + set: expect.any(Function), + reset: expect.any(Function), + }); +}); + +it('should init counter to 0 if not initial value received', () => { + const { result } = setUp(); + + expect(result.current[0]).toBe(0); +}); + +it('should init counter to negative number', () => { + const { result } = setUp(-2); + + expect(result.current[0]).toBe(-2); +}); + +it('should get current counter', () => { + const { result } = setUp(5); + const { get } = result.current[1]; + + expect(get()).toBe(5); +}); + +it('should increment by 1 if not value received', () => { + const { result } = setUp(5); + const { get, inc } = result.current[1]; + + act(() => inc()); + + expect(result.current[0]).toBe(6); + expect(get()).toBe(6); +}); + +it('should increment by value received', () => { + const { result } = setUp(5); + const { get, inc } = result.current[1]; + + act(() => inc(9)); + + expect(result.current[0]).toBe(14); + expect(get()).toBe(14); +}); + +it('should decrement by 1 if not value received', () => { + const { result } = setUp(5); + const { get, dec } = result.current[1]; + + act(() => dec()); + + expect(result.current[0]).toBe(4); + expect(get()).toBe(4); +}); + +it('should decrement by value received', () => { + const { result } = setUp(5); + const { get, dec } = result.current[1]; + + act(() => dec(9)); + + expect(result.current[0]).toBe(-4); + expect(get()).toBe(-4); +}); + +it('should set to value received', () => { + const { result } = setUp(5); + const { get, set } = result.current[1]; + + act(() => set(17)); + + expect(result.current[0]).toBe(17); + expect(get()).toBe(17); +}); + +it('should reset to original value', () => { + const { result } = setUp(5); + const { get, set, reset } = result.current[1]; + + // set different value than initial one... + act(() => set(17)); + expect(result.current[0]).toBe(17); + + // ... and reset it to initial one + act(() => reset()); + expect(result.current[0]).toBe(5); + expect(get()).toBe(5); +}); + +it('should reset and set new original value', () => { + const { result } = setUp(5); + const { get, set, reset } = result.current[1]; + + // set different value than initial one... + act(() => set(17)); + expect(result.current[0]).toBe(17); + + // ... now reset and set it to different than initial one... + act(() => reset(8)); + expect(result.current[0]).toBe(8); + + // ... and set different value than initial one again... + act(() => set(32)); + expect(result.current[0]).toBe(32); + + // ... and reset it to new initial value + act(() => reset()); + expect(result.current[0]).toBe(8); + expect(get()).toBe(8); +}); + +it.todo('should log an error if initial value is other than a number'); +it.todo('should log an error if increment value is other than a number'); +it.todo('should log an error if increment value is a negative number'); +it.todo('should log an error if decrement value is other than a number'); +it.todo('should log an error if decrement value is a negative number'); diff --git a/src/__tests__/useDefault.test.ts b/src/__tests__/useDefault.test.ts new file mode 100644 index 00000000..3e1fbd5a --- /dev/null +++ b/src/__tests__/useDefault.test.ts @@ -0,0 +1,61 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useDefault from '../useDefault'; + +const setUp = (defaultValue: any, initialValue: any) => renderHook(() => useDefault(defaultValue, initialValue)); + +describe.each` + valueType | defaultValue | initialValue | anotherValue + ${'number'} | ${0} | ${5} | ${77} + ${'object'} | ${{}} | ${{ name: 'John Doe' }} | ${{ name: 'Solid Snake' }} + ${'boolean'} | ${false} | ${false} | ${true} + ${'string'} | ${''} | ${'foo'} | ${'bar'} +`('when value type is $valueType', ({ defaultValue, initialValue, anotherValue }) => { + it('should init state with initial value', () => { + const { result } = setUp(defaultValue, initialValue); + const [value, setValue] = result.current; + + expect(value).toBe(initialValue); + expect(setValue).toBeInstanceOf(Function); + }); + + it('should set state to another value', () => { + const { result } = setUp(defaultValue, initialValue); + const [, setValue] = result.current; + + act(() => setValue(anotherValue)); + + expect(result.current[0]).toBe(anotherValue); + }); + + it('should return default value if state set to null', () => { + const { result } = setUp(defaultValue, initialValue); + const [, setValue] = result.current; + + act(() => setValue(null)); + + expect(result.current[0]).toBe(defaultValue); + }); + + it('should return default value if state set to undefined', () => { + const { result } = setUp(defaultValue, initialValue); + const [, setValue] = result.current; + + act(() => setValue(undefined)); + + expect(result.current[0]).toBe(defaultValue); + }); + + it('should handle state properly after being set to nil and then to another value', () => { + const { result } = setUp(defaultValue, initialValue); + const [, setValue] = result.current; + + act(() => setValue(undefined)); + expect(result.current[0]).toBe(defaultValue); + + act(() => setValue(null)); + expect(result.current[0]).toBe(defaultValue); + + act(() => setValue(anotherValue)); + expect(result.current[0]).toBe(anotherValue); + }); +}); diff --git a/src/__tests__/useDefault.test.tsx b/src/__tests__/useDefault.test.tsx deleted file mode 100644 index 97de7d4b..00000000 --- a/src/__tests__/useDefault.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { act, cleanup, renderHook } from 'react-hooks-testing-library'; -import useDefault from '../useDefault'; - -afterEach(cleanup); - -describe('useDefault', () => { - test('should be defined', () => { - expect(useDefault).toBeDefined(); - }); - - const hook = renderHook(() => useDefault({ name: 'Marshall' }, { name: '' })); - - test('should return initial state on initial render', () => { - expect(hook.result.current[0].name).toBe(''); - }); - - test('should update state with correct value', () => { - hook.rerender(); - act(() => { - hook.result.current[1]({ name: 'Mathers' }); - }); - - expect(hook.result.current[0].name).toBe('Mathers'); - }); - - test('should return the default value when updated state is nil', () => { - hook.rerender(); - act(() => { - hook.result.current[1](null); - }); - - expect(hook.result.current[0].name).toBe('Marshall'); - - act(() => { - hook.result.current[1](undefined); - }); - - expect(hook.result.current[0].name).toBe('Marshall'); - }); -}); diff --git a/src/__tests__/useGetSet.test.ts b/src/__tests__/useGetSet.test.ts new file mode 100644 index 00000000..1f971e09 --- /dev/null +++ b/src/__tests__/useGetSet.test.ts @@ -0,0 +1,64 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useGetSet from '../useGetSet'; + +const setUp = (initialValue: any) => renderHook(() => useGetSet(initialValue)); + +beforeEach(() => { + jest.useFakeTimers(); +}); + +it('should init getter and setter', () => { + const { result } = setUp('foo'); + const [get, set] = result.current; + + expect(get).toBeInstanceOf(Function); + expect(set).toBeInstanceOf(Function); +}); + +it('should get current value', () => { + const { result } = setUp('foo'); + const [get] = result.current; + + const currentValue = get(); + + expect(currentValue).toBe('foo'); +}); + +it('should set new value', () => { + const { result } = setUp('foo'); + const [get, set] = result.current; + + act(() => set('bar')); + + const currentValue = get(); + expect(currentValue).toBe('bar'); +}); + +/** + * This test implements the special demo in storybook that increments a number + * after 1 second on each click. + */ +it('should get and set expected values when used in nested functions', () => { + const onClick = jest.fn(() => { + setTimeout(() => { + set(get() + 1); + }, 1000); + }); + + const { result } = setUp(0); + const [get, set] = result.current; + + // simulate 3 clicks + onClick(); + onClick(); + onClick(); + + // fast-forward until all timers have been executed + act(() => { + jest.runAllTimers(); + }); + + const currentValue = get(); + expect(currentValue).toBe(3); + expect(onClick).toHaveBeenCalledTimes(3); +}); diff --git a/src/__tests__/useGetSetState.test.ts b/src/__tests__/useGetSetState.test.ts new file mode 100644 index 00000000..e9eb6aef --- /dev/null +++ b/src/__tests__/useGetSetState.test.ts @@ -0,0 +1,136 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useGetSetState from '../useGetSetState'; + +const originalConsoleError = console.error; +const mockConsoleError = jest.fn(); + +const setUp = (initialState: any) => renderHook(() => useGetSetState(initialState)); + +beforeAll(() => { + console.error = mockConsoleError; +}); + +afterAll(() => { + console.error = originalConsoleError; +}); + +beforeEach(() => { + jest.useFakeTimers(); +}); + +it('should init getter and setter', () => { + const { result } = setUp({ foo: 'initialValue' }); + const [get, set] = result.current; + + expect(get).toBeInstanceOf(Function); + expect(set).toBeInstanceOf(Function); +}); + +it('should log an error if init with something different than an object', () => { + expect(mockConsoleError).not.toHaveBeenCalled(); + + setUp('not an object'); + + expect(mockConsoleError).toHaveBeenCalledTimes(1); + expect(mockConsoleError).toHaveBeenCalledWith('useGetSetState initial state must be an object.'); +}); + +it('should get current state', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get] = result.current; + + const currentState = get(); + + expect(currentState).toEqual({ foo: 'a', bar: 'z' }); +}); + +it('should set new state by applying patch with existing keys', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get, set] = result.current; + + act(() => set({ bar: 'y' })); + + const currentState = get(); + expect(currentState).toEqual({ foo: 'a', bar: 'y' }); +}); + +it('should set new state by applying patch with new keys', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get, set] = result.current; + + act(() => set({ qux: 'f' })); + + const currentState = get(); + expect(currentState).toEqual({ foo: 'a', bar: 'z', qux: 'f' }); +}); + +it('should set new state by applying patch with both new and old keys', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get, set] = result.current; + + act(() => set({ bar: 'y', qux: 'f' })); + + const currentState = get(); + expect(currentState).toEqual({ foo: 'a', bar: 'y', qux: 'f' }); +}); + +it('should NOT set new state if empty patch received', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get, set] = result.current; + + act(() => set({})); + + const currentState = get(); + expect(currentState).toEqual({ foo: 'a', bar: 'z' }); +}); + +it('should NOT set new state if no patch received', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [get, set] = result.current; + + // @ts-ignore + act(() => set()); + + const currentState = get(); + expect(currentState).toEqual({ foo: 'a', bar: 'z' }); +}); + +it('should log an error if set with a patch different than an object', () => { + const { result } = setUp({ foo: 'a', bar: 'z' }); + const [, set] = result.current; + expect(mockConsoleError).not.toHaveBeenCalled(); + + // @ts-ignore + act(() => set('not an object')); + + expect(mockConsoleError).toHaveBeenCalledTimes(1); + expect(mockConsoleError).toHaveBeenCalledWith('useGetSetState setter patch must be an object.'); +}); + +/** + * This test is equivalent to demo one for `useGetSet` hook. + */ +it('should get and set expected state when used in nested functions', () => { + const onClick = jest.fn(() => { + setTimeout(() => { + set({ counter: get().counter + 1 }); + }, 1000); + }); + + const { result } = setUp({ counter: 0 }); + const [get, set] = result.current; + + // simulate 3 clicks + onClick(); + onClick(); + onClick(); + + // fast-forward until all timers have been executed + act(() => { + jest.runAllTimers(); + }); + + const currentState = get(); + expect(currentState).toEqual({ counter: 3 }); + expect(onClick).toHaveBeenCalledTimes(3); +}); diff --git a/src/__tests__/useList.test.ts b/src/__tests__/useList.test.ts new file mode 100644 index 00000000..047e6e68 --- /dev/null +++ b/src/__tests__/useList.test.ts @@ -0,0 +1,152 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useList from '../useList'; + +const setUp = (initialList?: any[]) => renderHook(() => useList(initialList)); + +it('should init list and utils', () => { + const { result } = setUp([1, 2, 3]); + const [list, utils] = result.current; + + expect(list).toEqual([1, 2, 3]); + expect(utils).toStrictEqual({ + set: expect.any(Function), + clear: expect.any(Function), + updateAt: expect.any(Function), + remove: expect.any(Function), + push: expect.any(Function), + filter: expect.any(Function), + sort: expect.any(Function), + }); +}); + +it('should init empty list if not initial list provided', () => { + const { result } = setUp(); + + expect(result.current[0]).toEqual([]); +}); + +it('should set new list', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.set([4, 5, 6]); + }); + + expect(result.current[0]).toEqual([4, 5, 6]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should clear current list', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.clear(); + }); + + expect(result.current[0]).toEqual([]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should update element at specific position', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.updateAt(1, 'foo'); + }); + + expect(result.current[0]).toEqual([1, 'foo', 3]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should remove element at specific position', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.remove(1); + }); + + expect(result.current[0]).toEqual([1, 3]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should push new element at the end of the list', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.push(0); + }); + + expect(result.current[0]).toEqual([1, 2, 3, 0]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should push duplicated element at the end of the list', () => { + const initList = [1, 2, 3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.push(2); + }); + + expect(result.current[0]).toEqual([1, 2, 3, 2]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should filter current list by provided function', () => { + const initList = [1, -1, 2, -2, 3, -3]; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.filter(n => n < 0); + }); + + expect(result.current[0]).toEqual([-1, -2, -3]); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should sort current list by default order', () => { + const initList = ['March', 'Jan', 'Feb', 'Dec']; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.sort(); + }); + + expect(result.current[0]).toEqual(['Dec', 'Feb', 'Jan', 'March']); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); + +it('should sort current list by provided function', () => { + const initList = ['March', 'Jan', 'Feb', 'Dec']; + const { result } = setUp(initList); + const [, utils] = result.current; + + act(() => { + utils.sort((a, b) => { + if (a < b) { + return 1; + } + if (a > b) { + return -1; + } + + return 0; + }); + }); + + expect(result.current[0]).toEqual(['March', 'Jan', 'Feb', 'Dec']); + expect(result.current[0]).not.toBe(initList); // checking immutability +}); diff --git a/src/__tests__/useMap.test.ts b/src/__tests__/useMap.test.ts new file mode 100644 index 00000000..242df55f --- /dev/null +++ b/src/__tests__/useMap.test.ts @@ -0,0 +1,115 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useMap from '../useMap'; + +const setUp = (initialMap?: object) => renderHook(() => useMap(initialMap)); + +it('should init map and utils', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [map, utils] = result.current; + + expect(map).toEqual({ foo: 'bar', a: 1 }); + expect(utils).toStrictEqual({ + get: expect.any(Function), + set: expect.any(Function), + remove: expect.any(Function), + reset: expect.any(Function), + }); +}); + +it('should init empty map if not initial object provided', () => { + const { result } = setUp(); + + expect(result.current[0]).toEqual({}); +}); + +it('should get corresponding value for existing provided key', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + let value; + act(() => { + // @ts-ignore + value = utils.get('a'); + }); + + expect(value).toBe(1); +}); + +it('should get undefined for non-existing provided key', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + let value; + act(() => { + // @ts-ignore + value = utils.get('nonExisting'); + }); + + expect(value).toBeUndefined(); +}); + +it('should set new key-value pair', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + act(() => { + // @ts-ignore + utils.set('newKey', 99); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', a: 1, newKey: 99 }); +}); + +it('should override current value if setting existing key', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + act(() => { + // @ts-ignore + utils.set('foo', 99); + }); + + expect(result.current[0]).toEqual({ foo: 99, a: 1 }); +}); + +it('should remove corresponding key-value pair for existing provided key', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + act(() => { + // @ts-ignore + utils.remove('foo'); + }); + + expect(result.current[0]).toEqual({ a: 1 }); +}); + +it('should do nothing if removing non-existing provided key', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + act(() => { + // @ts-ignore + utils.remove('nonExisting'); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', a: 1 }); +}); + +it('should reset map to initial object provided', () => { + const { result } = setUp({ foo: 'bar', a: 1 }); + const [, utils] = result.current; + + act(() => { + // @ts-ignore + utils.set('z', 99); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', a: 1, z: 99 }); + + act(() => { + utils.reset(); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', a: 1 }); +}); diff --git a/src/__tests__/useMountedState.test.tsx b/src/__tests__/useMountedState.test.tsx new file mode 100644 index 00000000..ea3d8af9 --- /dev/null +++ b/src/__tests__/useMountedState.test.tsx @@ -0,0 +1,28 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useMountedState from '../useMountedState'; + +describe('useMountedState', () => { + it('should be defined', () => { + expect(useMountedState).toBeDefined(); + }); + + it('should return a function', () => { + const hook = renderHook(() => useMountedState(), { initialProps: false }); + + expect(typeof hook.result.current).toEqual('function'); + }); + + it('should return true if component is mounted', () => { + const hook = renderHook(() => useMountedState(), { initialProps: false }); + + expect(hook.result.current()).toBeTruthy(); + }); + + it('should return false if component is unmounted', () => { + const hook = renderHook(() => useMountedState(), { initialProps: false }); + + hook.unmount(); + + expect(hook.result.current()).toBeFalsy(); + }); +}); diff --git a/src/__tests__/useNumber.test.ts b/src/__tests__/useNumber.test.ts new file mode 100644 index 00000000..f646f931 --- /dev/null +++ b/src/__tests__/useNumber.test.ts @@ -0,0 +1,6 @@ +import useNumber from '../useNumber'; +import useCounter from '../useCounter'; + +it('should be an alias for useCounter', () => { + expect(useNumber).toBe(useCounter); +}); diff --git a/src/__tests__/useObservable-layout.spec.ts b/src/__tests__/useObservable-layout.spec.ts deleted file mode 100644 index 3701401f..00000000 --- a/src/__tests__/useObservable-layout.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { Subject } from 'rxjs'; -import { useObservable } from '..'; -import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect'; - -jest.mock('../useIsomorphicLayoutEffect'); - -test('uses layout effect (to subscribe synchronously)', async () => { - const subject = new Subject(); - const container = document.createElement('div'); - - const Demo = ({ obs }) => { - const value = useObservable(obs); - return React.createElement(React.Fragment, {}, value); - }; - - expect(useIsomorphicLayoutEffect).toHaveBeenCalledTimes(0); - ReactDOM.render(React.createElement(Demo, { obs: subject }), container); - expect(useIsomorphicLayoutEffect).toHaveBeenCalledTimes(1); -}); diff --git a/src/__tests__/useObservable.test.ts b/src/__tests__/useObservable.test.ts new file mode 100644 index 00000000..7a4c2128 --- /dev/null +++ b/src/__tests__/useObservable.test.ts @@ -0,0 +1,108 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { Subject } from 'rxjs'; +import useObservable, { Observable } from '../useObservable'; +import * as useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect'; + +const setUp = (observable: Observable, initialValue?: any) => + renderHook(() => useObservable(observable, initialValue)); + +it('should init to initial value provided', () => { + const subject$ = new Subject(); + const { result } = setUp(subject$, 123); + + expect(result.current).toBe(123); +}); + +it('should init to undefined if not initial value provided', () => { + const subject$ = new Subject(); + const { result } = setUp(subject$); + + expect(result.current).toBeUndefined(); +}); + +it('should return latest value of observables', () => { + const subject$ = new Subject(); + const { result } = setUp(subject$, 123); + + act(() => { + subject$.next(125); + }); + expect(result.current).toBe(125); + + act(() => { + subject$.next(300); + subject$.next(400); + }); + expect(result.current).toBe(400); +}); + +it('should use layout effect to subscribe synchronously', async () => { + const subject$ = new Subject(); + const spy = jest.spyOn(useIsomorphicLayoutEffect, 'default'); + + expect(spy).toHaveBeenCalledTimes(0); + + setUp(subject$, 123); + + expect(spy).toHaveBeenCalledTimes(1); +}); + +it('should subscribe to observable only once', () => { + const subject$ = new Subject(); + const spy = jest.spyOn(subject$, 'subscribe'); + expect(spy).not.toHaveBeenCalled(); + + setUp(subject$, 123); + + expect(spy).toHaveBeenCalledTimes(1); + + act(() => { + subject$.next('a'); + }); + + act(() => { + subject$.next('b'); + }); + + expect(spy).toHaveBeenCalledTimes(1); +}); + +it('should return updated value when observable changes', () => { + const subject$ = new Subject(); + const { result } = setUp(subject$); + expect(result.current).toBeUndefined(); + + act(() => { + subject$.next('foo'); + }); + expect(result.current).toBe('foo'); + + act(() => { + subject$.next('bar'); + }); + expect(result.current).toBe('bar'); +}); + +it('should unsubscribe from observable', () => { + const subject$ = new Subject(); + const unsubscribeMock = jest.fn(); + subject$.subscribe = jest.fn().mockReturnValue({ + unsubscribe: unsubscribeMock, + }); + + const { unmount } = setUp(subject$); + expect(unsubscribeMock).not.toHaveBeenCalled(); + + act(() => { + subject$.next('foo'); + }); + expect(unsubscribeMock).not.toHaveBeenCalled(); + + act(() => { + subject$.next('bar'); + }); + expect(unsubscribeMock).not.toHaveBeenCalled(); + + unmount(); + expect(unsubscribeMock).toHaveBeenCalledTimes(1); +}); diff --git a/src/__tests__/useObservable.test.tsx b/src/__tests__/useObservable.test.tsx deleted file mode 100644 index 4166c986..00000000 --- a/src/__tests__/useObservable.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { act, renderHook } from 'react-hooks-testing-library'; -import { Subject } from 'rxjs'; -import { useObservable } from '..'; - -let container: HTMLDivElement | null; - -beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); -}); - -afterEach(() => { - document.body.removeChild(container!); - container = null; -}); - -test('default initial value is undefined', () => { - const subject$ = new Subject(); - const { result } = renderHook(() => useObservable(subject$)); - - expect(result.current).toBe(undefined); -}); - -test('can specify initial value', () => { - const subject$ = new Subject(); - const { result } = renderHook(() => useObservable(subject$, 123)); - - expect(result.current).toBe(123); -}); - -test('returns the latest value of observables', () => { - const subject$ = new Subject(); - const { result } = renderHook(() => useObservable(subject$, 123)); - - act(() => { - subject$.next(125); - }); - expect(result.current).toBe(125); - - act(() => { - subject$.next(300); - subject$.next(400); - }); - expect(result.current).toBe(400); -}); - -test('subscribes to observable only once', async () => { - const subject = new Subject(); - const spy = jest.spyOn(subject, 'subscribe'); - - expect(spy).toHaveBeenCalledTimes(0); - - const Demo = ({ obs }) => { - const value = useObservable(obs); - return <>{value}; - }; - - ReactDOM.render(, container); - - expect(spy).toHaveBeenCalledTimes(1); - - await new Promise(r => setTimeout(r, 1)); - act(() => { - subject.next('a'); - }); - await new Promise(r => setTimeout(r, 1)); - act(() => { - subject.next('b'); - }); - - expect(spy).toHaveBeenCalledTimes(1); -}); - -test('re-renders component as obsevable changes', async () => { - const subject = new Subject(); - - let cnt = 0; - const Demo = ({ obs }) => { - cnt++; - const value = useObservable(obs); - return <>{value}; - }; - - ReactDOM.render(, container); - - await new Promise(r => setTimeout(r, 1)); - expect(cnt).toBe(1); - expect(container.innerHTML).toBe(''); - - act(() => { - subject.next('a'); - }); - - await new Promise(r => setTimeout(r, 1)); - expect(cnt).toBe(2); - expect(container.innerHTML).toBe('a'); - - act(() => { - subject.next('b'); - }); - - await new Promise(r => setTimeout(r, 1)); - expect(cnt).toBe(3); - expect(container.innerHTML).toBe('b'); -}); diff --git a/src/__tests__/usePrevious.test.ts b/src/__tests__/usePrevious.test.ts new file mode 100644 index 00000000..f6d63797 --- /dev/null +++ b/src/__tests__/usePrevious.test.ts @@ -0,0 +1,23 @@ +import { renderHook } from '@testing-library/react-hooks'; +import usePrevious from '../usePrevious'; + +const setUp = () => renderHook(({ state }) => usePrevious(state), { initialProps: { state: 0 } }); + +it('should return undefined on initial render', () => { + const { result } = setUp(); + + expect(result.current).toBeUndefined(); +}); + +it('should always return previous state after each update', () => { + const { result, rerender } = setUp(); + + rerender({ state: 2 }); + expect(result.current).toBe(0); + + rerender({ state: 4 }); + expect(result.current).toBe(2); + + rerender({ state: 6 }); + expect(result.current).toBe(4); +}); diff --git a/src/__tests__/usePrevious.test.tsx b/src/__tests__/usePrevious.test.tsx deleted file mode 100644 index 70713653..00000000 --- a/src/__tests__/usePrevious.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cleanup, renderHook } from 'react-hooks-testing-library'; -import usePrevious from '../usePrevious'; - -afterEach(cleanup); - -describe('usePrevious', () => { - it('should be defined', () => { - expect(usePrevious).toBeDefined(); - }); - - const hook = renderHook(props => usePrevious(props), { initialProps: 0 }); - - it('should return undefined on initial render', () => { - expect(hook.result.current).toBe(undefined); - }); - - it('should return previous state after update', () => { - hook.rerender(1); - hook.rerender(2); - expect(hook.result.current).toBe(1); - }); -}); diff --git a/src/__tests__/useSetState.test.ts b/src/__tests__/useSetState.test.ts new file mode 100644 index 00000000..684ba30b --- /dev/null +++ b/src/__tests__/useSetState.test.ts @@ -0,0 +1,42 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useSetState from '../useSetState'; + +const setUp = (initialState?: object) => renderHook(() => useSetState(initialState)); + +it('should init state and setter', () => { + const { result } = setUp({ foo: 'bar' }); + const [state, setState] = result.current; + + expect(state).toEqual({ foo: 'bar' }); + expect(setState).toBeInstanceOf(Function); +}); + +it('should init empty state if not initial state provided', () => { + const { result } = setUp(); + + expect(result.current[0]).toEqual({}); +}); + +it('should merge changes into current state when providing object', () => { + const { result } = setUp({ foo: 'bar', count: 1 }); + const [state, setState] = result.current; + + act(() => { + // @ts-ignore + setState({ count: state.count + 1, someBool: true }); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', count: 2, someBool: true }); +}); + +it('should merge changes into current state when providing function', () => { + const { result } = setUp({ foo: 'bar', count: 1 }); + const [, setState] = result.current; + + act(() => { + // @ts-ignore + setState(prevState => ({ count: prevState.count + 1, someBool: true })); + }); + + expect(result.current[0]).toEqual({ foo: 'bar', count: 2, someBool: true }); +}); diff --git a/src/__tests__/useTitle.test.tsx b/src/__tests__/useTitle.test.tsx index a98c5a13..b8cb626e 100644 --- a/src/__tests__/useTitle.test.tsx +++ b/src/__tests__/useTitle.test.tsx @@ -1,8 +1,6 @@ -import { cleanup, renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import useTitle from '../useTitle'; -afterEach(cleanup); - describe('useTitle', () => { it('should be defined', () => { expect(useTitle).toBeDefined(); diff --git a/src/__tests__/useToggle.test.ts b/src/__tests__/useToggle.test.ts new file mode 100644 index 00000000..adafdc3b --- /dev/null +++ b/src/__tests__/useToggle.test.ts @@ -0,0 +1,83 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useToggle from '../useToggle'; + +const setUp = (initialValue: boolean) => renderHook(() => useToggle(initialValue)); + +it('should init state to true', () => { + const { result } = setUp(true); + + expect(result.current[0]).toBe(true); + expect(typeof result.current[1]).toBe('function'); +}); + +it('should init state to false', () => { + const { result } = setUp(false); + + expect(result.current[0]).toBe(false); + expect(result.current[1]).toBeInstanceOf(Function); +}); + +it('should set state to true', () => { + const { result } = setUp(false); + const [, toggle] = result.current; + + expect(result.current[0]).toBe(false); + + act(() => { + toggle(true); + }); + + expect(result.current[0]).toBe(true); +}); + +it('should set state to false', () => { + const { result } = setUp(true); + const [, toggle] = result.current; + + expect(result.current[0]).toBe(true); + + act(() => { + toggle(false); + }); + + expect(result.current[0]).toBe(false); +}); + +it('should toggle state from true', () => { + const { result } = setUp(true); + const [, toggle] = result.current; + + act(() => { + toggle(); + }); + + expect(result.current[0]).toBe(false); +}); + +it('should toggle state from false', () => { + const { result } = setUp(false); + const [, toggle] = result.current; + + act(() => { + toggle(); + }); + + expect(result.current[0]).toBe(true); +}); + +it('should ignore non-boolean parameters and toggle state', () => { + const { result } = setUp(true); + const [, toggle] = result.current; + + act(() => { + toggle('string'); + }); + + expect(result.current[0]).toBe(false); + + act(() => { + toggle({}); + }); + + expect(result.current[0]).toBe(true); +}); diff --git a/src/__tests__/useToggle.test.tsx b/src/__tests__/useToggle.test.tsx deleted file mode 100644 index bcf1d78f..00000000 --- a/src/__tests__/useToggle.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { act, cleanup, renderHook } from 'react-hooks-testing-library'; -import useToggle from '../useToggle'; - -afterEach(cleanup); - -describe('useToggle', () => { - it('should be defined', () => { - expect(useToggle).toBeDefined(); - }); - - const hook = renderHook(props => useToggle(props), { initialProps: false }); - - it('should return initial state on initial render', () => { - expect(hook.result.current[0]).toBe(false); - }); - - it('should update state with correct value', () => { - hook.rerender(true); - expect(hook.result.current[0]).toBe(true); - - act(() => { - hook.result.current[1](false); - }); - - expect(hook.result.current[0]).toBe(false); - }); - - // it('should toggle state without a value parameter', () => { - // act(() => { - // hook.result.current[1](); - // }); - - // expect(hook.result.current[0]).toBe(true); - // }); - - // it('should ignore non-boolean parameters', () => { - // act(() => { - // hook.result.current[1]('string'); - // }); - - // expect(hook.result.current[0]).toBe(true); - - // act(() => { - // hook.result.current[1]({}); - // }); - - // expect(hook.result.current[0]).toBe(false); - // }); -}); diff --git a/src/__tests__/useWindowSize.test.tsx b/src/__tests__/useWindowSize.test.tsx index 9cc4be50..b3cb7fe5 100644 --- a/src/__tests__/useWindowSize.test.tsx +++ b/src/__tests__/useWindowSize.test.tsx @@ -1,4 +1,4 @@ -import { act, cleanup, renderHook } from 'react-hooks-testing-library'; +import { act, renderHook } from '@testing-library/react-hooks'; import useWindowSize from '../useWindowSize'; // simulate window resize @@ -17,8 +17,6 @@ function fireResize(type, value) { window.dispatchEvent(new Event('resize')); } -afterEach(cleanup); - describe('useWindowSize', () => { it('should be defined', () => { expect(useWindowSize).toBeDefined(); diff --git a/src/index.ts b/src/index.ts index 3c4e7bc3..f196b68a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,7 @@ export { default as useMedia } from './useMedia'; export { default as useMediaDevices } from './useMediaDevices'; export { default as useMotion } from './useMotion'; export { default as useMount } from './useMount'; +export { default as useMountedState } from './useMountedState'; export { default as useMouse } from './useMouse'; export { default as useMouseHovered } from './useMouseHovered'; export { default as useNetwork } from './useNetwork'; diff --git a/src/useKeyboardJs.ts b/src/useKeyboardJs.ts index c67d9f2f..3ae5cc46 100644 --- a/src/useKeyboardJs.ts +++ b/src/useKeyboardJs.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import useMount from './useMount'; -const useKeyboardJs = (combination: string) => { +const useKeyboardJs = (combination: string | string[]) => { const [state, set] = useState<[boolean, null | KeyboardEvent]>([false, null]); const [keyboardJs, setKeyboardJs] = useState(null); diff --git a/src/useMountedState.ts b/src/useMountedState.ts new file mode 100644 index 00000000..6236d415 --- /dev/null +++ b/src/useMountedState.ts @@ -0,0 +1,15 @@ +import { useEffect, useRef } from 'react'; + +export default function useMountedState(): () => boolean { + const mountedRef = useRef(false); + + useEffect(() => { + mountedRef.current = true; + + return () => { + mountedRef.current = false; + }; + }); + + return () => mountedRef.current; +} diff --git a/yarn.lock b/yarn.lock index d2de2751..e867b975 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1528,7 +1528,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== @@ -1986,14 +1986,6 @@ source-map "^0.6.1" write-file-atomic "2.4.1" -"@jest/types@^24.7.0": - version "24.7.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.7.0.tgz#c4ec8d1828cdf23234d9b4ee31f5482a3f04f48b" - integrity sha512-ipJUa2rFWiKoBqMKP63Myb6h9+iT3FHRTF2M8OR6irxWzItisa8i4dcSg14IbvmXUnBlHBlUQPYUHWyX3UPpYA== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/yargs" "^12.0.9" - "@jest/types@^24.8.0": version "24.8.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad" @@ -2188,21 +2180,16 @@ into-stream "^4.0.0" lodash "^4.17.4" -"@sheerun/mutationobserver-shim@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" - integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== - -"@storybook/addon-actions@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.1.9.tgz#a515b62b109cb886ccd75ef2f5b12f8c27b43dd3" - integrity sha512-h/csHPotBESyEUYlML3yyF2jUlDChB+u3TUNC3Ztzh/x7HzLqy88SL0INSIdY0dCBGx4TK5Gh+rMI7z28Hfdyw== +"@storybook/addon-actions@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.1.10.tgz#8ed4272a6afc68f4a30372da2eeff414f0fe6ecd" + integrity sha512-njl2AHBGi27NvisOB8LFnWH/3RcyJT/CW7tl1cvV2j5FH2oBjq5MsjxKyJIcKwC677k1Wr8G8fw/zSEHrPpmgA== dependencies: - "@storybook/addons" "5.1.9" - "@storybook/api" "5.1.9" - "@storybook/components" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/api" "5.1.10" + "@storybook/components" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/theming" "5.1.10" core-js "^3.0.1" fast-deep-equal "^2.0.1" global "^4.3.2" @@ -2213,16 +2200,16 @@ react-inspector "^3.0.2" uuid "^3.3.2" -"@storybook/addon-knobs@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-5.1.9.tgz#74db07fd644b41e63274f8754fbfb18f43d4cf01" - integrity sha512-7/bICMYtR9CaTqfZX1kT2pBOTLZo3HxeslyQKWWsWlNElV33Ym2d0PPL5eS36eFxG/ZOp6lQWIFhunNnlmP5xg== +"@storybook/addon-knobs@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-5.1.10.tgz#f5d9f21090e28046169a0aa0418de59bd92c21fd" + integrity sha512-j5wXBIPGQxK+guFDAi8xNBdUnyQglhDplVoC9SswkSMarqtWq02TT+OLN2VSBgpvzHmhLUW3autjJGfmwP4ltQ== dependencies: - "@storybook/addons" "5.1.9" - "@storybook/client-api" "5.1.9" - "@storybook/components" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/client-api" "5.1.10" + "@storybook/components" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/theming" "5.1.10" copy-to-clipboard "^3.0.8" core-js "^3.0.1" escape-html "^1.0.3" @@ -2235,18 +2222,18 @@ react-lifecycles-compat "^3.0.4" react-select "^2.2.0" -"@storybook/addon-notes@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-notes/-/addon-notes-5.1.9.tgz#7a19373589b4280b9a10a107a0357e63d2d56f5c" - integrity sha512-UE+/RNysKRo2ZqCaqllwwV59x2S1laZjF4Netcm0nf9dKNcnqOoSHlTs0t4m8WpmfBQKAde4p+8fJGP2bCM+8A== +"@storybook/addon-notes@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-notes/-/addon-notes-5.1.10.tgz#92609d64401b962c5eaea0b5235474daa59a0d3f" + integrity sha512-S2I9W7n8xQKuJFxk3dF1RO1o648L+yX/JHR9j9rMHbre+5LuZ/Bev5WOIhHHthKllPqy1/Wxe9ECsGR7L4W2CQ== dependencies: - "@storybook/addons" "5.1.9" - "@storybook/api" "5.1.9" - "@storybook/client-logger" "5.1.9" - "@storybook/components" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/router" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/api" "5.1.10" + "@storybook/client-logger" "5.1.10" + "@storybook/components" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/router" "5.1.10" + "@storybook/theming" "5.1.10" core-js "^3.0.1" global "^4.3.2" markdown-to-jsx "^6.9.3" @@ -2254,37 +2241,37 @@ prop-types "^15.7.2" util-deprecate "^1.0.2" -"@storybook/addon-options@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-5.1.9.tgz#dc326b94c52a7ee27bf948fb7ca63a6ea3c9736d" - integrity sha512-wQIUw5Uc/5iREtI9eh2wPMlNG0O7jxaTaxGS8Bs6zDzkwiLHcLJCrfk1lPXU6O8ExEzWHpaCmxth8GyfKj8Qtw== +"@storybook/addon-options@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-5.1.10.tgz#ae6cf58744cee4ed161ffc537bc7e29a7c7337d0" + integrity sha512-DA6r84JYMJ5zACGdPQ7utpig8pUU1wywMMZt43k2IhxKBFcamw0Nmc80vvlTMM4mUu+mevH1ZiuzWxTZU7IcxA== dependencies: - "@storybook/addons" "5.1.9" + "@storybook/addons" "5.1.10" core-js "^3.0.1" util-deprecate "^1.0.2" -"@storybook/addons@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.1.9.tgz#ecf218d08508b97ca5e6e0f1ed361081385bd3ff" - integrity sha512-1bavbcS/NiE65DwyKj8c0DmWmz9VekOinB+has2Pqt2bOffZoZwVnbmepcz9hH3GUyvp5fQBYbxTEmTDvF2lLA== +"@storybook/addons@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.1.10.tgz#2d8d8ca20b6d9b4652744f5fc00ead483f705435" + integrity sha512-M9b2PCp9RZxDC6wL7vVt2SCKCGXrrEAOsdpMvU569yB1zoUPEiiqElVDwb91O2eAGPnmd2yjImp90kOpKUW0EA== dependencies: - "@storybook/api" "5.1.9" - "@storybook/channels" "5.1.9" - "@storybook/client-logger" "5.1.9" + "@storybook/api" "5.1.10" + "@storybook/channels" "5.1.10" + "@storybook/client-logger" "5.1.10" core-js "^3.0.1" global "^4.3.2" util-deprecate "^1.0.2" -"@storybook/api@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.9.tgz#eec5b2f775392ce0803930104c6ce14fa4931e8b" - integrity sha512-d1HhpOkW+706/WJ9lP5nCqOrp/icvbm0o+6jFFOGJ35AW5O9D8vDBxzvgMEO45jjN4I+rtbcNHQCxshSbPvP9w== +"@storybook/api@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.10.tgz#5eeb5d9a7c268e5c89bd40c9a80293a7c72343b8" + integrity sha512-YeZe/71zLMmgT95IMAEZOc9AwL6Y23mWvkZMwFbkokxS9+bU/qmVlQ0B9c3JBzO3OSs7sXaRqyP1o3QkQgVsiw== dependencies: - "@storybook/channels" "5.1.9" - "@storybook/client-logger" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/router" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/channels" "5.1.10" + "@storybook/client-logger" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/router" "5.1.10" + "@storybook/theming" "5.1.10" core-js "^3.0.1" fast-deep-equal "^2.0.1" global "^4.3.2" @@ -2298,33 +2285,33 @@ telejson "^2.2.1" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.1.9.tgz#bd710ca74d7998a234c6b1f38009020d7c34bbc0" - integrity sha512-H71PsnDKW81eflOS48Lv9yK4O8AcoqXL6ohsWvLdrHWIBsH4zpjOIhdWHtmAaT3hyfMy+l49DQ+uCHLECEt55g== +"@storybook/channel-postmessage@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.1.10.tgz#e0a58461d56ef20a87d8bc4df1067e7afc76950e" + integrity sha512-kQZIwltN2cWDXluhCfdModFDK1LHV9ZhNQ1b/uD9vn1c65rQ9u7r4lRajCfS0X1dmAWqz48cBcEurAubNgmswg== dependencies: - "@storybook/channels" "5.1.9" - "@storybook/client-logger" "5.1.9" + "@storybook/channels" "5.1.10" + "@storybook/client-logger" "5.1.10" core-js "^3.0.1" global "^4.3.2" telejson "^2.2.1" -"@storybook/channels@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.1.9.tgz#003cfca0b9f1ba6cf47ce68304aedd71bdb55e74" - integrity sha512-R6i7859FsXgY9XFFErVe7gS37wGYpQEEWsO1LzUW7YptGuFTUa8yLgKkNkgfy7Zs61Xm+GiBq8PvS/CWxjotPw== +"@storybook/channels@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.1.10.tgz#04fd35c05032c675f7816ea1ca873c1a0415c6d9" + integrity sha512-w7n/bV1BLu51KI1eLc75lN9H1ssBc3PZMXk88GkMiKyBVRzPlJA5ixnzH86qwYGReE0dhRpsgHXZ5XmoKaVmPA== dependencies: core-js "^3.0.1" -"@storybook/client-api@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.1.9.tgz#b598efe4ab07bffaeb4cb9e30ed9c21add739df1" - integrity sha512-J5HDtOS7x5YRpF/CMiHdxywV5NIh1i/03Xh2RhG15lmPy87VStIGpLzhF71uCRPLEJinYelcjuXRNAJgRzUOlg== +"@storybook/client-api@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.1.10.tgz#a10f028f2d33d044e5c3b3daea5d8375323e6a66" + integrity sha512-v2PqiNUhwDlVDLYL94f6LFjdYMToTpuwWh9aeqzt/4PAJUnIcA+2P8+qXiYdJTqQy/u7P72HFMlc9Ru4tl3QFg== dependencies: - "@storybook/addons" "5.1.9" - "@storybook/client-logger" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/router" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/client-logger" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/router" "5.1.10" common-tags "^1.8.0" core-js "^3.0.1" eventemitter3 "^3.1.0" @@ -2334,20 +2321,20 @@ memoizerific "^1.11.3" qs "^6.6.0" -"@storybook/client-logger@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.1.9.tgz#87e2f7578416269adeccd407584010bc353f14d3" - integrity sha512-1+Otcn0EFgWNviDPNCR5LtUViADlboz9fmpZc7UY7bgaY5FVNIUO01E4T43tO7fduiRZoEvdltwTuQRm260Vjw== +"@storybook/client-logger@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.1.10.tgz#f83a8717924dd222e0a6df82ae74701f27e0bb35" + integrity sha512-vB1NoFWRTgcERwodhbgoDwI00eqU8++nXI7GhMS1CY8haZaSp3gyKfHRWyfH+M+YjQuGBRUcvIk4gK6OtSrDOw== dependencies: core-js "^3.0.1" -"@storybook/components@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.1.9.tgz#2a5258780fff07172d103287759946dbb4b13e2d" - integrity sha512-F4xcRlifSAfqkuFWtCKRvQDahXyfWBWV2Wa+kYy4YGwEfm3kKtIHVlgdgARL22g9BdYpRFEOJ+42juOu5YvIeQ== +"@storybook/components@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.1.10.tgz#4b6436f0b5bb2483fb231bee263d173a9ed7d241" + integrity sha512-QUQeeQp1xNWiL4VlxFAea0kqn2zvBfmfPlUddOFO9lBhT6pVy0xYPjXjbTVWjVcYzZpyUNWw5GplqrR5jhlaCA== dependencies: - "@storybook/client-logger" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/client-logger" "5.1.10" + "@storybook/theming" "5.1.10" core-js "^3.0.1" global "^4.3.2" markdown-to-jsx "^6.9.1" @@ -2365,32 +2352,32 @@ recompose "^0.30.0" simplebar-react "^1.0.0-alpha.6" -"@storybook/core-events@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.1.9.tgz#441a6297e2ccfa743e15d1db1f4ac445b91f40d8" - integrity sha512-jHe2uyoLj9i6fntHtOj5azfGdLOb75LF0e1xXE8U2SX7Zp3uwbLAcfJ+dPStdc/q+f/wBiip3tH1dIjaNuUiMw== +"@storybook/core-events@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.1.10.tgz#5aed88c572036b6bd6dfff28976ee96e6e175d7a" + integrity sha512-Lvu/rNcgS+XCkQKSGdNpUSWjpFF9AOSHPXsvkwHbRwJYdMDn3FznlXfDUiubOWtsziXHB6vl3wkKDlH+ckb32Q== dependencies: core-js "^3.0.1" -"@storybook/core@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.1.9.tgz#8b30507676531fd41ac333b7c71b1c0db6b8da35" - integrity sha512-P3aavCnl3Cl3WMXVERjQqnqV1Z8tN0tyOTqqiGb1fMxITSE8uZNvp33Dl0K3jr1PBl9trW+2t7eHH4h0sguLlQ== +"@storybook/core@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.1.10.tgz#53d23d07716aa2721e1572d44a7f05967d7da39e" + integrity sha512-zkNjufOFrLpFpmr73F/gaJh0W0vWqXIo5zrKvQt1LqmMeCU/v8MstHi4XidlK43UpeogfaXl5tjNCQDO/bd0Dw== dependencies: "@babel/plugin-proposal-class-properties" "^7.3.3" "@babel/plugin-proposal-object-rest-spread" "^7.3.2" "@babel/plugin-syntax-dynamic-import" "^7.2.0" "@babel/plugin-transform-react-constant-elements" "^7.2.0" "@babel/preset-env" "^7.4.5" - "@storybook/addons" "5.1.9" - "@storybook/channel-postmessage" "5.1.9" - "@storybook/client-api" "5.1.9" - "@storybook/client-logger" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/node-logger" "5.1.9" - "@storybook/router" "5.1.9" - "@storybook/theming" "5.1.9" - "@storybook/ui" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/channel-postmessage" "5.1.10" + "@storybook/client-api" "5.1.10" + "@storybook/client-logger" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/node-logger" "5.1.10" + "@storybook/router" "5.1.10" + "@storybook/theming" "5.1.10" + "@storybook/ui" "5.1.10" airbnb-js-shims "^1 || ^2" autoprefixer "^9.4.9" babel-plugin-add-react-displayname "^0.0.5" @@ -2444,10 +2431,10 @@ webpack-dev-middleware "^3.7.0" webpack-hot-middleware "^2.25.0" -"@storybook/node-logger@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.1.9.tgz#4aacf0096811fde1639fc9d1d2d521f7220dd4be" - integrity sha512-rcSuI5n53hDMHW83gl5TR0Yn885/i2XY0AzX1DsbTeGOl3x5LhrCSZsZWetKGcx7zsO4n7o5mQszLuN1JlyE8A== +"@storybook/node-logger@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.1.10.tgz#92c80b46177687cd8fda1f93a055c22711984154" + integrity sha512-Z4UKh7QBOboQhUF5S/dKOx3OWWCNZGwYu8HZa/O+P68+XnQDhuZCYwqWG49xFhZd0Jb0W9gdUL2mWJw5POG9PA== dependencies: chalk "^2.4.2" core-js "^3.0.1" @@ -2455,16 +2442,16 @@ pretty-hrtime "^1.0.3" regenerator-runtime "^0.12.1" -"@storybook/react@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-5.1.9.tgz#4052f4b88e91d5a823bb9cbb61104c530fcfb1a1" - integrity sha512-Byykpsttf6p2jv3LvqFtntEYfbUZSNTts0TjcZHNsHoUGmT7/M1PyqTeB7JUcYUNwSgdACY8FbowCrwZwDJDWQ== +"@storybook/react@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-5.1.10.tgz#a5cf2b7d086e121c969d34100fb03fcfdc74cbed" + integrity sha512-wWy9l83KgbP8P2A8AbkwExEAdA0iznb4jEnCGzP1hAv8Q5LmL3MLPb1dIZqhWrg+E2m3tZei+7A7qu2Q8/cLLw== dependencies: "@babel/plugin-transform-react-constant-elements" "^7.2.0" "@babel/preset-flow" "^7.0.0" "@babel/preset-react" "^7.0.0" - "@storybook/core" "5.1.9" - "@storybook/node-logger" "5.1.9" + "@storybook/core" "5.1.10" + "@storybook/node-logger" "5.1.10" "@svgr/webpack" "^4.0.3" babel-plugin-add-react-displayname "^0.0.5" babel-plugin-named-asset-import "^0.3.1" @@ -2481,10 +2468,10 @@ semver "^6.0.0" webpack "^4.33.0" -"@storybook/router@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.1.9.tgz#8cd97bea4f2acf8ec5f6694d06fb0633dde33417" - integrity sha512-eAmeerE/OTIwCV7WBnb1BPINVN1GTSMsUXLNWpqSISuyWJ+NZAJlObFkvXoc57QSQlv0cvXlm1FMkmRt8ku1Hw== +"@storybook/router@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.1.10.tgz#d3cffd3f1105eb665882f389746ccabbb98c3c16" + integrity sha512-BdG6/essPZFHCP2ewCG0gYFQfmuuTSHXAB5fd/rwxLSYj1IzNznC5OxkvnSaTr4rgoxxaW/z1hbN1NuA0ivlFA== dependencies: "@reach/router" "^1.2.1" core-js "^3.0.1" @@ -2492,14 +2479,14 @@ memoizerific "^1.11.3" qs "^6.6.0" -"@storybook/theming@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.9.tgz#c425f5867fae0db79e01112853b1808332a5f1a2" - integrity sha512-4jIFJwTWVf9tsv27noLoFHlKC2Jl9DHV3q+rxGPU8bTNbufCu4oby82SboO5GAKuS3eu1cxL1YY9pYad9WxfHg== +"@storybook/theming@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.10.tgz#f9bd519cdf9cccf730656e3f5fd56a339dd07c9f" + integrity sha512-5cN1lmdVUwAR8U3T49Lfb8JW5RBvxBSPGZpUmbLGz1zi0tWBJgYXoGtw4RbTBjV9kCQOXkHGH12AsdDxHh931w== dependencies: "@emotion/core" "^10.0.9" "@emotion/styled" "^10.0.7" - "@storybook/client-logger" "5.1.9" + "@storybook/client-logger" "5.1.10" common-tags "^1.8.0" core-js "^3.0.1" deep-object-diff "^1.1.0" @@ -2510,19 +2497,19 @@ prop-types "^15.7.2" resolve-from "^5.0.0" -"@storybook/ui@5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.1.9.tgz#406667469e6dbdf320086647d8d80776bb051a51" - integrity sha512-guzKv4VYM+06BzMXeO3QqlX0IwUHyeS6lwdPCL8Oy2V4Gi2IYHHiD6Hr1NgnBO18j9luxE38f4Ii7gEIzXMFbQ== +"@storybook/ui@5.1.10": + version "5.1.10" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.1.10.tgz#4262b1b09efa43d125d694452ae879b89071edd1" + integrity sha512-ezkoVtzoKh93z2wzkqVIqyrIzTkj8tizgAkoPa7mUAbLCxu6LErHITODQoyEiJWI4Epy3yU9GYXFWwT71hdwsA== dependencies: - "@storybook/addons" "5.1.9" - "@storybook/api" "5.1.9" - "@storybook/channels" "5.1.9" - "@storybook/client-logger" "5.1.9" - "@storybook/components" "5.1.9" - "@storybook/core-events" "5.1.9" - "@storybook/router" "5.1.9" - "@storybook/theming" "5.1.9" + "@storybook/addons" "5.1.10" + "@storybook/api" "5.1.10" + "@storybook/channels" "5.1.10" + "@storybook/client-logger" "5.1.10" + "@storybook/components" "5.1.10" + "@storybook/core-events" "5.1.10" + "@storybook/router" "5.1.10" + "@storybook/theming" "5.1.10" copy-to-clipboard "^3.0.8" core-js "^3.0.1" core-js-pure "^3.0.1" @@ -2655,6 +2642,15 @@ "@svgr/plugin-svgo" "^4.0.3" loader-utils "^1.1.0" +"@testing-library/react-hooks@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-1.1.0.tgz#14b6b5c7c3d0e2cb3e55e9cbb248b44321641c64" + integrity sha512-piE/ceQoNf134FFVXBABDbttBJ8eLPD4eg7zIciVJv92RyvoIsBHCvvG8Vd4IG5pyuWYrkLsZTO8ucZBwa4twA== + dependencies: + "@babel/runtime" "^7.4.2" + "@types/react" "^16.8.22" + "@types/react-test-renderer" "^16.8.2" + "@types/babel__core@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.0.tgz#710f2487dda4dcfd010ca6abb2b4dc7394365c51" @@ -2732,10 +2728,10 @@ resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== -"@types/jest@24.0.15": - version "24.0.15" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.15.tgz#6c42d5af7fe3b44ffff7cc65de7bf741e8fa427f" - integrity sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA== +"@types/jest@24.0.16": + version "24.0.16" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.16.tgz#8d3e406ec0f0dc1688d6711af3062ff9bd428066" + integrity sha512-JrAiyV+PPGKZzw6uxbI761cHZ0G7QMOHXPhtSpcl08rZH6CswXaaejckn3goFKmF7M3nzEoJ0lwYCbqLMmjziQ== dependencies: "@types/jest-diff" "*" @@ -2764,6 +2760,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react-test-renderer@^16.8.2": + version "16.8.3" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.3.tgz#b6ca14d5fe4c742fd6d68ef42d45e3b5c6dd470a" + integrity sha512-vEEYh6gFk3jkPHqRe7N5CxWA+yb92hCZxNyhq/1Wj3mClqVWLJYakJDMp3iFmntCgEq96m68N9Oad3wOHm+pdQ== + dependencies: + "@types/react" "*" + "@types/react-wait@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/react-wait/-/react-wait-0.3.0.tgz#6f7ef17571a17e72c7864ede8cf7d3aa525a005e" @@ -2779,6 +2782,14 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/react@^16.8.22": + version "16.8.24" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.24.tgz#8d1ea1fcbfa214220da3d3c04e506f1077b0deac" + integrity sha512-VpFHUoD37YNY2+lr/+c7qL/tZsIU/bKuskUF3tmGUArbxIcQdb5j3zvo4cuuzu2A6UaVmVn7sJ4PgWYNFEBGzg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -5697,16 +5708,6 @@ dom-serializer@0: domelementtype "~1.1.1" entities "~1.1.1" -dom-testing-library@^3.19.0: - version "3.19.0" - resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.19.0.tgz#652ade2cd93ae98beb52b2878eeb9420d7599832" - integrity sha512-gkGXP5GevcjC24Tk6Y6RwrZ7Nz0Ul4bchXV4yHLcnMidMp/EdBCvtHEgHTsZ2yZ4DhUpLowGbJv/1u1Z7bPvtw== - dependencies: - "@babel/runtime" "^7.3.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - pretty-format "^24.5.0" - wait-for-expect "^1.1.0" - dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -6871,10 +6872,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -gh-pages@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/gh-pages/-/gh-pages-2.0.1.tgz#aefe47a43b8d9d2aa3130576b33fe95641e29a2f" - integrity sha512-uFlk3bukljeiWKQ2XvPfjcSi/ou7IfoDf2p+Fj672saLAr8bnOdFVqI/JSgrSgInKpCg5BksxEwGUl++dbg8Dg== +gh-pages@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/gh-pages/-/gh-pages-2.1.0.tgz#dcf519825d77d3a3ee78763076f4158403fc88c4" + integrity sha512-QmV1fh/2W5GZkfoLsG4g6dRTWiNYuCetMQmm8CL6Us8JVnAufYtS0uJPD8NYogmNB4UZzdRG44uPAL+jcBzEwQ== dependencies: async "^2.6.1" commander "^2.18.0" @@ -7373,10 +7374,10 @@ humanize-url@^1.0.0: normalize-url "^1.0.0" strip-url-auth "^1.0.0" -husky@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.1.tgz#06152c28e129622b05fa09c494209de8cf2dfb59" - integrity sha512-PXBv+iGKw23GHUlgELRlVX9932feFL407/wHFwtsGeArp0dDM4u+/QusSQwPKxmNgjpSL+ustbOdQ2jetgAZbA== +husky@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.2.tgz#e78fd2ae16edca59fc88e56aeb8d70acdcc1c082" + integrity sha512-WXCtaME2x0o4PJlKY4ap8BzLA+D0zlvefqAvLCPriOOu+x0dpO5uc5tlB7CY6/0SE2EESmoZsj4jW5D09KrJoA== dependencies: chalk "^2.4.2" cosmiconfig "^5.2.1" @@ -9338,13 +9339,13 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-loader@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/markdown-loader/-/markdown-loader-5.0.0.tgz#d6af0f2d5fe7713fb692cbd85b547ff0ef26baa8" - integrity sha512-CnRuBrTQNJ2VNlyfPJl+14QU6Sfscse4M6TpwuY0KDuCafMHv6vAcVYInphXFtdvtvjG5kMpF+PwN6CWke0M3A== +markdown-loader@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/markdown-loader/-/markdown-loader-5.1.0.tgz#4efd5006b1514ca966141c661a47e542a9836e6e" + integrity sha512-xtQNozLEL+55ZSPTNwro8epZqf1h7HjAZd/69zNe8lbckDiGVHeLQm849bXzocln2pwRK2A/GrW/7MAmwjcFog== dependencies: loader-utils "^1.2.3" - marked "^0.6.0" + marked "^0.7.0" markdown-to-jsx@^6.9.1: version "6.9.2" @@ -9374,10 +9375,10 @@ marked-terminal@^3.2.0: node-emoji "^1.4.1" supports-hyperlinks "^1.0.1" -marked@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.1.tgz#a63addde477bca9613028de4b2bc3629e53a0562" - integrity sha512-+H0L3ibcWhAZE02SKMqmvYsErLo4EAVJxu5h3bHBBDvvjeWXtl92rGUSBYHL2++5Y+RSNgl8dYOAXcYe7lp1fA== +marked@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e" + integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg== material-colors@^1.2.1: version "1.2.6" @@ -11307,16 +11308,6 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^24.5.0: - version "24.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.7.0.tgz#d23106bc2edcd776079c2daa5da02bcb12ed0c10" - integrity sha512-apen5cjf/U4dj7tHetpC7UEFCvtAgnNZnBDkfPv3fokzIqyOJckAG9OlAPC1BlFALnqT/lGB2tl9EJjlK6eCsA== - dependencies: - "@jest/types" "^24.7.0" - ansi-regex "^4.0.0" - ansi-styles "^3.2.0" - react-is "^16.8.4" - pretty-format@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" @@ -11759,14 +11750,6 @@ react-helmet-async@^1.0.2: react-fast-compare "2.0.4" shallowequal "1.1.0" -react-hooks-testing-library@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.4.1.tgz#481b960d647d3cc7c8bbaf410014daa8ce0b1360" - integrity sha512-ogmlyW7ycZe3+HIk1Y+FUV5ExpZwLi/RlF1qGLLbTkUdaAsGgp9fSF+wFNWi2KNKBTWG0FkkqOrLkcwq0z3ebQ== - dependencies: - "@babel/runtime" "^7.4.2" - react-testing-library "^6.0.3" - react-hotkeys@2.0.0-pre4: version "2.0.0-pre4" resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0-pre4.tgz#a1c248a51bdba4282c36bf3204f80d58abc73333" @@ -11795,7 +11778,7 @@ react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== -react-is@^16.8.4: +react-is@^16.8.4, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -11868,13 +11851,15 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-testing-library@^6.0.3: - version "6.1.2" - resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.1.2.tgz#f6bba6eeecedac736eb00b22b4c70bae04535a4f" - integrity sha512-z69lhRDGe7u/NOjDCeFRoe1cB5ckJ4656n0tj/Fdcr6OoBUu7q9DBw0ftR7v5i3GRpdSWelnvl+feZFOyXyxwg== +react-test-renderer@^16.8.6: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" + integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== dependencies: - "@babel/runtime" "^7.4.2" - dom-testing-library "^3.19.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.13.6" react-textarea-autosize@^7.1.0: version "7.1.0" @@ -12643,10 +12628,10 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -semantic-release@15.13.18: - version "15.13.18" - resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.18.tgz#72e284c6f7cb7817e1aaaa0a9d73600a9447d146" - integrity sha512-JtfdrhF1zRm91nJH/Rg3taftbWGwktJqqrJJdbmZGKYx63cfC4PoaS0jxRifGJUdmmgW/Kxz8f5bhtB+p1bu8A== +semantic-release@15.13.19: + version "15.13.19" + resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.19.tgz#d1d05b3516fb8701d81f4e6b9be42bafffef13cb" + integrity sha512-6eqqAmzGaJWgP5R5IkWIQK9is+cWUp/A+pwzxf/YaG1hJv1eD25klUP7Y0fedsPOxxI8eLuDUVlEs7U8SOlK0Q== dependencies: "@semantic-release/commit-analyzer" "^6.1.0" "@semantic-release/error" "^2.2.0" @@ -12665,7 +12650,7 @@ semantic-release@15.13.18: hook-std "^2.0.0" hosted-git-info "^2.7.1" lodash "^4.17.4" - marked "^0.6.0" + marked "^0.7.0" marked-terminal "^3.2.0" p-locate "^4.0.0" p-reduce "^2.0.0" @@ -14402,11 +14387,6 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-for-expect@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.1.tgz#9cd10e07d52810af9e0aaf509872e38f3c3d81ae" - integrity sha512-vd9JOqqEcBbCDhARWhW85ecjaEcfBLuXgVBqatfS3iw6oU4kzAcs+sCNjF+TC9YHPImCW7ypsuQc+htscIAQCw== - walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"