From 6f31a70df40fbe3738a194325b889d7ba1347e0f Mon Sep 17 00:00:00 2001 From: Antonio Russo Date: Sat, 7 Aug 2021 17:26:38 +0200 Subject: [PATCH] Typescript rewrite (version 1.0.0) --- .babelrc | 6 - .eslintignore | 1 - .eslintrc | 22 +- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 6 + babel.config.js | 3 + docs/Installation.md | 10 +- docs/Introduction.md | 17 +- .../CustomComponentListRenderer.js | 45 --- docs/styleguidist/CustomLogo.js | 28 -- docs/styleguidist/CustomSidebar.js | 11 - docs/styleguidist/getHooksDocFiles.js | 13 - docs/styleguidist/styleguidist.config.js | 35 -- docs/styleguidist/webpack.config.js | 27 -- docs/useValidatedState.md | 2 +- docs/utils/_CustomLogo.js | 26 ++ .../_EmptyComponent.js} | 0 .../custom.css => utils/_custom.css} | 2 +- .../doc-logo.png => utils/_doc-logo.png} | Bin .../setup.js => utils/_setup.js} | 0 .../_styleguidist.theme.js} | 3 - index.d.ts | 346 ------------------ package.json | 76 ++-- rollup.config.js | 36 -- src/index.js | 40 -- src/index.ts | 40 ++ src/shared/assignEventOnMount.ts | 33 ++ src/shared/createCbSetterErrorProxy.ts | 16 + src/shared/createStorageHook.ts | 43 +++ src/shared/geolocationStandardOptions.ts | 7 + src/shared/isAPISupported.ts | 6 + src/{utils/isClient.js => shared/isClient.ts} | 4 +- .../isDevelopment.ts} | 4 +- .../isSamePosition.ts} | 14 +- src/shared/makePositionObject.ts | 30 ++ src/shared/safeHasOwnProperty.ts | 3 + src/shared/safelyParseJson.ts | 9 + src/shared/swipeUtils.ts | 31 ++ src/shared/types.ts | 11 + src/shared/useHandlerSetterRef.ts | 33 ++ ...nalTimeout.js => useConditionalTimeout.ts} | 56 +-- src/useDebouncedFn.js | 20 - src/useDebouncedFn.ts | 28 ++ src/useDefaultedState.js | 16 - src/useDefaultedState.ts | 18 + src/useDidMount.js | 19 - src/useDidMount.ts | 20 + src/useDrag.js | 37 -- src/useDrag.ts | 43 +++ src/useDragEvents.js | 74 ---- src/useDragEvents.ts | 89 +++++ src/useDropZone.js | 20 - src/useDropZone.ts | 23 ++ src/useGeolocation.js | 17 - src/useGeolocation.ts | 17 + src/useGeolocationEvents.js | 48 --- src/useGeolocationEvents.ts | 55 +++ src/useGeolocationState.js | 45 --- src/useGeolocationState.ts | 51 +++ src/useGlobalEvent.js | 38 -- src/useGlobalEvent.ts | 31 ++ src/useHorizontalSwipe.js | 20 - src/useHorizontalSwipe.ts | 18 + src/useInterval.js | 54 --- src/useInterval.ts | 59 +++ src/useLifecycle.js | 15 - src/useLifecycle.ts | 16 + src/useLocalStorage.js | 8 - src/useLocalStorage.ts | 8 + src/{useMediaQuery.js => useMediaQuery.ts} | 50 ++- src/useMouse.js | 16 - src/useMouse.ts | 17 + src/useMouseEvents.js | 53 --- src/useMouseEvents.ts | 66 ++++ src/useMouseState.js | 28 -- src/useMouseState.ts | 35 ++ src/useObservable.js | 14 - src/useObservable.ts | 16 + src/{useOnlineState.js => useOnlineState.ts} | 32 +- src/usePreviousValue.js | 20 - src/usePreviousValue.ts | 20 + src/useRenderInfo.js | 36 -- src/useRenderInfo.ts | 43 +++ src/useRequestAnimationFrame.js | 46 --- src/useRequestAnimationFrame.ts | 52 +++ src/useResizeObserver.js | 55 --- src/useResizeObserver.ts | 58 +++ src/useSessionStorage.js | 8 - src/useSessionStorage.ts | 8 + src/useSpeechSynthesis.js | 37 -- src/useSpeechSynthesis.ts | 49 +++ src/useStorage.js | 43 --- src/useSwipe.js | 137 ------- src/useSwipe.ts | 151 ++++++++ src/useSwipeEvents.js | 156 -------- src/useSwipeEvents.ts | 175 +++++++++ ...{useSystemVoices.js => useSystemVoices.ts} | 24 +- src/useThrottledFn.js | 20 - src/useThrottledFn.ts | 27 ++ src/useTimeout.js | 52 --- src/useTimeout.ts | 57 +++ src/useTouch.js | 16 - src/useTouch.ts | 17 + src/useTouchEvents.js | 44 --- src/useTouchEvents.ts | 55 +++ src/useTouchState.js | 20 - src/useTouchState.ts | 22 ++ src/useValidatedState.js | 18 - src/useValidatedState.ts | 19 + src/useValueHistory.js | 23 -- src/useValueHistory.ts | 23 ++ src/useVerticalSwipe.js | 20 - src/useVerticalSwipe.ts | 21 ++ src/useViewportSpy.js | 43 --- src/useViewportSpy.ts | 47 +++ src/useWillUnmount.js | 19 - src/useWillUnmount.ts | 20 + src/useWindowResize.js | 8 - src/useWindowResize.ts | 8 + src/useWindowScroll.js | 8 - src/useWindowScroll.ts | 8 + src/utils/assignEventCallbackOnMountEffect.js | 32 -- src/utils/createCbSetterErrorProxy.js | 16 - src/utils/createHandlerSetter.js | 27 -- src/utils/geolocationStandardOptions.js | 7 - src/utils/hasOwnProperty.js | 3 - src/utils/isAPISupported.js | 6 - src/utils/makePositionObject.js | 17 - src/utils/safelyParseJson.js | 9 - src/utils/swipeUtils.js | 29 -- styleguide.config.js | 70 ++++ test/createCbSetterErrorProxy.spec.js | 2 +- test/geolocationStandardOptions.spec.js | 2 +- test/isAPISupported.spec.js | 2 +- test/isClient.spec.js | 2 +- test/isSamePosition.spec.js | 2 +- test/makePositionObject.spec.js | 2 +- ...rty.spec.js => safeHasOwnProperty.spec.js} | 12 +- ...etter.spec.js => useHandlerSetter.spec.js} | 14 +- test/useStorage.spec.js | 10 +- test/useSwipe.spec.js | 4 +- tsconfig.cjs.json | 6 + tsconfig.esm.json | 8 + tsconfig.json | 15 + 144 files changed, 2076 insertions(+), 2285 deletions(-) delete mode 100644 .babelrc create mode 100644 babel.config.js delete mode 100644 docs/styleguidist/CustomComponentListRenderer.js delete mode 100644 docs/styleguidist/CustomLogo.js delete mode 100644 docs/styleguidist/CustomSidebar.js delete mode 100644 docs/styleguidist/getHooksDocFiles.js delete mode 100644 docs/styleguidist/styleguidist.config.js delete mode 100644 docs/styleguidist/webpack.config.js create mode 100644 docs/utils/_CustomLogo.js rename docs/{styleguidist/EmptyComponent.js => utils/_EmptyComponent.js} (100%) rename docs/{styleguidist/custom.css => utils/_custom.css} (81%) rename docs/{styleguidist/doc-logo.png => utils/_doc-logo.png} (100%) rename docs/{styleguidist/setup.js => utils/_setup.js} (100%) rename docs/{styleguidist/styleguidist.theme.js => utils/_styleguidist.theme.js} (96%) delete mode 100644 index.d.ts delete mode 100644 rollup.config.js delete mode 100644 src/index.js create mode 100644 src/index.ts create mode 100644 src/shared/assignEventOnMount.ts create mode 100644 src/shared/createCbSetterErrorProxy.ts create mode 100644 src/shared/createStorageHook.ts create mode 100644 src/shared/geolocationStandardOptions.ts create mode 100644 src/shared/isAPISupported.ts rename src/{utils/isClient.js => shared/isClient.ts} (61%) rename src/{utils/isDevelopment.js => shared/isDevelopment.ts} (81%) rename src/{utils/isSamePosition.js => shared/isSamePosition.ts} (66%) create mode 100644 src/shared/makePositionObject.ts create mode 100644 src/shared/safeHasOwnProperty.ts create mode 100644 src/shared/safelyParseJson.ts create mode 100644 src/shared/swipeUtils.ts create mode 100644 src/shared/types.ts create mode 100644 src/shared/useHandlerSetterRef.ts rename src/{useConditionalTimeout.js => useConditionalTimeout.ts} (51%) delete mode 100644 src/useDebouncedFn.js create mode 100644 src/useDebouncedFn.ts delete mode 100644 src/useDefaultedState.js create mode 100644 src/useDefaultedState.ts delete mode 100644 src/useDidMount.js create mode 100644 src/useDidMount.ts delete mode 100644 src/useDrag.js create mode 100644 src/useDrag.ts delete mode 100644 src/useDragEvents.js create mode 100644 src/useDragEvents.ts delete mode 100644 src/useDropZone.js create mode 100644 src/useDropZone.ts delete mode 100644 src/useGeolocation.js create mode 100644 src/useGeolocation.ts delete mode 100644 src/useGeolocationEvents.js create mode 100644 src/useGeolocationEvents.ts delete mode 100644 src/useGeolocationState.js create mode 100644 src/useGeolocationState.ts delete mode 100644 src/useGlobalEvent.js create mode 100644 src/useGlobalEvent.ts delete mode 100644 src/useHorizontalSwipe.js create mode 100644 src/useHorizontalSwipe.ts delete mode 100644 src/useInterval.js create mode 100644 src/useInterval.ts delete mode 100644 src/useLifecycle.js create mode 100644 src/useLifecycle.ts delete mode 100644 src/useLocalStorage.js create mode 100644 src/useLocalStorage.ts rename src/{useMediaQuery.js => useMediaQuery.ts} (55%) delete mode 100644 src/useMouse.js create mode 100644 src/useMouse.ts delete mode 100644 src/useMouseEvents.js create mode 100644 src/useMouseEvents.ts delete mode 100644 src/useMouseState.js create mode 100644 src/useMouseState.ts delete mode 100644 src/useObservable.js create mode 100644 src/useObservable.ts rename src/{useOnlineState.js => useOnlineState.ts} (56%) delete mode 100644 src/usePreviousValue.js create mode 100644 src/usePreviousValue.ts delete mode 100644 src/useRenderInfo.js create mode 100644 src/useRenderInfo.ts delete mode 100644 src/useRequestAnimationFrame.js create mode 100644 src/useRequestAnimationFrame.ts delete mode 100644 src/useResizeObserver.js create mode 100644 src/useResizeObserver.ts delete mode 100644 src/useSessionStorage.js create mode 100644 src/useSessionStorage.ts delete mode 100644 src/useSpeechSynthesis.js create mode 100644 src/useSpeechSynthesis.ts delete mode 100644 src/useStorage.js delete mode 100644 src/useSwipe.js create mode 100644 src/useSwipe.ts delete mode 100644 src/useSwipeEvents.js create mode 100644 src/useSwipeEvents.ts rename src/{useSystemVoices.js => useSystemVoices.ts} (52%) delete mode 100644 src/useThrottledFn.js create mode 100644 src/useThrottledFn.ts delete mode 100644 src/useTimeout.js create mode 100644 src/useTimeout.ts delete mode 100644 src/useTouch.js create mode 100644 src/useTouch.ts delete mode 100644 src/useTouchEvents.js create mode 100644 src/useTouchEvents.ts delete mode 100644 src/useTouchState.js create mode 100644 src/useTouchState.ts delete mode 100644 src/useValidatedState.js create mode 100644 src/useValidatedState.ts delete mode 100644 src/useValueHistory.js create mode 100644 src/useValueHistory.ts delete mode 100644 src/useVerticalSwipe.js create mode 100644 src/useVerticalSwipe.ts delete mode 100644 src/useViewportSpy.js create mode 100644 src/useViewportSpy.ts delete mode 100644 src/useWillUnmount.js create mode 100644 src/useWillUnmount.ts delete mode 100644 src/useWindowResize.js create mode 100644 src/useWindowResize.ts delete mode 100644 src/useWindowScroll.js create mode 100644 src/useWindowScroll.ts delete mode 100644 src/utils/assignEventCallbackOnMountEffect.js delete mode 100644 src/utils/createCbSetterErrorProxy.js delete mode 100644 src/utils/createHandlerSetter.js delete mode 100644 src/utils/geolocationStandardOptions.js delete mode 100644 src/utils/hasOwnProperty.js delete mode 100644 src/utils/isAPISupported.js delete mode 100644 src/utils/makePositionObject.js delete mode 100644 src/utils/safelyParseJson.js delete mode 100644 src/utils/swipeUtils.js create mode 100644 styleguide.config.js rename test/{hasOwnProperty.spec.js => safeHasOwnProperty.spec.js} (54%) rename test/{createHandlerSetter.spec.js => useHandlerSetter.spec.js} (72%) create mode 100644 tsconfig.cjs.json create mode 100644 tsconfig.esm.json create mode 100644 tsconfig.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 85196b55..00000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": [ - "@babel/preset-react", - "@babel/preset-env" - ] -} diff --git a/.eslintignore b/.eslintignore index 4546fb9f..e454be5d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,3 @@ /docs /test /docs -index.d.ts diff --git a/.eslintrc b/.eslintrc index c935f1a4..0475b64f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,16 +1,28 @@ { - "extends": "airbnb", - "parser": "babel-eslint", - "plugins": ["chai-expect", "react-hooks"], + "extends": [ + "airbnb-base", + "airbnb-typescript" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "project": "./tsconfig.esm.json" + }, + "plugins": [ + "chai-expect", + "react-hooks" + ], "env": { "browser": true, "mocha": true }, "rules": { - "max-len": ["error", { "code": 120 }], + "max-len": ["error", {"code": 140}], + "semi": [2, "never"], + "@typescript-eslint/semi": "off", "linebreak-style": "off", "object-curly-newline": "off", - "react/jsx-filename-extension": [1, { "extensions": [ ".js", ".jsx" ] } ] + "react/jsx-filename-extension": "off" }, "overrides": [ { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8be9680..29961e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 registry-url: https://registry.npmjs.org/ - name: NPM CI diff --git a/CHANGELOG.md b/CHANGELOG.md index 53320b06..1d724fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -725,3 +725,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - `useSwipeEvents` exports`onSwipeEnd`,`onSwipeStart` + +## [1.0.0] - 2021-08-27 + +### Change + +- Complete typescript rewrite diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..6e4a54db --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['@babel/react', '@babel/env'] +} diff --git a/docs/Installation.md b/docs/Installation.md index 45800c40..251f1a9d 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -1,18 +1,20 @@ # Getting started - Using `npm`: + ```bash $ npm i --save beautiful-react-hooks ``` Using `yarn`: + ```bash $ yarn add beautiful-react-hooks ``` then just import any hook described by the documentation in your React component file: -```js -import { useSomeHook } from 'beautiful-react-hooks'; -``` \ No newline at end of file +```ts static +import { useSomeHook } from 'beautiful-react-hooks' +import useSomeHook from 'beautiful-react-hoks/useSomeHook' +``` diff --git a/docs/Introduction.md b/docs/Introduction.md index ae867655..9456ab2b 100644 --- a/docs/Introduction.md +++ b/docs/Introduction.md @@ -10,7 +10,7 @@ components and hooks development. ## 💡 Why? -React custom hooks allow to abstract components' business logic into single reusable functions.
+React custom hooks allow abstracting components' business logic into single reusable functions.
So far, I've found that most of the hooks I've created and therefore shared between my projects have quite often a similar gist that involves callback references, events and components' lifecycle.
For this reason I've tried to sum up that gist into `beautiful-react-hooks`: a collection of (*hopefully*) useful @@ -18,7 +18,6 @@ React hooks to possibly help other developers to speed up their development proc Furthermore, I've tried to create a concise yet concrete API having in mind the code readability, focusing to keep the learning curve as lower as possible so that the it can be used and shared in bigger teams. - ## ☕️ Features * Concise API @@ -26,17 +25,3 @@ to keep the learning curve as lower as possible so that the it can be used and s * Easy to learn * Functional approach * Fully written in JS (although TS types are supported) - -### Credits - -This library is provided and sponsored by: - -
-

- - Beautiful interactions - -

-
- -As part of our commitment to support and provide the open source community. diff --git a/docs/styleguidist/CustomComponentListRenderer.js b/docs/styleguidist/CustomComponentListRenderer.js deleted file mode 100644 index a2581936..00000000 --- a/docs/styleguidist/CustomComponentListRenderer.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Sidebar } from 'beautiful-react-ui'; - -const SidebarItem = (props) => { - const { visibleName, selected, href } = props; - - return ( - <> - - {visibleName === 'Installation' && } - - ); -}; - -const SidebarCollapsible = (props) => { - const { visibleName, selected, sections, href } = props; - - return ( - - {sections.map((compProps) => ( - - ))} - - ); -}; - -const SidebarItemRenderer = (props) => { - const { sections, components, href } = props; - const isLeaf = sections.length === 0 && components.length === 0; - const Component = isLeaf ? SidebarItem : SidebarCollapsible; - - return ( - - ); -}; - - -const CustomComponentListRenderer = ({ items }) => items.map(SidebarItemRenderer); - -export default CustomComponentListRenderer; diff --git a/docs/styleguidist/CustomLogo.js b/docs/styleguidist/CustomLogo.js deleted file mode 100644 index 2703b82a..00000000 --- a/docs/styleguidist/CustomLogo.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Styled from 'rsg-components/Styled'; -import logo from './doc-logo.png'; - -const styles = ({ fontFamily, color }) => ({ - logo: { - display: 'block', - }, - image: { - width: '100%', - }, -}); - -export function LogoRenderer({ classes }) { - return ( -

- Beautiful React Hooks -

- ); -} - -LogoRenderer.propTypes = { - classes: PropTypes.object.isRequired, - children: PropTypes.node, -}; - -export default Styled(styles)(LogoRenderer); diff --git a/docs/styleguidist/CustomSidebar.js b/docs/styleguidist/CustomSidebar.js deleted file mode 100644 index aacd76d3..00000000 --- a/docs/styleguidist/CustomSidebar.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { Sidebar } from 'beautiful-react-ui'; -import CustomLogo from './CustomLogo'; - -const CustomSidebar = ({ children }) => ( - }> - {children} - -); - -export default CustomSidebar; diff --git a/docs/styleguidist/getHooksDocFiles.js b/docs/styleguidist/getHooksDocFiles.js deleted file mode 100644 index 407a26c4..00000000 --- a/docs/styleguidist/getHooksDocFiles.js +++ /dev/null @@ -1,13 +0,0 @@ -const glob = require('glob'); -const path = require('path'); - -const getHooksDocFiles = () => glob.sync(path.join(__dirname, '..', '[use]*.md')).map((filePath) => { - const [filename] = filePath.match(/use[a-zA-Z]*/, 'gm'); - - return ({ - name: filename, - content: `../${filename}.md`, - }); -}); - -module.exports = getHooksDocFiles; diff --git a/docs/styleguidist/styleguidist.config.js b/docs/styleguidist/styleguidist.config.js deleted file mode 100644 index a6fc11cd..00000000 --- a/docs/styleguidist/styleguidist.config.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require('path'); -const theme = require('./styleguidist.theme'); -const getHooksDocFiles = require('./getHooksDocFiles'); - -module.exports = { - title: 'Beautiful React Hooks docs', - /* eslint-disable global-require */ - webpackConfig: require('./webpack.config.js'), - /* eslint-enable global-require */ - ignore: ['test/**/*.spec.{js,jsx}', 'node_modules', 'docs', 'test'], - exampleMode: 'expand', - pagePerSection: true, - skipComponentsWithoutExample: true, - styleguideDir: '../../dist-ghpages', - ribbon: { - url: 'https://github.com/beautifulinteractions/beautiful-react-hooks', - text: 'Fork me on GitHub', - }, - sections: [ - { name: 'Introduction', content: '../Introduction.md', sectionDepth: 1, }, - { name: 'Installation', content: '../Installation.md', sectionDepth: 1, }, - ...getHooksDocFiles(), - ], - require: [path.join(__dirname, 'setup.js'), path.join(__dirname, 'custom.css')], - // Override Styleguidist components - styleguideComponents: { - LogoRenderer: path.join(__dirname, 'EmptyComponent'), - PathlineRenderer: path.join(__dirname, 'EmptyComponent'), - ToolbarButtonRenderer: path.join(__dirname, 'EmptyComponent'), - TableOfContentsRenderer: path.join(__dirname, 'CustomSidebar'), - ComponentsListRenderer: path.join(__dirname, 'CustomComponentListRenderer'), - }, - ...theme, -}; - diff --git a/docs/styleguidist/webpack.config.js b/docs/styleguidist/webpack.config.js deleted file mode 100644 index 3c4f55fd..00000000 --- a/docs/styleguidist/webpack.config.js +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('path'); - -// local constants -const srcPath = path.resolve(__dirname, '../..', 'src'); - -module.exports = { - resolve: { - alias: { 'beautiful-react-hooks': srcPath }, - }, - module: { - rules: [ - { - test: /\.jsx?$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - { - test: /\.png$/, - loader: 'url-loader', - }, - ], - }, -}; diff --git a/docs/useValidatedState.md b/docs/useValidatedState.md index 1d47bb12..e8303b14 100644 --- a/docs/useValidatedState.md +++ b/docs/useValidatedState.md @@ -40,4 +40,4 @@ const ValidatedField = () => { #### ✅ good to know: -- useValidatedState does not re-render your component twice to save the validation state. \ No newline at end of file +- useValidatedState does not re-render your component twice to save the validation state. diff --git a/docs/utils/_CustomLogo.js b/docs/utils/_CustomLogo.js new file mode 100644 index 00000000..617e6859 --- /dev/null +++ b/docs/utils/_CustomLogo.js @@ -0,0 +1,26 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Styled from 'rsg-components/Styled' +import logo from './_doc-logo.png' + +const styles = ({ fontFamily, color }) => ({ + logo: { + display: 'block' + }, + image: { + width: '100%' + } +}) + +const LogoRenderer = ({ classes }) => ( +

+ Beautiful React Hooks +

+) + +LogoRenderer.propTypes = { + classes: PropTypes.object.isRequired, + children: PropTypes.node +} + +export default Styled(styles)(LogoRenderer) diff --git a/docs/styleguidist/EmptyComponent.js b/docs/utils/_EmptyComponent.js similarity index 100% rename from docs/styleguidist/EmptyComponent.js rename to docs/utils/_EmptyComponent.js diff --git a/docs/styleguidist/custom.css b/docs/utils/_custom.css similarity index 81% rename from docs/styleguidist/custom.css rename to docs/utils/_custom.css index f44096ad..8519eff9 100644 --- a/docs/styleguidist/custom.css +++ b/docs/utils/_custom.css @@ -2,5 +2,5 @@ @import "~beautiful-react-ui/beautiful-react-ui.css"; body { - font-family: 'Ubuntu', sans-serif; + font-family: 'Ubuntu', sans-serif; } diff --git a/docs/styleguidist/doc-logo.png b/docs/utils/_doc-logo.png similarity index 100% rename from docs/styleguidist/doc-logo.png rename to docs/utils/_doc-logo.png diff --git a/docs/styleguidist/setup.js b/docs/utils/_setup.js similarity index 100% rename from docs/styleguidist/setup.js rename to docs/utils/_setup.js diff --git a/docs/styleguidist/styleguidist.theme.js b/docs/utils/_styleguidist.theme.js similarity index 96% rename from docs/styleguidist/styleguidist.theme.js rename to docs/utils/_styleguidist.theme.js index b29e9e81..f0897d5e 100644 --- a/docs/styleguidist/styleguidist.theme.js +++ b/docs/utils/_styleguidist.theme.js @@ -36,9 +36,6 @@ module.exports = { }, }, StyleGuide: { - logo: { - display: 'none', - }, sidebar: { border: 0, width: '16rem', diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 44bdd850..00000000 --- a/index.d.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { DependencyList, Dispatch, EffectCallback, MutableRefObject, SetStateAction } from 'react'; -import { Observable } from 'rxjs'; - -declare module 'beautiful-react-hooks' { - - type GenericCallback = (...args: unknown[]) => unknown; - - type TimeoutOrIntervalOpts = { - cancelOnUnmount: boolean, - } - - type HandlerSetter = (fn: (arg: T) => R) => unknown; - - /** - * useMouseEvents - */ - type MouseTarget = HTMLElement | Document | Window; - - type MouseState = { - clientX: number, - clientY: number, - screenX: number, - screenY: number, - } - - type MouseHandlerSetters = { - onMouseDown: HandlerSetter, - onMouseEnter: HandlerSetter, - onMouseLeave: HandlerSetter, - onMouseMove: HandlerSetter, - onMouseOut: HandlerSetter, - onMouseOver: HandlerSetter, - onMouseUp: HandlerSetter, - } - - /** - * useConditionalTimeout - */ - export const useConditionalTimeout: (fn: GenericCallback, milliseconds: number, condition: boolean, options?: TimeoutOrIntervalOpts) => [boolean, EffectCallback]; - - type Cancelable = { - cancel(): void; - flush(): void; - } - - type ThrottleOrDebounceOpts = { - leading: boolean, - trailing: boolean, - } - - /** - * Accepts a function and returns a new debounced yet memoized version of that same function that delays - * its invoking by the defined time. - * - * If `wait` is not defined, its default value will be 250ms. - */ - export const useDebouncedFn: (fn: F, wait?: number, options?: Partial, dependencies?: DependencyList) => F & Cancelable; - - - /** - * useDefaultedState - */ - export const useDefaultedState: (defaultValue: S, initialState?: S) => [S, SetStateAction]; - - - /** - * useDidMount - */ - export const useDidMount: (handler: CallableFunction) => HandlerSetter; - - type DragOptions = { - dragImage?: string, - dragImageXOffset?: number, - dragImageYOffset?: number, - transfer?: object | string | number, - transferFormat: 'text' | 'text/plain', - }; - - /** - * useDrag - */ - export const useDrag: (ref: MutableRefObject | null | undefined, options: DragOptions) => boolean; - - /** - * useDragEvents - */ - export const useDragEvents: (ref: MutableRefObject | null | undefined, setDraggable?: boolean) => ({ - onDrag: HandlerSetter, - onDrop: HandlerSetter, - onDragEnter: HandlerSetter, - onDragEnd: HandlerSetter, - onDragExit: HandlerSetter, - onDragLeave: HandlerSetter, - onDragOver: HandlerSetter, - onDragStart: HandlerSetter, - }); - - - export const useDropZone: (ref: MutableRefObject | null | undefined) => ({ isOver: boolean, onDrop: HandlerSetter }) - - - /** - * useGeolocation - */ - export const useGeolocation: (options?: PositionOptions) => [GeolocationState, GeolocationCallbackSetters]; - - /** - * useGeolocationEvents - */ - type GeolocationCallbackSetters = { - isSupported: boolean, - onChange: HandlerSetter, - onError: HandlerSetter, - } - export const useGeolocationEvents: (options?: PositionOptions) => GeolocationCallbackSetters; - - /** - * useGeolocationState - */ - type GeolocationState = { - isSupported: boolean, - isRetrieving: boolean, - position: { - timestamp: number, - coords: { - latitude: number, - longitude: number, - altitude: number, - accuracy: number, - altitudeAccuracy: number, - heading: number, - speed: number, - }, - } - } - export const useGeolocationState: (options?: PositionOptions) => GeolocationState; - - /** - * useGlobalEvent - */ - export const useGlobalEvent: (eventName: string, options?: EventListenerOptions, handler?: Function) => HandlerSetter; - - type OrientedSwipeOptions = Pick - - export const useHorizontalSwipe: (ref?: MutableRefObject | null | undefined, options?: OrientedSwipeOptions) => SwipeState - - /** - * useInterval - */ - export const useInterval: (fn: Function, milliseconds: number, options?: TimeoutOrIntervalOpts) => [boolean, EffectCallback]; - - /** - * useLifecycle - */ - export const useLifecycle: (mount: Function, unmount: Function) => { onDidMount: HandlerSetter, onWillUnmount: HandlerSetter }; - - /** - * useLocalStorage - */ - export const useLocalStorage: (localStorageKey: string, defaultValue: T) => [T, HandlerSetter]; - - /** - * useMediaQuery - */ - export const useMediaQuery: (mediaQuery: string) => boolean; - - /** - * useMouse - */ - export const useMouse: (ref?: MutableRefObject) => [MouseState, MouseHandlerSetters]; - - /** - * useMouseEvents - */ - type MouseCallbackSetters = { - onMouseDown: HandlerSetter, - onMouseEnter: HandlerSetter, - onMouseLeave: HandlerSetter, - onMouseMove: HandlerSetter, - onMouseOut: HandlerSetter, - onMouseOver: HandlerSetter, - onMouseUp: HandlerSetter, - } - export const useMouseEvents: (ref?: MutableRefObject) => MouseCallbackSetters; - - /** - * useMouseState - */ - export const useMouseState: (ref?: MutableRefObject) => MouseState; - - /** - * useObservable - */ - export const useObservable: (observable: Observable, setter: Dispatch>) => void; - - /** - * useOnlineState - */ - export const useOnlineState: () => boolean; - /** - * usePreviousValue - */ - export const usePreviousValue: (value: T) => T; - - type RenderInfo = { - module: string, - renders: number, - timestamp: number, - sinceLast: string, - } - - export const useRenderInfo: (name?: string, log?: boolean) => RenderInfo; - - /** - * useRequestAnimationFrame - */ - - type UseRequestAnimationFrameOptions = { increment: number, startAt: number, finishAt: number }; - - export const useRequestAnimationFrame: (func: Function, options?: UseRequestAnimationFrameOptions) => HandlerSetter; - - /** - * useResizeObserver - */ - export const useResizeObserver: (elementRef: MutableRefObject, timeout?: number) => DOMRect | undefined; - - /** - * useSessionStorage - */ - export const useSessionStorage: (localStorageKey: string, defaultValue: T) => [T, HandlerSetter]; - - /** - * useSpeechSynthesis - */ - type SpeechOptions = { - voice?: SpeechSynthesisVoice, - pitch?: number, - volume?: number, - rate?: number, - } - - export const useSpeechSynthesis: (text: string, options?: SpeechOptions) => ({ speak: Function, speechSynthUtterance: SpeechSynthesisUtterance }); - - /** - * useStorage - */ - export const useStorage: (type: 'local' | 'session') => typeof useSessionStorage | typeof useLocalStorage; - - - type UseSwipeOptions = { - direction?: 'both' | 'vertical' | 'horizontal', - threshold?: number, - preventDefault?: boolean, - } - - type SwipeState = { - swiping: boolean, - direction: 'left' | 'right' | 'up' | 'down' | null, - alphaX: number, - alphaY: number, - count: number, - } - - export const useSwipe: (ref?: MutableRefObject | null | undefined, options?: UseSwipeOptions) => SwipeState - - export const useSwipeEvents: (ref?: MutableRefObject | null | undefined, options?: OrientedSwipeOptions) => ({ - onSwipeLeft: HandlerSetter>, - onSwipeRight: HandlerSetter>, - onSwipeUp: HandlerSetter>, - onSwipeDown: HandlerSetter>, - onSwipeStart: HandlerSetter<{ clientX: number, clientY: number }>, - onSwipeMove: HandlerSetter<{ clientX: number, clientY: number } & Pick>, - onSwipeEnd: HandlerSetter>, - }) - - /** - * useSystemVoices - */ - export const useSystemVoices: () => Array; - - /** - * useThrottledFn - */ - export const useThrottledFn: (fn: F, wait?: number, options?: ThrottleOrDebounceOpts, dependencies?: DependencyList) => F & Cancelable; - - /** - * useTimeout - */ - export const useTimeout: (fn: Function, milliseconds: number, options?: TimeoutOrIntervalOpts) => [boolean, EffectCallback]; - - /** - * useTouch - */ - export const useTouch: (ref?: MutableRefObject) => [TouchList, TouchCallbackSetters]; - - /** - * useTouchEvents - */ - type TouchCallbackSetters = { - onTouchStart: HandlerSetter, - onTouchEnd: HandlerSetter, - onTouchMove: HandlerSetter, - onTouchCancel: HandlerSetter, - } - export const useTouchEvents: (ref?: MutableRefObject) => MouseCallbackSetters; - - /** - * useTouchState - */ - export const useTouchState: (ref?: MutableRefObject) => TouchList; - - - /** - * useValidatedState - */ - export const useValidatedState: (validator: Function, initialState?: any) => [ - any, - Dispatch>, - { changed: boolean, valid: boolean } - ]; - - /** - * useValueHistory - */ - export const useValueHistory: (value: any, distinct?: boolean) => Array; - - export const useVerticalSwipe: (ref?: MutableRefObject | null | undefined, options?: OrientedSwipeOptions) => SwipeState - /** - * useViewportSpy - */ - export const useViewportSpy: (elementRef: MutableRefObject, options?: IntersectionObserverInit) => boolean; - /** - * useWillUnmount - */ - export const useWillUnmount: (handler?: Function) => HandlerSetter; - - /** - * useWindowResize - */ - export const useWindowResize: (handler: Function) => HandlerSetter; - - /** - * useWindowScroll - */ - export const useWindowScroll: (handler: Function) => HandlerSetter; -} diff --git a/package.json b/package.json index ff79e7c7..1df3c5d1 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,25 @@ { "name": "beautiful-react-hooks", - "version": "0.35.0", + "version": "1.0.0", "description": "A collection of beautiful (and hopefully useful) React hooks to speed-up your components and hooks development", "main": "index.js", "module": "esm/index.js", - "typings": "index.d.ts", "scripts": { - "lint": "eslint --ext .js src/", - "build": "npx del-cli dist && rollup -c", - "build-doc": "npx del-cli dist-ghpages && styleguidist build --config docs/styleguidist/styleguidist.config.js", + "lint": "eslint src/ --ext .ts", + "build-cjs": "tsc --project ./tsconfig.cjs.json", + "build-esm": "tsc --project ./tsconfig.esm.json", + "build": "npx del-cli dist && npm run build-cjs && npm run build-esm && echo '\nSuccessfully built'", + "build-doc": "npx del-cli dist-ghpages && npx styleguidist build", "test": "nyc mocha --recursive --exit \"./test/**/*.spec.+(js|jsx)\"", "test:watch": "npm run test -- --watch", - "start": "styleguidist server --config docs/styleguidist/styleguidist.config.js --open" + "start": "npx styleguidist server" }, + "browserslist": [ + ">1%", + "last 1 version", + "Firefox ESR", + "not dead" + ], "repository": { "type": "git", "url": "git+https://github.com/beautifulinteractions/beautiful-react-hooks.git" @@ -39,47 +46,50 @@ "react-dom": ">=17.0.2" }, "devDependencies": { - "@babel/core": "^7.14.0", + "@babel/core": "7.15.0", "@babel/polyfill": "^7.10.4", - "@babel/preset-env": "^7.14.1", - "@babel/preset-react": "^7.13.13", - "@babel/register": "^7.13.16", - "@rollup/plugin-node-resolve": "13.0.0", - "@testing-library/react": "11.2.6", - "@testing-library/react-hooks": "^5.1.2", + "@babel/preset-env": "7.15.0", + "@babel/preset-react": "7.14.5", + "@babel/register": "^7.15.3", + "@testing-library/react": "12.0.0", + "@testing-library/react-hooks": "^7.0.1", + "@types/lodash.debounce": "4.0.6", + "@types/lodash.throttle": "4.1.6", + "@typescript-eslint/eslint-plugin": "4.29.3", + "@typescript-eslint/parser": "4.29.3", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "babel-plugin-istanbul": "^6.0.0", "babel-plugin-transform-require-ignore": "^0.1.1", "beautiful-react-ui": "^0.57.1", "chai": "^4.3.4", - "css-loader": "^5.2.4", - "eslint": "^7.25.0", - "eslint-config-airbnb": "^18.2.1", + "css-loader": "^2.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "14.2.1", + "eslint-config-airbnb-typescript": "14.0.0", "eslint-plugin-chai-expect": "^2.2.0", - "eslint-plugin-import": "^2.22.1", + "eslint-plugin-import": "2.24.2", "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "4.2.0", - "glob": "^7.1.6", - "husky": "^6.0.0", + "glob": "^7.1.7", + "husky": "^7.0.2", "jsdoc-to-markdown": "^7.0.1", - "jsdom": "^16.5.3", + "jsdom": "^17.0.0", "jsdom-global": "^3.0.2", - "mocha": "8.3.2", + "mocha": "9.1.0", "mock-local-storage": "1.1.17", "nyc": "^15.1.0", - "react": "^17.0.0", - "react-dom": "^17.0.0", - "react-styleguidist": "^11.1.6", - "react-test-renderer": "^17.0.2", - "regenerator-runtime": "0.13.7", - "rollup": "2.47.0", - "rollup-plugin-babel": "4.4.0", - "rxjs": "7.0.0", - "sinon": "^10.0.0", - "style-loader": "2.0.0", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-styleguidist": "^9.0.0", + "regenerator-runtime": "0.13.9", + "rxjs": "7.3.0", + "sinon": "^11.1.2", + "style-loader": "^2.0.0", + "ts-loader": "8.3.0", + "typescript": "4.3.5", "url-loader": "^4.1.1", - "webpack": "^5.36.2" + "webpack": "^4.9.2" } } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 266d8d0b..00000000 --- a/rollup.config.js +++ /dev/null @@ -1,36 +0,0 @@ -import glob from 'glob'; -import babel from 'rollup-plugin-babel'; -import resolve from '@rollup/plugin-node-resolve'; -import { version } from './package.json'; - -const name = 'beautiful-react-hooks'; -const banner = `/* ${name} version: ${version} */`; - -const standardOpts = { - name, banner, exports: 'named', minifyInternalExports: true, preserveModules: true, -}; - -// CommonJS (for Node) and ES module (for bundlers) build. -// (We could have three entries in the configuration array -// instead of two, but it's quicker to generate multiple -// builds from a single configuration where possible, using -// an array for the `output` option, where we can specify -// `file` and `format` for each target) -const config = [{ - input: glob.sync('./src/**/*.js'), - strictDeprecations: true, - output: [ - { ...standardOpts, dir: 'dist', format: 'cjs' }, - { ...standardOpts, dir: 'dist/esm', format: 'esm' }, - ], - external: ['react', 'react-dom', 'lodash.debounce', 'lodash.throttle'], - plugins: [ - resolve(), - babel({ - comments: false, - presets: ['@babel/preset-env'], - }), - ], -}]; - -export default config; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 57fa4f05..00000000 --- a/src/index.js +++ /dev/null @@ -1,40 +0,0 @@ -export { default as useDidMount } from './useDidMount'; -export { default as useWillUnmount } from './useWillUnmount'; -export { default as useLifecycle } from './useLifecycle'; -export { default as useWindowResize } from './useWindowResize'; -export { default as useWindowScroll } from './useWindowScroll'; -export { default as useDebouncedFn } from './useDebouncedFn'; -export { default as useThrottledFn } from './useThrottledFn'; -export { default as useMouse } from './useMouse'; -export { default as useMouseEvents } from './useMouseEvents'; -export { default as useMouseState } from './useMouseState'; -export { default as useTimeout } from './useTimeout'; -export { default as useInterval } from './useInterval'; -export { default as useGlobalEvent } from './useGlobalEvent'; -export { default as usePreviousValue } from './usePreviousValue'; -export { default as useGeolocation } from './useGeolocation'; -export { default as useGeolocationEvents } from './useGeolocationEvents'; -export { default as useGeolocationState } from './useGeolocationState'; -export { default as useMediaQuery } from './useMediaQuery'; -export { default as useValueHistory } from './useValueHistory'; -export { default as useOnlineState } from './useOnlineState'; -export { default as useViewportSpy } from './useViewportSpy'; -export { default as useValidatedState } from './useValidatedState'; -export { default as useDragEvents } from './useDragEvents'; -export { default as useDrag } from './useDrag'; -export { default as useDropZone } from './useDropZone'; -export { default as useRequestAnimationFrame } from './useRequestAnimationFrame'; -export { default as useLocalStorage } from './useLocalStorage'; -export { default as useSessionStorage } from './useSessionStorage'; -export { default as useStorage } from './useStorage'; -export { default as useResizeObserver } from './useResizeObserver'; -export { default as useDefaultedState } from './useDefaultedState'; -export { default as useObservable } from './useObservable'; -export { default as useSpeechSynthesis } from './useSpeechSynthesis'; -export { default as useSystemVoices } from './useSystemVoices'; -export { default as useRenderInfo } from './useRenderInfo'; -export { default as useSwipe } from './useSwipe'; -export { default as useHorizontalSwipe } from './useHorizontalSwipe'; -export { default as useVerticalSwipe } from './useVerticalSwipe'; -export { default as useSwipeEvents } from './useSwipeEvents'; -export { default as useConditionalTimeout } from './useConditionalTimeout'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..d918a062 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,40 @@ +export { default as useDidMount } from './useDidMount' +export { default as useWillUnmount } from './useWillUnmount' +export { default as useLifecycle } from './useLifecycle' +export { default as useWindowResize } from './useWindowResize' +export { default as useWindowScroll } from './useWindowScroll' +export { default as useDebouncedFn } from './useDebouncedFn' +export { default as useThrottledFn } from './useThrottledFn' +export { default as useMouse } from './useMouse' +export { default as useMouseEvents } from './useMouseEvents' +export { default as useMouseState } from './useMouseState' +export { default as useTimeout } from './useTimeout' +export { default as useInterval } from './useInterval' +export { default as useGlobalEvent } from './useGlobalEvent' +export { default as usePreviousValue } from './usePreviousValue' +export { default as useGeolocation } from './useGeolocation' +export { default as useGeolocationEvents } from './useGeolocationEvents' +export { default as useGeolocationState } from './useGeolocationState' +export { default as useMediaQuery } from './useMediaQuery' +export { default as useValueHistory } from './useValueHistory' +export { default as useOnlineState } from './useOnlineState' +export { default as useViewportSpy } from './useViewportSpy' +export { default as useValidatedState } from './useValidatedState' +export { default as useDragEvents } from './useDragEvents' +export { default as useDrag } from './useDrag' +export { default as useDropZone } from './useDropZone' +export { default as useRequestAnimationFrame } from './useRequestAnimationFrame' +export { default as useLocalStorage } from './useLocalStorage' +export { default as useSessionStorage } from './useSessionStorage' +export { default as useStorage } from './shared/createStorageHook' +export { default as useResizeObserver } from './useResizeObserver' +export { default as useDefaultedState } from './useDefaultedState' +export { default as useObservable } from './useObservable' +export { default as useSpeechSynthesis } from './useSpeechSynthesis' +export { default as useSystemVoices } from './useSystemVoices' +export { default as useRenderInfo } from './useRenderInfo' +export { default as useSwipe } from './useSwipe' +export { default as useHorizontalSwipe } from './useHorizontalSwipe' +export { default as useVerticalSwipe } from './useVerticalSwipe' +export { default as useSwipeEvents } from './useSwipeEvents' +export { default as useConditionalTimeout } from './useConditionalTimeout' diff --git a/src/shared/assignEventOnMount.ts b/src/shared/assignEventOnMount.ts new file mode 100644 index 00000000..c74f6004 --- /dev/null +++ b/src/shared/assignEventOnMount.ts @@ -0,0 +1,33 @@ +import { MutableRefObject, useEffect } from 'react' + +const assignEventOnMount = + (targetRef: MutableRefObject, handler: MutableRefObject<(e: E) => unknown>, eventName: string) => { + useEffect(() => { + const cb = (mouseEvent: E) => { + if (handler && handler.current) { + handler.current(mouseEvent) + } + } + let target: T | Document + + if (targetRef !== null && !!targetRef.current) { + target = targetRef.current + } + + if (targetRef === null) { + target = document + } + + if (target && target.addEventListener) { + target.addEventListener(eventName, cb) + } + + return () => { + if (target && target.removeEventListener) { + target.removeEventListener(eventName, cb) + } + } + }, []) +} + +export default assignEventOnMount diff --git a/src/shared/createCbSetterErrorProxy.ts b/src/shared/createCbSetterErrorProxy.ts new file mode 100644 index 00000000..ca8bd56a --- /dev/null +++ b/src/shared/createCbSetterErrorProxy.ts @@ -0,0 +1,16 @@ +/** + * Create setter error proxy + */ +const createCbSetterErrorProxy = (errorMessage: string) => new Proxy(Object.create(null), { + get: (target, property) => { + if (property && typeof property === 'string' && property.slice(0, 2) === 'on') { + return () => { + throw new Error(errorMessage) + } + } + + return { error: errorMessage } + }, +}) + +export default createCbSetterErrorProxy diff --git a/src/shared/createStorageHook.ts b/src/shared/createStorageHook.ts new file mode 100644 index 00000000..4dbb3aca --- /dev/null +++ b/src/shared/createStorageHook.ts @@ -0,0 +1,43 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import safelyParseJson from './safelyParseJson' +import isClient from './isClient' +import isAPISupported from './isAPISupported' +import isDevelopment from './isDevelopment' + +/** + * An utility to quickly create hooks to access both Session Storage and Local Storage + */ +const createStorageHook = (type: 'session' | 'local') => { + const storageName = `${type}Storage` + + if (isClient && !isAPISupported(storageName)) { + // eslint-disable-next-line no-console + console.warn(`${storageName} is not supported`) + } + + /** + * the hook + */ + return (storageKey: string, defaultValue?: any): [T, Dispatch>] => { + if (!isClient) { + if (isDevelopment) { + // eslint-disable-next-line no-console + console.warn(`Please be aware that ${storageName} could not be available during SSR`) + } + return [JSON.stringify(defaultValue) as unknown as T, () => undefined] + } + + const storage = (window as any)[storageName] + const [value, setValue] = useState( + safelyParseJson(storage.getItem(storageKey) || JSON.stringify(defaultValue)), + ) + + useEffect(() => { + storage.setItem(storageKey, JSON.stringify(value)) + }, [storageKey, value]) + + return [value, setValue] + } +} + +export default createStorageHook diff --git a/src/shared/geolocationStandardOptions.ts b/src/shared/geolocationStandardOptions.ts new file mode 100644 index 00000000..49f10ef6 --- /dev/null +++ b/src/shared/geolocationStandardOptions.ts @@ -0,0 +1,7 @@ +const geoStandardOptions: PositionOptions = Object.freeze({ + enableHighAccuracy: false, + timeout: 0xFFFFFFFF, + maximumAge: 0, +}) + +export default geoStandardOptions diff --git a/src/shared/isAPISupported.ts b/src/shared/isAPISupported.ts new file mode 100644 index 00000000..4660ded1 --- /dev/null +++ b/src/shared/isAPISupported.ts @@ -0,0 +1,6 @@ +/** + * Exports a boolean value reporting whether the given API is supported or not + */ +const isApiSupported = (api: string): boolean => (typeof window !== 'undefined' ? api in window : false) + +export default isApiSupported diff --git a/src/utils/isClient.js b/src/shared/isClient.ts similarity index 61% rename from src/utils/isClient.js rename to src/shared/isClient.ts index 387b00b2..147aa911 100644 --- a/src/utils/isClient.js +++ b/src/shared/isClient.ts @@ -1,6 +1,6 @@ /** * Exports a boolean value reporting whether is client side or server side by checking on the window object */ -const isClient = (typeof window === 'object'); +const isClient = (typeof window === 'object') -export default isClient; +export default isClient diff --git a/src/utils/isDevelopment.js b/src/shared/isDevelopment.ts similarity index 81% rename from src/utils/isDevelopment.js rename to src/shared/isDevelopment.ts index 277f7056..fe702b35 100644 --- a/src/utils/isDevelopment.js +++ b/src/shared/isDevelopment.ts @@ -1,5 +1,5 @@ const isDevelopment = ( typeof process !== 'undefined' && process.env && (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') -); +) -export default isDevelopment; +export default isDevelopment diff --git a/src/utils/isSamePosition.js b/src/shared/isSamePosition.ts similarity index 66% rename from src/utils/isSamePosition.js rename to src/shared/isSamePosition.ts index 2e342fd8..aeb87b19 100644 --- a/src/utils/isSamePosition.js +++ b/src/shared/isSamePosition.ts @@ -1,9 +1,11 @@ /** * Checks if two position are equals */ -const isSamePosition = (current, next) => { - if (!current || !next || !next.coords) return false; - if (current.timestamp && next.timestamp && current.timestamp !== next.timestamp) return false; +import { PositionSummary } from './makePositionObject' + +const isSamePosition = (current: PositionSummary, next: GeolocationPosition): boolean => { + if (!current || !next || !next.coords) return false + if (current.timestamp && next.timestamp && current.timestamp !== next.timestamp) return false return ( (current.coords.latitude === next.coords.latitude) @@ -13,7 +15,7 @@ const isSamePosition = (current, next) => { && (current.coords.altitudeAccuracy === next.coords.altitudeAccuracy) && (current.coords.heading === next.coords.heading) && (current.coords.speed === next.coords.speed) - ); -}; + ) +} -export default isSamePosition; +export default isSamePosition diff --git a/src/shared/makePositionObject.ts b/src/shared/makePositionObject.ts new file mode 100644 index 00000000..926395e3 --- /dev/null +++ b/src/shared/makePositionObject.ts @@ -0,0 +1,30 @@ +export type PositionSummary = { + readonly timestamp: number, + readonly coords: { + readonly latitude: number, + readonly longitude: number, + readonly heading: number | null, + readonly altitude: number | null, + readonly speed: number | null, + readonly accuracy: number, + readonly altitudeAccuracy: number | null, + } +} + +/** + * Given a position object returns only its properties + */ +const makePositionObj = (position: GeolocationPosition): PositionSummary | null => (!position ? null : ({ + timestamp: position.timestamp, + coords: { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + altitude: position.coords.altitude, + accuracy: position.coords.accuracy, + altitudeAccuracy: position.coords.altitudeAccuracy, + heading: position.coords.heading, + speed: position.coords.speed, + }, +})) + +export default makePositionObj diff --git a/src/shared/safeHasOwnProperty.ts b/src/shared/safeHasOwnProperty.ts new file mode 100644 index 00000000..4f6673de --- /dev/null +++ b/src/shared/safeHasOwnProperty.ts @@ -0,0 +1,3 @@ +const safeHasOwnProperty = (obj: any, prop: string): boolean => (obj ? Object.prototype.hasOwnProperty.call(obj, prop) : false) + +export default safeHasOwnProperty diff --git a/src/shared/safelyParseJson.ts b/src/shared/safelyParseJson.ts new file mode 100644 index 00000000..12794b7e --- /dev/null +++ b/src/shared/safelyParseJson.ts @@ -0,0 +1,9 @@ +const safelyParseJson = (parseString: string): T => { + try { + return JSON.parse(parseString) + } catch (e) { + return null + } +} + +export default safelyParseJson diff --git a/src/shared/swipeUtils.ts b/src/shared/swipeUtils.ts new file mode 100644 index 00000000..f155830e --- /dev/null +++ b/src/shared/swipeUtils.ts @@ -0,0 +1,31 @@ +/** + * Takes a mouse or a touch events and returns clientX and clientY values + * @param event + * @return {[undefined, undefined]} + */ +export const getPointerCoordinates = (event: TouchEvent | MouseEvent): [number, number] => { + if ((event as TouchEvent).touches) { + const { clientX, clientY } = (event as TouchEvent).touches[0] + return [clientX, clientY] + } + + const { clientX, clientY } = event as MouseEvent + + return [clientX, clientY] +} + +export const getHorizontalDirection = (alpha: number) => (alpha < 0 ? 'right' : 'left') + +export const getVerticalDirection = (alpha: number) => (alpha < 0 ? 'down' : 'up') + +export type Direction = 'right' | 'left' | 'down' | 'up' + +export const getDirection = (currentPoint: [number, number], startingPoint: [number, number], alpha: [number, number]): Direction => { + const alphaX = startingPoint[0] - currentPoint[0] + const alphaY = startingPoint[1] - currentPoint[1] + if (Math.abs(alphaX) > Math.abs(alphaY)) { + return getHorizontalDirection(alpha[0]) + } + + return getVerticalDirection(alpha[1]) +} diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 00000000..84c2f796 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,11 @@ +export type CallbackSetter = (handler: T) => void; + +export type Noop = () => void; + +export interface DebouncedFunc any> { + (...args: Parameters): ReturnType | undefined; + + cancel(): void; + + flush(): ReturnType | undefined; +} diff --git a/src/shared/useHandlerSetterRef.ts b/src/shared/useHandlerSetterRef.ts new file mode 100644 index 00000000..6618e3f9 --- /dev/null +++ b/src/shared/useHandlerSetterRef.ts @@ -0,0 +1,33 @@ +import { MutableRefObject, useCallback, useRef } from 'react' +import { CallbackSetter } from './types' + +type HandlerPair = readonly [ + handler: MutableRefObject, + setHandler: CallbackSetter, +] + +/** + * Returns an object where the first item is the [ref](https://reactjs.org/docs/hooks-reference.html#useref) to a + * callback function and the second one is setter for that function.

+ * + * Although it function looks quite similar to the [useState](https://reactjs.org/docs/hooks-reference.html#usestate), + * hook, in this case the setter just makes sure the given callback is indeed a new function.

+ * **Setting a callback ref does not force your component to re-render.**

+ * + * `useHandlerSetter` is useful when abstracting other hooks to possibly implement handlers setters. + */ +const useHandlerSetterRef = any>(handler?: T): HandlerPair => { + const handlerRef: MutableRefObject = useRef(handler) + + const setHandler: CallbackSetter = useCallback((nextCallback: T): void => { + if (typeof nextCallback !== 'function') { + throw new Error('the argument supplied to the \'setHandler\' function should be of type function') + } + + handlerRef.current = nextCallback + }, [handlerRef]) + + return [handlerRef, setHandler] +} + +export default useHandlerSetterRef diff --git a/src/useConditionalTimeout.js b/src/useConditionalTimeout.ts similarity index 51% rename from src/useConditionalTimeout.js rename to src/useConditionalTimeout.ts index 8ca9ad26..2535555b 100644 --- a/src/useConditionalTimeout.js +++ b/src/useConditionalTimeout.ts @@ -1,61 +1,67 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import usePreviousValue from './usePreviousValue'; +import { useCallback, useEffect, useRef, useState } from 'react' +import usePreviousValue from './usePreviousValue' -const defaultOptions = { +type UseConditionalTimeoutOptions = { + cancelOnUnmount?: boolean, + cancelOnConditionChange?: boolean +} + +const defaultOptions: UseConditionalTimeoutOptions = { cancelOnUnmount: true, cancelOnConditionChange: true, -}; +} /** * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then delays the * execution of the given function by the defined time from when the condition verifies. */ -const useConditionalTimeout = (fn, milliseconds, condition, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}) }; - const timeout = useRef(); - const callback = useRef(fn); - const [isCleared, setIsCleared] = useState(false); - const prevCondition = usePreviousValue(condition); +const useConditionalTimeout = any> + (fn: T, milliseconds: number, condition: boolean, options: UseConditionalTimeoutOptions = defaultOptions): [boolean, () => void] => { + const opts = { ...defaultOptions, ...(options || {}) } + const timeout = useRef() + const callback = useRef(fn) + const [isCleared, setIsCleared] = useState(false) + const prevCondition = usePreviousValue(condition) // the clear method const clear = useCallback(() => { if (timeout.current) { - clearTimeout(timeout.current); - setIsCleared(true); + clearTimeout(timeout.current) + setIsCleared(true) } - }, []); + }, []) // if the provided function changes, change its reference useEffect(() => { if (typeof fn === 'function') { - callback.current = fn; + callback.current = fn } - }, [fn]); + }, [fn]) // when the milliseconds change, reset the timeout useEffect(() => { if (condition && typeof milliseconds === 'number') { timeout.current = setTimeout(() => { - callback.current(); - }, milliseconds); + callback.current() + }, milliseconds) } - }, [condition, milliseconds]); + }, [condition, milliseconds]) // when the condition change, clear the timeout useEffect(() => { if (prevCondition && condition !== prevCondition && opts.cancelOnConditionChange) { - clear(); + clear() } - }, [condition, options]); + }, [condition, options]) // when component unmount clear the timeout useEffect(() => () => { if (opts.cancelOnUnmount) { - clear(); + clear() } - }, []); + }, []) - return [isCleared, clear]; -}; + return [isCleared, clear] +} -export default useConditionalTimeout; +export default useConditionalTimeout diff --git a/src/useDebouncedFn.js b/src/useDebouncedFn.js deleted file mode 100644 index 303ec6f1..00000000 --- a/src/useDebouncedFn.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useCallback } from 'react'; -import debounce from 'lodash.debounce'; - -const defaultOptions = { - leading: false, - trailing: true, -}; - -/** - * Accepts a function and returns a new debounced yet memoized version of that same function that delays - * its invoking by the defined time. - * If time is not defined, its default value will be 100ms. - */ -const useDebouncedFn = (fn, wait = 100, options = defaultOptions, dependencies) => { - const debounced = debounce(fn, wait, options); - - return useCallback(debounced, dependencies); -}; - -export default useDebouncedFn; diff --git a/src/useDebouncedFn.ts b/src/useDebouncedFn.ts new file mode 100644 index 00000000..656049ce --- /dev/null +++ b/src/useDebouncedFn.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react' +import debounce from 'lodash.debounce' +import { DebouncedFunc } from './shared/types' + +export type DebounceOptions = { + leading?: boolean | undefined; + maxWait?: number | undefined; + trailing?: boolean | undefined; +} + +const defaultOptions: DebounceOptions = { + leading: false, + trailing: true, +} + +/** + * Accepts a function and returns a new debounced yet memoized version of that same function that delays + * its invoking by the defined time. + * If time is not defined, its default value will be 100ms. + */ +const useDebouncedFn = any> + (fn: T, wait: number = 100, options: DebounceOptions = defaultOptions, dependencies?: any[]): DebouncedFunc => { + const debounced = debounce(fn, wait, options) + + return useCallback(debounced, dependencies) +} + +export default useDebouncedFn diff --git a/src/useDefaultedState.js b/src/useDefaultedState.js deleted file mode 100644 index 0c88a2cd..00000000 --- a/src/useDefaultedState.js +++ /dev/null @@ -1,16 +0,0 @@ -import { useState } from 'react'; - -const maybeState = (state, defaultValue) => (state ?? defaultValue); - -/** - * Returns a safe state by making sure the given value is not null or undefined - */ -const useDefaultedState = (defaultValue, initialState) => { - const [state, setState] = useState(maybeState(initialState, defaultValue)); - - const setStateSafe = (nextState) => setState(maybeState(nextState, defaultValue)); - - return [maybeState(state, defaultValue), setStateSafe]; -}; - -export default useDefaultedState; diff --git a/src/useDefaultedState.ts b/src/useDefaultedState.ts new file mode 100644 index 00000000..61a02a48 --- /dev/null +++ b/src/useDefaultedState.ts @@ -0,0 +1,18 @@ +import { Dispatch, SetStateAction, useCallback, useState } from 'react' + +const maybeState = (state: T, defaultValue?: T) => (state ?? defaultValue) + +/** + * Returns a safe state by making sure the given value is not null or undefined + */ +const useDefaultedState = (defaultValue: T, initialState?: T): [T, Dispatch>] => { + const [state, setState] = useState(maybeState(initialState, defaultValue)) + + const setStateSafe = useCallback((nextState: T) => { + setState(maybeState(nextState, defaultValue)) + }, [setState]) + + return [maybeState(state, defaultValue), setStateSafe] +} + +export default useDefaultedState diff --git a/src/useDidMount.js b/src/useDidMount.js deleted file mode 100644 index 566ed96b..00000000 --- a/src/useDidMount.js +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; - -/** - * Returns a callback setter for a function to be performed when the component did mount. - */ -const useDidMount = (handler) => { - const [onMountHandler, setOnMountHandler] = createHandlerSetter(handler); - - useEffect(() => { - if (onMountHandler.current) { - onMountHandler.current(); - } - }, []); - - return setOnMountHandler; -}; - -export default useDidMount; diff --git a/src/useDidMount.ts b/src/useDidMount.ts new file mode 100644 index 00000000..00fdd3fb --- /dev/null +++ b/src/useDidMount.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import { CallbackSetter, Noop } from './shared/types' + +/** + * Returns a callback setter for a function to be performed when the component did mount. + */ +const useDidMount = any = Noop>(callback?: T): CallbackSetter => { + const [handler, setHandler] = useHandlerSetterRef(callback) + + useEffect(() => { + if (handler.current) { + handler.current() + } + }, []) + + return setHandler +} + +export default useDidMount diff --git a/src/useDrag.js b/src/useDrag.js deleted file mode 100644 index 9a410ff9..00000000 --- a/src/useDrag.js +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from 'react'; -import useDragEvents from './useDragEvents'; - -const defaultOptions = { - dragImage: null, - dragImageXOffset: 0, - dragImageYOffset: 0, - transfer: null, - transferFormat: 'text', -}; - -const useDrag = (targetRef, options = defaultOptions) => { - const { onDragStart, onDragEnd } = useDragEvents(targetRef, true); - const [isDragging, setIsDragging] = useState(false); - const opts = { ...defaultOptions, ...(options || {}) }; - - onDragStart((event) => { - setIsDragging(true); - - if (opts.dragImage) { - const img = new Image(); - img.src = opts.dragImage; - event.dataTransfer.setDragImage(img, opts.dragImageXOffset, opts.dragImageYOffset); - } - - if (opts.transfer) { - const data = typeof opts.transfer === 'object' ? JSON.stringify(opts.transfer) : `${opts.transfer}`; - event.dataTransfer.setData(opts.transferFormat, data); - } - }); - - onDragEnd(() => setIsDragging(false)); - - return isDragging; -}; - -export default useDrag; diff --git a/src/useDrag.ts b/src/useDrag.ts new file mode 100644 index 00000000..024d6185 --- /dev/null +++ b/src/useDrag.ts @@ -0,0 +1,43 @@ +import { MutableRefObject, useState } from 'react' +import useDragEvents from './useDragEvents' + +export type UseDragOptions = { + dragImage?: string, + dragImageXOffset?: number, + dragImageYOffset?: number, + transfer?: string | number | Record, + transferFormat?: string, +} + +const defaultOptions: UseDragOptions = { + dragImageXOffset: 0, + dragImageYOffset: 0, + transferFormat: 'text', +} + +const useDrag = (targetRef: MutableRefObject, options = defaultOptions): boolean => { + const { onDragStart, onDragEnd } = useDragEvents(targetRef, true) + const [isDragging, setIsDragging] = useState(false) + const opts: UseDragOptions = { ...defaultOptions, ...(options || {}) } + + onDragStart((event: DragEvent) => { + setIsDragging(true) + + if (opts.dragImage) { + const img = new Image() + img.src = opts.dragImage + event.dataTransfer.setDragImage(img, opts.dragImageXOffset, opts.dragImageYOffset) + } + + if (opts.transfer) { + const data = typeof opts.transfer === 'object' ? JSON.stringify(opts.transfer) : `${opts.transfer}` + event.dataTransfer.setData(opts.transferFormat, data) + } + }) + + onDragEnd(() => setIsDragging(false)) + + return isDragging +} + +export default useDrag diff --git a/src/useDragEvents.js b/src/useDragEvents.js deleted file mode 100644 index 79d0ce2f..00000000 --- a/src/useDragEvents.js +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect } from 'react'; -import hasOwnProperty from './utils/hasOwnProperty'; -import createCbSetterErrorProxy from './utils/createCbSetterErrorProxy'; -import createHandlerSetter from './utils/createHandlerSetter'; - -const assignDragEventOnMount = (targetRef, handlerRef, eventName) => { - useEffect(() => { - const cb = (dragEvent) => { - if (handlerRef.current) { - handlerRef.current(dragEvent); - } - }; - - if (targetRef.current) { - targetRef.current.addEventListener(eventName, cb); - } - - return () => { - if (targetRef.current) { - targetRef.current.removeEventListener(eventName, cb); - } - }; - }, []); -}; - -/** - * Returns an object of callback setters to handle the drag-related events. - * It accepts a DOM ref representing the events target (where attach the events to). - * - * Returned callback setters: `onDrag`, `onDrop`, `onDragEnter`, `onDragEnd`, `onDragExit`, `onDragLeave`, - * `onDragOver`, `onDragStart`; - */ -const useDragEvents = (targetRef, setDraggable = true) => { - const [onDrag, setOnDrag] = createHandlerSetter(); - const [onDrop, setOnDrop] = createHandlerSetter(); - const [onDragEnter, setOnDragEnter] = createHandlerSetter(); - const [onDragEnd, setOnDragEnd] = createHandlerSetter(); - const [onDragExit, setOnDragExit] = createHandlerSetter(); - const [onDragLeave, setOnDragLeave] = createHandlerSetter(); - const [onDragOver, setOnDragOver] = createHandlerSetter(); - const [onDragStart, setOnDragStart] = createHandlerSetter(); - - if (targetRef !== null && !hasOwnProperty(targetRef, 'current')) { - return createCbSetterErrorProxy('Unable to assign any drag event to the given ref'); - } - - useEffect(() => { - if (setDraggable && targetRef.current && !targetRef.current.hasAttribute('draggable')) { - targetRef.current.setAttribute('draggable', true); - } - }, []); - - assignDragEventOnMount(targetRef, onDrag, 'drag'); - assignDragEventOnMount(targetRef, onDrop, 'drop'); - assignDragEventOnMount(targetRef, onDragEnter, 'dragenter'); - assignDragEventOnMount(targetRef, onDragEnd, 'dragend'); - assignDragEventOnMount(targetRef, onDragExit, 'dragexit'); - assignDragEventOnMount(targetRef, onDragLeave, 'dragleave'); - assignDragEventOnMount(targetRef, onDragOver, 'dragover'); - assignDragEventOnMount(targetRef, onDragStart, 'dragstart'); - - return Object.freeze({ - onDrag: setOnDrag, - onDrop: setOnDrop, - onDragEnter: setOnDragEnter, - onDragEnd: setOnDragEnd, - onDragExit: setOnDragExit, - onDragLeave: setOnDragLeave, - onDragOver: setOnDragOver, - onDragStart: setOnDragStart, - }); -}; - -export default useDragEvents; diff --git a/src/useDragEvents.ts b/src/useDragEvents.ts new file mode 100644 index 00000000..7f99eba0 --- /dev/null +++ b/src/useDragEvents.ts @@ -0,0 +1,89 @@ +import { MutableRefObject, useEffect } from 'react' +import safeHasOwnProperty from './shared/safeHasOwnProperty' +import createCbSetterErrorProxy from './shared/createCbSetterErrorProxy' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import { CallbackSetter } from './shared/types' + +const assignDragEventOnMount = + (targetRef: MutableRefObject, handlerRef: MutableRefObject>, eventName: string) => { + useEffect(() => { + const cb = (dragEvent: DragEvent) => { + if (handlerRef && handlerRef.current) { + handlerRef.current(dragEvent) + } + } + + if (targetRef.current) { + targetRef.current.addEventListener(eventName, cb) + } + + return () => { + if (targetRef.current) { + targetRef.current.removeEventListener(eventName, cb) + } + } + }, []) +} + +type DragEventCallback = (event: DragEvent) => any; + +type DragEventsMap = { + readonly onDrag: CallbackSetter, + readonly onDrop: CallbackSetter, + readonly onDragEnter: CallbackSetter, + readonly onDragEnd: CallbackSetter, + readonly onDragExit: CallbackSetter, + readonly onDragLeave: CallbackSetter, + readonly onDragOver: CallbackSetter, + readonly onDragStart: CallbackSetter, +} + +/** + * Returns an object of callback setters to handle the drag-related events. + * It accepts a DOM ref representing the events target (where attach the events to). + * + * Returned callback setters: `onDrag`, `onDrop`, `onDragEnter`, `onDragEnd`, `onDragExit`, `onDragLeave`, + * `onDragOver`, `onDragStart`; + */ +const useDragEvents = (targetRef: MutableRefObject, setDraggable: boolean = true): DragEventsMap => { + const [onDrag, setOnDrag] = useHandlerSetterRef() + const [onDrop, setOnDrop] = useHandlerSetterRef() + const [onDragEnter, setOnDragEnter] = useHandlerSetterRef() + const [onDragEnd, setOnDragEnd] = useHandlerSetterRef() + const [onDragExit, setOnDragExit] = useHandlerSetterRef() + const [onDragLeave, setOnDragLeave] = useHandlerSetterRef() + const [onDragOver, setOnDragOver] = useHandlerSetterRef() + const [onDragStart, setOnDragStart] = useHandlerSetterRef() + + if (targetRef !== null && !safeHasOwnProperty(targetRef, 'current')) { + return createCbSetterErrorProxy('Unable to assign any drag event to the given ref') + } + + useEffect(() => { + if (setDraggable && targetRef.current && !targetRef.current.hasAttribute('draggable')) { + targetRef.current.setAttribute('draggable', String(true)) + } + }, []) + + assignDragEventOnMount(targetRef, onDrag, 'drag') + assignDragEventOnMount(targetRef, onDrop, 'drop') + assignDragEventOnMount(targetRef, onDragEnter, 'dragenter') + assignDragEventOnMount(targetRef, onDragEnd, 'dragend') + assignDragEventOnMount(targetRef, onDragExit, 'dragexit') + assignDragEventOnMount(targetRef, onDragLeave, 'dragleave') + assignDragEventOnMount(targetRef, onDragOver, 'dragover') + assignDragEventOnMount(targetRef, onDragStart, 'dragstart') + + return Object.freeze({ + onDrag: setOnDrag, + onDrop: setOnDrop, + onDragEnter: setOnDragEnter, + onDragEnd: setOnDragEnd, + onDragExit: setOnDragExit, + onDragLeave: setOnDragLeave, + onDragOver: setOnDragOver, + onDragStart: setOnDragStart, + }) +} + +export default useDragEvents diff --git a/src/useDropZone.js b/src/useDropZone.js deleted file mode 100644 index 6a1e01b3..00000000 --- a/src/useDropZone.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useState } from 'react'; -import useDragEvents from './useDragEvents'; - -const useDropZone = (targetRef) => { - const { onDrop, onDragOver, onDragLeave } = useDragEvents(targetRef, false); - const [isOver, setIsOver] = useState(false); - - onDragOver((event) => { - event.preventDefault(); - setIsOver(true); - }); - - onDragLeave(() => { - setIsOver(false); - }); - - return { isOver, onDrop }; -}; - -export default useDropZone; diff --git a/src/useDropZone.ts b/src/useDropZone.ts new file mode 100644 index 00000000..c4887a97 --- /dev/null +++ b/src/useDropZone.ts @@ -0,0 +1,23 @@ +import { MutableRefObject, useState } from 'react' +import useDragEvents from './useDragEvents' +import { CallbackSetter } from './shared/types' + +export type DropZoneState = { isOver: boolean, onDrop: CallbackSetter<(event: DragEvent) => any> } + +const useDropZone = (targetRef: MutableRefObject): DropZoneState => { + const { onDrop, onDragOver, onDragLeave } = useDragEvents(targetRef, false) + const [isOver, setIsOver] = useState(false) + + onDragOver((event: DragEvent) => { + event.preventDefault() + setIsOver(true) + }) + + onDragLeave(() => { + setIsOver(false) + }) + + return { isOver, onDrop } +} + +export default useDropZone diff --git a/src/useGeolocation.js b/src/useGeolocation.js deleted file mode 100644 index cdf21c24..00000000 --- a/src/useGeolocation.js +++ /dev/null @@ -1,17 +0,0 @@ -import useGeolocationState from './useGeolocationState'; -import useGeolocationEvents from './useGeolocationEvents'; -import geolocationStandardOptions from './utils/geolocationStandardOptions'; - -/** - * Returns an array where the first item is the geolocation state from the `useGeolocationState` hook and the - * second one is the object of callback setters from the `useGeolocationEvents` hook. - * It is intended as a shortcut to those hooks. - */ -const useGeolocation = (options = geolocationStandardOptions) => { - const state = useGeolocationState(options); - const events = useGeolocationEvents(options); - - return [state, events]; -}; - -export default useGeolocation; diff --git a/src/useGeolocation.ts b/src/useGeolocation.ts new file mode 100644 index 00000000..f36bfb5e --- /dev/null +++ b/src/useGeolocation.ts @@ -0,0 +1,17 @@ +import useGeolocationState, { GeolocationState } from './useGeolocationState' +import useGeolocationEvents, { GeolocationEventsMap } from './useGeolocationEvents' +import geolocationStandardOptions from './shared/geolocationStandardOptions' + +/** + * Returns an array where the first item is the geolocation state from the `useGeolocationState` hook and the + * second one is the object of callback setters from the `useGeolocationEvents` hook. + * It is intended as a shortcut to those hooks. + */ +const useGeolocation = (options: PositionOptions = geolocationStandardOptions): [GeolocationState, GeolocationEventsMap] => { + const state = useGeolocationState(options) + const events = useGeolocationEvents(options) + + return [state, events] +} + +export default useGeolocation diff --git a/src/useGeolocationEvents.js b/src/useGeolocationEvents.js deleted file mode 100644 index f352c4a9..00000000 --- a/src/useGeolocationEvents.js +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect, useRef } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; -import createCbSetterErrorProxy from './utils/createCbSetterErrorProxy'; -import geolocationStandardOptions from './utils/geolocationStandardOptions'; - -/** - * Returns a frozen object of callback setters to handle the geolocation events.
- * So far, the supported methods are: `onChange`, invoked when the position changes and `onError`, invoked when - * an error occur while retrieving the position.
- * The returned object also contains the `isSupported` boolean flag reporting whether the geolocation API is supported. - */ -const useGeolocationEvents = (options = geolocationStandardOptions) => { - const watchId = useRef(); - const [onChangeRef, setOnChangeRef] = createHandlerSetter(); - const [onErrorRef, setOnErrorRef] = createHandlerSetter(); - const isSupported = typeof window !== 'undefined' && 'geolocation' in window.navigator; - - useEffect(() => { - const onSuccess = (successEvent) => { - if (onChangeRef.current) { - onChangeRef.current(successEvent); - } - }; - const onError = (err) => { - if (onErrorRef.current) { - onErrorRef.current(err); - } - }; - - if (isSupported) { - watchId.current = window.navigator.geolocation.watchPosition(onSuccess, onError, options); - } - - return () => { - if (isSupported) { - window.navigator.geolocation.clearWatch(watchId.current); - } - }; - }, []); - - return !isSupported ? createCbSetterErrorProxy('The Geolocation API is not supported') : Object.freeze({ - isSupported, - onChange: setOnChangeRef, - onError: setOnErrorRef, - }); -}; - -export default useGeolocationEvents; diff --git a/src/useGeolocationEvents.ts b/src/useGeolocationEvents.ts new file mode 100644 index 00000000..e280a0b8 --- /dev/null +++ b/src/useGeolocationEvents.ts @@ -0,0 +1,55 @@ +import { useEffect, useRef } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import createCbSetterErrorProxy from './shared/createCbSetterErrorProxy' +import geolocationStandardOptions from './shared/geolocationStandardOptions' +import { CallbackSetter } from './shared/types' + +export type GeolocationEventsMap = { + readonly isSupported: boolean, + readonly onChange: CallbackSetter + readonly onError: CallbackSetter +} + +/** + * Returns a frozen object of callback setters to handle the geolocation events.
+ * So far, the supported methods are: `onChange`, invoked when the position changes and `onError`, invoked when + * an error occur while retrieving the position.
+ * The returned object also contains the `isSupported` boolean flag reporting whether the geolocation API is supported. + */ +const useGeolocationEvents = (options: PositionOptions = geolocationStandardOptions): GeolocationEventsMap => { + const watchId = useRef() + const [onChangeRef, setOnChangeRef] = useHandlerSetterRef() + const [onErrorRef, setOnErrorRef] = useHandlerSetterRef() + const isSupported: boolean = typeof window !== 'undefined' && 'geolocation' in window.navigator + + useEffect(() => { + const onSuccess = (successEvent: GeolocationPosition) => { + if (onChangeRef.current) { + onChangeRef.current(successEvent) + } + } + const onError = (err: GeolocationPositionError) => { + if (onErrorRef.current) { + onErrorRef.current(err) + } + } + + if (isSupported) { + watchId.current = window.navigator.geolocation.watchPosition(onSuccess, onError, options) + } + + return () => { + if (isSupported) { + window.navigator.geolocation.clearWatch(watchId.current) + } + } + }, []) + + return !isSupported ? createCbSetterErrorProxy('The Geolocation API is not supported') : Object.freeze({ + isSupported, + onChange: setOnChangeRef, + onError: setOnErrorRef, + }) +} + +export default useGeolocationEvents diff --git a/src/useGeolocationState.js b/src/useGeolocationState.js deleted file mode 100644 index 31fd2678..00000000 --- a/src/useGeolocationState.js +++ /dev/null @@ -1,45 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import useGeolocationEvents from './useGeolocationEvents'; -import geolocationStandardOptions from './utils/geolocationStandardOptions'; -import makePositionObj from './utils/makePositionObject'; -import isSamePosition from './utils/isSamePosition'; - -/** - * Returns a frozen object containing the `position` object, the `isSupported` boolean flag, reporting whether the - * geolocation API is supported or not and the `isRetrieving` boolean flag reporting whether the hook is fetching the - * current position. - * The position is retrieved by using the - * [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API/Using_the_Geolocation_API), - * when supported.

- * It possibly accepts an object of [geolocation options] - * (https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) to be used as parameter when using the - * `Geolocation.getCurrentPosition()` method. - */ -const useGeolocationState = (options = geolocationStandardOptions) => { - const [position, setPosition] = useState(null); - const [isRetrieving, setRetrieving] = useState(false); - const { isSupported, onChange } = useGeolocationEvents(options); - - const savePosition = useCallback(() => { - if (position === null) { - setRetrieving(true); - navigator.geolocation.getCurrentPosition((nextPosition) => { - if (!isSamePosition(position, nextPosition)) { - setPosition(makePositionObj(nextPosition)); - setRetrieving(false); - } - }); - } - }, [position]); - - useEffect(savePosition, [position]); - onChange(savePosition); - - return Object.freeze({ - isSupported, - isRetrieving, - position, - }); -}; - -export default useGeolocationState; diff --git a/src/useGeolocationState.ts b/src/useGeolocationState.ts new file mode 100644 index 00000000..f956b0fc --- /dev/null +++ b/src/useGeolocationState.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useState } from 'react' +import useGeolocationEvents from './useGeolocationEvents' +import geolocationStandardOptions from './shared/geolocationStandardOptions' +import makePositionObj, { PositionSummary } from './shared/makePositionObject' +import isSamePosition from './shared/isSamePosition' + +export type GeolocationState = { + readonly isSupported: boolean, + readonly isRetrieving: boolean, + readonly position: PositionSummary, +} + +/** + * Returns a frozen object containing the `position` object, the `isSupported` boolean flag, reporting whether the + * geolocation API is supported or not and the `isRetrieving` boolean flag reporting whether the hook is fetching the + * current position. + * The position is retrieved by using the + * [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API/Using_the_Geolocation_API), + * when supported.

+ * It possibly accepts an object of [geolocation options] + * (https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) to be used as parameter when using the + * `Geolocation.getCurrentPosition()` method. + */ +const useGeolocationState = (options: PositionOptions = geolocationStandardOptions): GeolocationState => { + const [position, setPosition] = useState(null) + const [isRetrieving, setRetrieving] = useState(false) + const { isSupported, onChange } = useGeolocationEvents(options) + + const savePosition = useCallback(() => { + if (position === null) { + setRetrieving(true) + navigator.geolocation.getCurrentPosition((nextPosition: GeolocationPosition) => { + if (!isSamePosition(position, nextPosition)) { + setPosition(makePositionObj(nextPosition)) + setRetrieving(false) + } + }) + } + }, [position]) + + useEffect(savePosition, [position]) + onChange(savePosition) + + return Object.freeze({ + isSupported, + isRetrieving, + position, + }) +} + +export default useGeolocationState diff --git a/src/useGlobalEvent.js b/src/useGlobalEvent.js deleted file mode 100644 index 6c7a4096..00000000 --- a/src/useGlobalEvent.js +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; - -const defaultOptions = { - capture: false, - once: false, - passive: false, -}; - -/** - * Accepts an event name then returns a callback setter for a function to be performed when the event triggers. - */ -const useGlobalEvent = (eventName, options = defaultOptions, fn) => { - const [callbackRef, setCallbackRef] = createHandlerSetter(fn); - const opts = { ...defaultOptions, ...(options || {}) }; - - useEffect(() => { - const cb = (event) => { - if (callbackRef.current) { - callbackRef.current(event); - } - }; - - if (callbackRef.current && eventName) { - window.addEventListener(eventName, cb, opts); - } - - return () => { - if (eventName) { - window.removeEventListener(eventName, cb, opts); - } - }; - }, [eventName, options]); - - return setCallbackRef; -}; - -export default useGlobalEvent; diff --git a/src/useGlobalEvent.ts b/src/useGlobalEvent.ts new file mode 100644 index 00000000..dba2131f --- /dev/null +++ b/src/useGlobalEvent.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' + +/** + * Accepts an event name then returns a callback setter for a function to be performed when the event triggers. + */ +const useGlobalEvent = (eventName: keyof WindowEventMap, fn?: (event: E) => void, opts?: AddEventListenerOptions) => { + const [handler, setHandler] = useHandlerSetterRef(fn) + + useEffect(() => { + const cb: EventListenerOrEventListenerObject = (event: E) => { + if (handler.current) { + handler.current(event) + } + } + + if (handler && eventName) { + window.addEventListener(eventName, cb, opts) + } + + return () => { + if (eventName) { + window.removeEventListener(eventName, cb, opts) + } + } + }, [eventName, opts]) + + return setHandler +} + +export default useGlobalEvent diff --git a/src/useHorizontalSwipe.js b/src/useHorizontalSwipe.js deleted file mode 100644 index c9215204..00000000 --- a/src/useHorizontalSwipe.js +++ /dev/null @@ -1,20 +0,0 @@ -import useSwipe from './useSwipe'; - -const defaultOptions = { - threshold: 15, - preventDefault: true, -}; - -/** - * A shortcut to useSwipe (with horizontal options) - * @param ref - * @param options - * @return {{alpha: number, count: number, swiping: boolean, direction: null}} - */ -const useHorizontalSwipe = (ref = null, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}), ...{ direction: 'horizontal' } }; - - return useSwipe(ref, opts); -}; - -export default useHorizontalSwipe; diff --git a/src/useHorizontalSwipe.ts b/src/useHorizontalSwipe.ts new file mode 100644 index 00000000..b6492f99 --- /dev/null +++ b/src/useHorizontalSwipe.ts @@ -0,0 +1,18 @@ +import { MutableRefObject } from 'react' +import useSwipe, { UseSwipeOptions } from './useSwipe' + +const defaultOptions: UseSwipeOptions = { + threshold: 15, + preventDefault: true, +} + +/** + * A shortcut to useSwipe (with horizontal options) + */ +const useHorizontalSwipe = (ref: MutableRefObject = null, options: UseSwipeOptions = defaultOptions) => { + const opts: UseSwipeOptions = { ...defaultOptions, ...(options || {}), ...{ direction: 'horizontal' } } + + return useSwipe(ref, opts) +} + +export default useHorizontalSwipe diff --git a/src/useInterval.js b/src/useInterval.js deleted file mode 100644 index 18007af3..00000000 --- a/src/useInterval.js +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; - -const defaultOptions = { - cancelOnUnmount: true, -}; - -/** - * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then repeats the - * execution of the given function by the defined milliseconds. - */ -const useInterval = (fn, milliseconds, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}) }; - const timeout = useRef(); - const callback = useRef(fn); - const [isCleared, setIsCleared] = useState(false); - - // the clear method - const clear = useCallback(() => { - if (timeout.current) { - setIsCleared(true); - clearInterval(timeout.current); - } - }, []); - - // if the provided function changes, change its reference - useEffect(() => { - if (typeof fn === 'function') { - callback.current = fn; - } - }, [fn]); - - // when the milliseconds change, reset the timeout - useEffect(() => { - if (typeof milliseconds === 'number') { - timeout.current = setInterval(() => { - callback.current(); - }, milliseconds); - } - - // cleanup previous interval - return clear; - }, [milliseconds]); - - // when component unmount clear the timeout - useEffect(() => () => { - if (opts.cancelOnUnmount) { - clear(); - } - }, []); - - return [isCleared, clear]; -}; - -export default useInterval; diff --git a/src/useInterval.ts b/src/useInterval.ts new file mode 100644 index 00000000..1be52381 --- /dev/null +++ b/src/useInterval.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +export type UseIntervalOptions = { + cancelOnUnmount?: boolean, +} + +const defaultOptions: UseIntervalOptions = { + cancelOnUnmount: true, +} + +/** + * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then repeats the + * execution of the given function by the defined milliseconds. + */ +const useInterval = any> + (fn: T, milliseconds: number, options: UseIntervalOptions = defaultOptions): [boolean, () => void] => { + const opts = { ...defaultOptions, ...(options || {}) } + const timeout = useRef() + const callback = useRef(fn) + const [isCleared, setIsCleared] = useState(false) + + // the clear method + const clear = useCallback(() => { + if (timeout.current) { + setIsCleared(true) + clearInterval(timeout.current) + } + }, []) + + // if the provided function changes, change its reference + useEffect(() => { + if (typeof fn === 'function') { + callback.current = fn + } + }, [fn]) + + // when the milliseconds change, reset the timeout + useEffect(() => { + if (typeof milliseconds === 'number') { + timeout.current = setInterval(() => { + callback.current() + }, milliseconds) + } + + // cleanup previous interval + return clear + }, [milliseconds]) + + // when component unmount clear the timeout + useEffect(() => () => { + if (opts.cancelOnUnmount) { + clear() + } + }, []) + + return [isCleared, clear] +} + +export default useInterval diff --git a/src/useLifecycle.js b/src/useLifecycle.js deleted file mode 100644 index 500d5aa1..00000000 --- a/src/useLifecycle.js +++ /dev/null @@ -1,15 +0,0 @@ -import useDidMount from './useDidMount'; -import useWillUnmount from './useWillUnmount'; - -/** - * Returns an object wrapping lifecycle hooks such as `useDidMount` or `useWillUnmount`. - * It is intended as a shortcut to those hooks. - */ -const useLifecycle = (mount, unmount) => { - const onDidMount = useDidMount(mount); - const onWillUnmount = useWillUnmount(unmount); - - return { onDidMount, onWillUnmount }; -}; - -export default useLifecycle; diff --git a/src/useLifecycle.ts b/src/useLifecycle.ts new file mode 100644 index 00000000..f989c343 --- /dev/null +++ b/src/useLifecycle.ts @@ -0,0 +1,16 @@ +import useDidMount from './useDidMount' +import useWillUnmount from './useWillUnmount' +import { Noop } from './shared/types' + +/** + * Returns an object wrapping lifecycle hooks such as `useDidMount` or `useWillUnmount`. + * It is intended as a shortcut to those hooks. + */ +const useLifecycle = any = Noop, U extends (...args: any[]) => any = Noop>(mount?: M, unmount?: U) => { + const onDidMount = useDidMount(mount) + const onWillUnmount = useWillUnmount(unmount) + + return { onDidMount, onWillUnmount } +} + +export default useLifecycle diff --git a/src/useLocalStorage.js b/src/useLocalStorage.js deleted file mode 100644 index b8d9baa1..00000000 --- a/src/useLocalStorage.js +++ /dev/null @@ -1,8 +0,0 @@ -import useStorage from './useStorage'; - -/** - * Save a value on local storage - */ -const useLocalStorage = useStorage('local'); - -export default useLocalStorage; diff --git a/src/useLocalStorage.ts b/src/useLocalStorage.ts new file mode 100644 index 00000000..454b1af1 --- /dev/null +++ b/src/useLocalStorage.ts @@ -0,0 +1,8 @@ +import createStorageHook from './shared/createStorageHook' + +/** + * Save a value on local storage + */ +const useLocalStorage = createStorageHook('local') + +export default useLocalStorage diff --git a/src/useMediaQuery.js b/src/useMediaQuery.ts similarity index 55% rename from src/useMediaQuery.js rename to src/useMediaQuery.ts index 9cd48123..e751688f 100644 --- a/src/useMediaQuery.js +++ b/src/useMediaQuery.ts @@ -1,9 +1,9 @@ -import { useEffect, useState } from 'react'; -import isClient from './utils/isClient'; -import isAPISupported from './utils/isAPISupported'; +import { useEffect, useState } from 'react' +import isClient from './shared/isClient' +import isAPISupported from './shared/isAPISupported' const errorMessage = 'matchMedia is not supported, this could happen both because window.matchMedia is not supported by' - + ' your current browser or you\'re using the useMediaQuery hook whilst server side rendering.'; + + ' your current browser or you\'re using the useMediaQuery hook whilst server side rendering.' /** * Accepts a media query string then uses the @@ -13,42 +13,38 @@ const errorMessage = 'matchMedia is not supported, this could happen both becaus * Returns the validity state of the given media query. * */ -const useMediaQuery = (mediaQuery) => { +const useMediaQuery = (mediaQuery: string): boolean => { if (!isClient || !isAPISupported('matchMedia')) { // eslint-disable-next-line no-console - console.warn(errorMessage); - return null; + console.warn(errorMessage) + return null } - const [isVerified, setIsVerified] = useState(!!window.matchMedia(mediaQuery).matches); + const [isVerified, setIsVerified] = useState(!!window.matchMedia(mediaQuery).matches) useEffect(() => { - const mediaQueryList = window.matchMedia(mediaQuery); - const documentChangeHandler = () => setIsVerified(!!mediaQueryList.matches); + const mediaQueryList = window.matchMedia(mediaQuery) + const documentChangeHandler = () => setIsVerified(!!mediaQueryList.matches) try { - mediaQueryList.addEventListener('change', documentChangeHandler); + mediaQueryList.addEventListener('change', documentChangeHandler) } catch (e) { - //Safari isn't supporting mediaQueryList.addEventListener - console.error(e); - mediaQueryList.addListener(documentChangeHandler); + // Safari isn't supporting mediaQueryList.addEventListener + mediaQueryList.addListener(documentChangeHandler) } - documentChangeHandler(); + documentChangeHandler() return () => { try { - mediaQueryList.removeEventListener('change', documentChangeHandler); - } catch (e) { - //Safari isn't supporting mediaQueryList.removeEventListener - console.error(e); - mediaQueryList.removeListener(documentChangeHandler); + mediaQueryList.removeEventListener('change', documentChangeHandler) + } catch (e) { + // Safari isn't supporting mediaQueryList.removeEventListener + mediaQueryList.removeListener(documentChangeHandler) + } } - - - }; - }, [mediaQuery]); + }, [mediaQuery]) - return isVerified; -}; + return isVerified +} -export default useMediaQuery; +export default useMediaQuery diff --git a/src/useMouse.js b/src/useMouse.js deleted file mode 100644 index c3a63303..00000000 --- a/src/useMouse.js +++ /dev/null @@ -1,16 +0,0 @@ -import useMouseEvents from './useMouseEvents'; -import useMouseState from './useMouseState'; - -/** - * Returns an array where the first item is the mouse state from the `useMouseState` hook and the second item - * is the object of callback setters from the `useMouseEvents` hook. - * It is intended as a shortcut to those hooks. - */ -const useMouse = (ref = null) => { - const state = useMouseState(ref); - const events = useMouseEvents(ref); - - return [state, events]; -}; - -export default useMouse; diff --git a/src/useMouse.ts b/src/useMouse.ts new file mode 100644 index 00000000..a76f5143 --- /dev/null +++ b/src/useMouse.ts @@ -0,0 +1,17 @@ +import { MutableRefObject } from 'react' +import useMouseEvents, { MouseEventsMap } from './useMouseEvents' +import useMouseState, { MouseStateSummary } from './useMouseState' + +/** + * Returns an array where the first item is the mouse state from the `useMouseState` hook and the second item + * is the object of callback setters from the `useMouseEvents` hook. + * It is intended as a shortcut to those hooks. + */ +const useMouse = (targetRef: MutableRefObject = null): [MouseStateSummary, MouseEventsMap] => { + const state = useMouseState(targetRef) + const events = useMouseEvents(targetRef) + + return [state, events] +} + +export default useMouse diff --git a/src/useMouseEvents.js b/src/useMouseEvents.js deleted file mode 100644 index 74240be4..00000000 --- a/src/useMouseEvents.js +++ /dev/null @@ -1,53 +0,0 @@ -import createHandlerSetter from './utils/createHandlerSetter'; -import createCbSetterErrorProxy from './utils/createCbSetterErrorProxy'; -import hasOwnProperty from './utils/hasOwnProperty'; -import assignEventCallbackOnMountEffect from './utils/assignEventCallbackOnMountEffect'; - -/** - * Returns a frozen object of callback setters to handle the mouse events.
- * It accepts a DOM ref representing the events target.
- * If a target is not provided the events will be globally attached to the document object. - *
- * ### Shall the `useMouseEvents` callbacks replace the standard mouse handler props? - * - * **They shall not!**
- * **useMouseEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: - * a drag n drop hook.
- * Using useMouseEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll - * lose the React SyntheticEvent performance boost.
- * If you were doing something like the following: - * - */ -const useMouseEvents = (targetRef = null) => { - const [onMouseDownHandler, setOnMouseDown] = createHandlerSetter(); - const [onMouseEnterHandler, setOnMouseEnter] = createHandlerSetter(); - const [onMouseLeaveHandler, setOnMouseLeave] = createHandlerSetter(); - const [onMouseMoveHandler, setOnMouseMove] = createHandlerSetter(); - const [onMouseOutHandler, setOnMouseOut] = createHandlerSetter(); - const [onMouseOverHandler, setOnMouseOver] = createHandlerSetter(); - const [onMouseUpHandler, setOnMouseUp] = createHandlerSetter(); - - if (targetRef !== null && !hasOwnProperty(targetRef, 'current')) { - return createCbSetterErrorProxy('Unable to assign any mouse event to the given ref'); - } - - assignEventCallbackOnMountEffect(targetRef, onMouseDownHandler, 'mousedown'); - assignEventCallbackOnMountEffect(targetRef, onMouseEnterHandler, 'mouseenter'); - assignEventCallbackOnMountEffect(targetRef, onMouseLeaveHandler, 'mouseleave'); - assignEventCallbackOnMountEffect(targetRef, onMouseMoveHandler, 'mousemove'); - assignEventCallbackOnMountEffect(targetRef, onMouseOutHandler, 'mouseout'); - assignEventCallbackOnMountEffect(targetRef, onMouseOverHandler, 'mouseover'); - assignEventCallbackOnMountEffect(targetRef, onMouseUpHandler, 'mouseup'); - - return Object.freeze({ - onMouseDown: setOnMouseDown, - onMouseEnter: setOnMouseEnter, - onMouseLeave: setOnMouseLeave, - onMouseMove: setOnMouseMove, - onMouseOut: setOnMouseOut, - onMouseOver: setOnMouseOver, - onMouseUp: setOnMouseUp, - }); -}; - -export default useMouseEvents; diff --git a/src/useMouseEvents.ts b/src/useMouseEvents.ts new file mode 100644 index 00000000..a0397d14 --- /dev/null +++ b/src/useMouseEvents.ts @@ -0,0 +1,66 @@ +import { MutableRefObject } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import createCbSetterErrorProxy from './shared/createCbSetterErrorProxy' +import safeHasOwnProperty from './shared/safeHasOwnProperty' +import { CallbackSetter } from './shared/types' +import assignEventOnMount from './shared/assignEventOnMount' + +type MouseEventCallback = (event: MouseEvent) => any + +export type MouseEventsMap = { + readonly onMouseDown: CallbackSetter, + readonly onMouseEnter: CallbackSetter, + readonly onMouseLeave: CallbackSetter, + readonly onMouseMove: CallbackSetter, + readonly onMouseOut: CallbackSetter, + readonly onMouseOver: CallbackSetter, + readonly onMouseUp: CallbackSetter, +} + +/** + * Returns a frozen object of callback setters to handle the mouse events.
+ * It accepts a DOM ref representing the events target.
+ * If a target is not provided the events will be globally attached to the document object. + *
+ * ### Shall the `useMouseEvents` callbacks replace the standard mouse handler props? + * + * **They shall not!**
+ * **useMouseEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: + * a drag n drop hook.
+ * Using useMouseEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll + * lose the React SyntheticEvent performance boost.
+ * If you were doing something like the following: + */ +const useMouseEvents = (targetRef: MutableRefObject = null): MouseEventsMap => { + const [onMouseDownHandler, setOnMouseDown] = useHandlerSetterRef() + const [onMouseEnterHandler, setOnMouseEnter] = useHandlerSetterRef() + const [onMouseLeaveHandler, setOnMouseLeave] = useHandlerSetterRef() + const [onMouseMoveHandler, setOnMouseMove] = useHandlerSetterRef() + const [onMouseOutHandler, setOnMouseOut] = useHandlerSetterRef() + const [onMouseOverHandler, setOnMouseOver] = useHandlerSetterRef() + const [onMouseUpHandler, setOnMouseUp] = useHandlerSetterRef() + + if (targetRef !== null && !safeHasOwnProperty(targetRef, 'current')) { + return createCbSetterErrorProxy('Unable to assign any mouse event to the given ref') + } + + assignEventOnMount(targetRef, onMouseDownHandler, 'mousedown') + assignEventOnMount(targetRef, onMouseEnterHandler, 'mouseenter') + assignEventOnMount(targetRef, onMouseLeaveHandler, 'mouseleave') + assignEventOnMount(targetRef, onMouseMoveHandler, 'mousemove') + assignEventOnMount(targetRef, onMouseOutHandler, 'mouseout') + assignEventOnMount(targetRef, onMouseOverHandler, 'mouseover') + assignEventOnMount(targetRef, onMouseUpHandler, 'mouseup') + + return Object.freeze({ + onMouseDown: setOnMouseDown, + onMouseEnter: setOnMouseEnter, + onMouseLeave: setOnMouseLeave, + onMouseMove: setOnMouseMove, + onMouseOut: setOnMouseOut, + onMouseOver: setOnMouseOver, + onMouseUp: setOnMouseUp, + }) +} + +export default useMouseEvents diff --git a/src/useMouseState.js b/src/useMouseState.js deleted file mode 100644 index 5dd6c909..00000000 --- a/src/useMouseState.js +++ /dev/null @@ -1,28 +0,0 @@ -import { useState } from 'react'; -import useMouseEvents from './useMouseEvents'; - -const createStateObject = (event) => ({ - clientX: event.clientX, - clientY: event.clientY, - screenX: event.screenX, - screenY: event.screenY, -}); - -/** - * Returns the current state (position) of the mouse pointer. - * It possibly accepts a DOM ref representing the mouse target. - * If a target is not provided the state will be caught globally. - */ -const useMouseState = (ref = null) => { - const [state, setState] = useState({ clientX: 0, clientY: 0, screenX: 0, screenY: 0 }); - const { onMouseMove } = useMouseEvents(ref); - - onMouseMove((event) => { - const nextState = createStateObject(event); - setState(nextState); - }); - - return state; -}; - -export default useMouseState; diff --git a/src/useMouseState.ts b/src/useMouseState.ts new file mode 100644 index 00000000..21fe50e0 --- /dev/null +++ b/src/useMouseState.ts @@ -0,0 +1,35 @@ +import { MutableRefObject, useState } from 'react' +import useMouseEvents from './useMouseEvents' + +export type MouseStateSummary = { + clientX: number, + clientY: number, + screenX: number, + screenY: number +} + +const createStateObject = (event: MouseEvent): MouseStateSummary => ({ + clientX: event.clientX, + clientY: event.clientY, + screenX: event.screenX, + screenY: event.screenY, +}) + +/** + * Returns the current state (position) of the mouse pointer. + * It possibly accepts a DOM ref representing the mouse target. + * If a target is not provided the state will be caught globally. + */ +const useMouseState = (targetRef: MutableRefObject = null): MouseStateSummary => { + const [state, setState] = useState({ clientX: 0, clientY: 0, screenX: 0, screenY: 0 }) + const { onMouseMove } = useMouseEvents(targetRef) + + onMouseMove((event) => { + const nextState = createStateObject(event) + setState(nextState) + }) + + return state +} + +export default useMouseState diff --git a/src/useObservable.js b/src/useObservable.js deleted file mode 100644 index 8fef1515..00000000 --- a/src/useObservable.js +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect } from 'react'; - -/** - * Hook, which helps you combine rxjs flow and setState in your component - */ -const useObservable = (observable, setter) => { - useEffect(() => { - const subscription = observable.subscribe(setter); - - return () => subscription.unsubscribe(); - }, [observable, setter]); -}; - -export default useObservable; diff --git a/src/useObservable.ts b/src/useObservable.ts new file mode 100644 index 00000000..238280c6 --- /dev/null +++ b/src/useObservable.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +// eslint-disable-next-line import/no-extraneous-dependencies +import { Observable, Observer, Subscription } from 'rxjs' + +/** + * Hook, which helps you combine rxjs flow and setState in your component + */ +const useObservable = >) => Subscription>(observable: Observable, setter: F) => { + useEffect(() => { + const subscription = observable.subscribe(setter) + + return () => subscription.unsubscribe() + }, [observable, setter]) +} + +export default useObservable diff --git a/src/useOnlineState.js b/src/useOnlineState.ts similarity index 56% rename from src/useOnlineState.js rename to src/useOnlineState.ts index 69d785ea..79c34bfe 100644 --- a/src/useOnlineState.js +++ b/src/useOnlineState.ts @@ -1,35 +1,35 @@ -import { useState } from 'react'; -import useGlobalEvent from './useGlobalEvent'; +import { useState } from 'react' +import useGlobalEvent from './useGlobalEvent' /** * Uses the [Navigator online API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine) to define * whether the browser is connected or not. */ -const useOnlineState = () => { +const useOnlineState = (): boolean => { /** * If the browser doesn't support the `navigator.onLine` state, the hook will always return true * assuming the app is already online. */ - const isSupported = typeof window !== 'undefined' && 'ononline' in window; - const [isOnline, setIsOnline] = useState(isSupported ? navigator.onLine : true); - const whenOnline = useGlobalEvent('online', { capture: true }); - const whenOffline = useGlobalEvent('offline', { capture: true }); + const isSupported = typeof window !== 'undefined' && 'ononline' in window + const [isOnline, setIsOnline] = useState(isSupported ? navigator.onLine : true) + const whenOnline = useGlobalEvent('online', undefined, { capture: true }) + const whenOffline = useGlobalEvent('offline', undefined, { capture: true }) if (!isSupported) { // eslint-disable-next-line max-len, no-console - console.warn('The current device does not support the \'online/offline\' events, you should avoid using useOnlineState'); - return isOnline; + console.warn('The current device does not support the \'online/offline\' events, you should avoid using useOnlineState') + return isOnline } whenOnline(() => { - setIsOnline(true); - }); + setIsOnline(true) + }) whenOffline(() => { - setIsOnline(false); - }); + setIsOnline(false) + }) - return isOnline; -}; + return isOnline +} -export default useOnlineState; +export default useOnlineState diff --git a/src/usePreviousValue.js b/src/usePreviousValue.js deleted file mode 100644 index 261c20e0..00000000 --- a/src/usePreviousValue.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect, useRef } from 'react'; - -/** - * On each render returns the previous value of the given variable/constant. - */ -const usePreviousValue = (value) => { - const prevValue = useRef(); - - useEffect(() => { - prevValue.current = value; - - return () => { - prevValue.current = undefined; - }; - }); - - return prevValue.current; -}; - -export default usePreviousValue; diff --git a/src/usePreviousValue.ts b/src/usePreviousValue.ts new file mode 100644 index 00000000..110eaad8 --- /dev/null +++ b/src/usePreviousValue.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef } from 'react' + +/** + * On each render returns the previous value of the given variable/constant. + */ +const usePreviousValue = (value?: T): T => { + const prevValue = useRef() + + useEffect(() => { + prevValue.current = value + + return () => { + prevValue.current = undefined + } + }) + + return prevValue.current +} + +export default usePreviousValue diff --git a/src/useRenderInfo.js b/src/useRenderInfo.js deleted file mode 100644 index 895dd66c..00000000 --- a/src/useRenderInfo.js +++ /dev/null @@ -1,36 +0,0 @@ -import { useRef } from 'react'; - -const getInitial = (module) => ({ - module, - renders: 0, - timestamp: null, - sinceLast: null, -}); - -/** - * useRenderInfo - * @param module - * @param log - * @returns {{renders: number, module: *, timestamp: null}} - */ -const useRenderInfo = (module = 'Unknown component', log = true) => { - const { current: info } = useRef(getInitial(module)); - const now = +Date.now(); - - info.renders += 1; - info.sinceLast = info.timestamp ? (now - info.timestamp) / 1000 : '[now]'; - info.timestamp = now; - - if (log) { - /* eslint-disable no-console */ - console.group(`${module} info`); - console.log(`Render no: ${info.renders}${info.renders > 1 ? `, ${info.sinceLast}s since last render` : ''}`); - console.dir(info); - console.groupEnd(); - /* eslint-enable no-console */ - } - - return info; -}; - -export default useRenderInfo; diff --git a/src/useRenderInfo.ts b/src/useRenderInfo.ts new file mode 100644 index 00000000..11da9295 --- /dev/null +++ b/src/useRenderInfo.ts @@ -0,0 +1,43 @@ +import { useRef } from 'react' + +export interface RenderInfo { + readonly module: string; + renders: number; + timestamp: number; + sinceLast: number | '[now]' +} + +const getInitial = (moduleName: string): RenderInfo => ({ + module: moduleName, + renders: 0, + timestamp: null, + sinceLast: null, +}) + +/** + * useRenderInfo + * @param moduleName + * @param log + * @returns {{renders: number, module: *, timestamp: null}} + */ +const useRenderInfo = (moduleName: string = 'Unknown component', log: boolean = true): RenderInfo => { + const { current: info } = useRef(getInitial(moduleName)) + const now = +Date.now() + + info.renders += 1 + info.sinceLast = info.timestamp ? (now - info.timestamp) / 1000 : '[now]' + info.timestamp = now + + if (log) { + /* eslint-disable no-console */ + console.group(`${moduleName} info`) + console.log(`Render no: ${info.renders}${info.renders > 1 ? `, ${info.sinceLast}s since last render` : ''}`) + console.dir(info) + console.groupEnd() + /* eslint-enable no-console */ + } + + return info +} + +export default useRenderInfo diff --git a/src/useRequestAnimationFrame.js b/src/useRequestAnimationFrame.js deleted file mode 100644 index befb9591..00000000 --- a/src/useRequestAnimationFrame.js +++ /dev/null @@ -1,46 +0,0 @@ -import { useCallback, useRef } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; -import isClient from './utils/isClient'; -import isAPISupported from './utils/isAPISupported'; -import createCbSetterErrorProxy from './utils/createCbSetterErrorProxy'; - -const defaultOptions = { increment: 1, startAt: 0, finishAt: 100 }; - -const errorMessage = 'requestAnimationFrame is not supported, this could happen both because ' - + 'window.requestAnimationFrame is not supported by your current browser version or you\'re using the ' - + 'useRequestAnimationFrame hook whilst server side rendering.'; - -/** - * Takes care of running an animating function, provided as the first argument, while keeping track of its progress. - */ -const useRequestAnimationFrame = (func, options = defaultOptions) => { - if (!isClient || !isAPISupported('requestAnimationFrame')) { - // eslint-disable-next-line no-console - console.warn(errorMessage); - return createCbSetterErrorProxy(errorMessage); - } - - const opts = { ...defaultOptions, ...options }; - const progress = useRef(opts.startAt); - const [onFinish, setOnFinish] = createHandlerSetter(); - - // eslint-disable-next-line no-use-before-define - const next = () => window.requestAnimationFrame(step); - - const step = useCallback(() => { - if (progress.current <= opts.finishAt || opts.finishAt === -1) { - func(progress.current, next); - progress.current += opts.increment; - } else if (onFinish.current) { - onFinish.current(); - } - }, [func, opts.finishAt, opts.increment, progress.current, onFinish.current]); - - if (progress.current <= opts.startAt) { - next(); - } - - return setOnFinish; -}; - -export default useRequestAnimationFrame; diff --git a/src/useRequestAnimationFrame.ts b/src/useRequestAnimationFrame.ts new file mode 100644 index 00000000..269747ce --- /dev/null +++ b/src/useRequestAnimationFrame.ts @@ -0,0 +1,52 @@ +import { useCallback, useRef } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import isClient from './shared/isClient' +import isAPISupported from './shared/isAPISupported' +import createCbSetterErrorProxy from './shared/createCbSetterErrorProxy' + +export type UseRequestAnimationFrameOpts = { + increment?: number, + startAt?: number, + finishAt?: number, +} + +const defaultOptions = { increment: 1, startAt: 0, finishAt: 100 } + +const errorMessage = 'requestAnimationFrame is not supported, this could happen both because ' + + 'window.requestAnimationFrame is not supported by your current browser version or you\'re using the ' + + 'useRequestAnimationFrame hook whilst server side rendering.' + +/** + * Takes care of running an animating function, provided as the first argument, while keeping track of its progress. + */ +const useRequestAnimationFrame = any>(func: T, options: UseRequestAnimationFrameOpts = defaultOptions) => { + if (!isClient || !isAPISupported('requestAnimationFrame')) { + // eslint-disable-next-line no-console + console.warn(errorMessage) + return createCbSetterErrorProxy(errorMessage) + } + + const opts = { ...defaultOptions, ...options } + const progress = useRef(opts.startAt) + const [onFinish, setOnFinish] = useHandlerSetterRef() + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const next = () => window.requestAnimationFrame(step) + + const step = useCallback(() => { + if (progress.current <= opts.finishAt || opts.finishAt === -1) { + func(progress.current, next) + progress.current += opts.increment + } else if (onFinish.current) { + onFinish.current() + } + }, [func, opts.finishAt, opts.increment, progress.current, onFinish]) + + if (progress.current <= opts.startAt) { + next() + } + + return setOnFinish +} + +export default useRequestAnimationFrame diff --git a/src/useResizeObserver.js b/src/useResizeObserver.js deleted file mode 100644 index 7c92a0b7..00000000 --- a/src/useResizeObserver.js +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import debounce from 'lodash.debounce'; -import isApiSupported from './utils/isAPISupported'; -import isClient from './utils/isClient'; - -// eslint-disable-next-line max-len -const errorMessage = 'ResizeObserver is not supported, this could happen both because window.ResizeObserver is not supported by your current browser or you\'re using the useResizeObserver hook whilst server side rendering.'; - -/** - * Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect. - * @param elementRef - * @param debounceTimeout - * @returns {undefined} - */ -const useResizeObserver = (elementRef, debounceTimeout = 100) => { - const isSupported = isApiSupported('ResizeObserver'); - const observerRef = useRef(null); - const [DOMRect, setDOMRect] = useState(); - - if (isClient && !isSupported) { - // eslint-disable-next-line no-console - console.warn(errorMessage); - } - - // creates the observer reference on mount - useEffect(() => { - if (isSupported) { - const fn = debounce((entries) => { - const { bottom, height, left, right, top, width } = entries[0].contentRect; - - setDOMRect({ bottom, height, left, right, top, width }); - }, debounceTimeout); - - observerRef.current = new ResizeObserver(fn); - - return () => { - fn.cancel(); - observerRef.current.disconnect(); - }; - } - - return () => {}; - }, []); - - // observes on the provided element ref - useEffect(() => { - if (isSupported && elementRef.current) { - observerRef.current.observe(elementRef.current); - } - }, [elementRef.current]); - - return DOMRect; -}; - -export default useResizeObserver; diff --git a/src/useResizeObserver.ts b/src/useResizeObserver.ts new file mode 100644 index 00000000..ea75b7ec --- /dev/null +++ b/src/useResizeObserver.ts @@ -0,0 +1,58 @@ +import { MutableRefObject, useEffect, useRef, useState } from 'react' +import debounce from 'lodash.debounce' +import isApiSupported from './shared/isAPISupported' +import isClient from './shared/isClient' + +// eslint-disable-next-line max-len +const errorMessage = 'ResizeObserver is not supported, this could happen both because window.ResizeObserver is not supported by your current browser or you\'re using the useResizeObserver hook whilst server side rendering.' + +export type DOMRectValues = Pick + +/** + * Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect. + * @param elementRef + * @param debounceTimeout + * @returns {undefined} + */ +const useResizeObserver = (elementRef: MutableRefObject, debounceTimeout: number = 100): DOMRectValues => { + const isSupported = isApiSupported('ResizeObserver') + const observerRef = useRef(null) + const [DOMRect, setDOMRect] = useState() + + if (isClient && !isSupported) { + // eslint-disable-next-line no-console + console.warn(errorMessage) + } + + // creates the observer reference on mount + useEffect(() => { + if (isSupported) { + const fn = debounce((entries) => { + const { bottom, height, left, right, top, width } = entries[0].contentRect + + setDOMRect({ bottom, height, left, right, top, width }) + }, debounceTimeout) + + observerRef.current = new ResizeObserver(fn) + + return () => { + fn.cancel() + observerRef.current.disconnect() + } + } + + return () => { + } + }, []) + + // observes on the provided element ref + useEffect(() => { + if (isSupported && elementRef.current) { + observerRef.current.observe(elementRef.current) + } + }, [elementRef.current]) + + return DOMRect +} + +export default useResizeObserver diff --git a/src/useSessionStorage.js b/src/useSessionStorage.js deleted file mode 100644 index 6394ae8c..00000000 --- a/src/useSessionStorage.js +++ /dev/null @@ -1,8 +0,0 @@ -import useStorage from './useStorage'; - -/** - * Save a value on session storage - */ -const useSessionStorage = useStorage('session'); - -export default useSessionStorage; diff --git a/src/useSessionStorage.ts b/src/useSessionStorage.ts new file mode 100644 index 00000000..d7f18ad8 --- /dev/null +++ b/src/useSessionStorage.ts @@ -0,0 +1,8 @@ +import createStorageHook from './shared/createStorageHook' + +/** + * Save a value on session storage + */ +const useSessionStorage = createStorageHook('session') + +export default useSessionStorage diff --git a/src/useSpeechSynthesis.js b/src/useSpeechSynthesis.js deleted file mode 100644 index 28e06c07..00000000 --- a/src/useSpeechSynthesis.js +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useEffect, useMemo } from 'react'; - -const defaultOptions = { rate: 0, pitch: 0, volume: 1 }; - -/** - * Enables the possibility to perform a text-to-speach (with different voices) operation in your - * React component by using the Web_Speech_API - */ -const useSpeechSynthesis = (text, options = defaultOptions) => { - const utter = useMemo(() => new SpeechSynthesisUtterance(text), [text]); - const voiceOptions = { ...defaultOptions, ...options }; - utter.voice = voiceOptions.voice; - - useEffect(() => { - utter.pitch = voiceOptions.pitch; - }, [voiceOptions.pitch]); - - useEffect(() => { - utter.rate = voiceOptions.rate; - }, [voiceOptions.rate]); - - useEffect(() => { - utter.volume = voiceOptions.volume; - }, [voiceOptions.volume]); - - const speak = useCallback( - () => speechSynthesis.speak(utter), - [text, voiceOptions.pitch, voiceOptions.rate, voiceOptions.voice, voiceOptions.volume], - ); - - return { - speak, - speechSynthUtterance: utter, - }; -}; - -export default useSpeechSynthesis; diff --git a/src/useSpeechSynthesis.ts b/src/useSpeechSynthesis.ts new file mode 100644 index 00000000..d0cc26ca --- /dev/null +++ b/src/useSpeechSynthesis.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useMemo } from 'react' + +export type UseSpeechSynthesisOptions = { + rate?: number, + pitch?: number, + volume?: number + voice?: SpeechSynthesisVoice, +} + +const defaultOptions: UseSpeechSynthesisOptions = { rate: 0, pitch: 0, volume: 1 } + +export type SpeechSynthesisResult = { + speak: () => void, + speechSynthUtterance: SpeechSynthesisUtterance +} + +/** + * Enables the possibility to perform a text-to-speech (with different voices) operation in your + * React component by using the Web_Speech_API + */ +const useSpeechSynthesis = (text: string, options: UseSpeechSynthesisOptions = defaultOptions): SpeechSynthesisResult => { + const utter: SpeechSynthesisUtterance = useMemo(() => new SpeechSynthesisUtterance(text), [text]) + const voiceOptions = { ...defaultOptions, ...options } + utter.voice = voiceOptions.voice + + useEffect(() => { + utter.pitch = voiceOptions.pitch + }, [voiceOptions.pitch]) + + useEffect(() => { + utter.rate = voiceOptions.rate + }, [voiceOptions.rate]) + + useEffect(() => { + utter.volume = voiceOptions.volume + }, [voiceOptions.volume]) + + const speak = useCallback( + () => speechSynthesis.speak(utter), + [text, voiceOptions.pitch, voiceOptions.rate, voiceOptions.voice, voiceOptions.volume], + ) + + return { + speak, + speechSynthUtterance: utter, + } +} + +export default useSpeechSynthesis diff --git a/src/useStorage.js b/src/useStorage.js deleted file mode 100644 index d717415c..00000000 --- a/src/useStorage.js +++ /dev/null @@ -1,43 +0,0 @@ -import { useState, useEffect } from 'react'; -import safelyParseJson from './utils/safelyParseJson'; -import isClient from './utils/isClient'; -import isAPISupported from './utils/isAPISupported'; -import isDevelopment from './utils/isDevelopment'; - -/** - * An utility to quickly create hooks to access both Session Storage and Local Storage - */ -const useStorage = (type) => { - const storageName = `${type}Storage`; - - if (isClient && !isAPISupported(storageName)) { - // eslint-disable-next-line no-console - console.warn(`${storageName} is not supported`); - } - - /** - * hook - */ - return (storageKey, defaultValue) => { - if (!isClient) { - if (isDevelopment) { - // eslint-disable-next-line no-console - console.warn(`Please be aware that ${storageName} could not be available during SSR`); - } - return [JSON.stringify(defaultValue), () => undefined]; - } - - const storage = window[storageName]; - const [value, setValue] = useState( - safelyParseJson(storage.getItem(storageKey) || JSON.stringify(defaultValue)), - ); - - useEffect(() => { - storage.setItem(storageKey, JSON.stringify(value)); - }, [storageKey, value]); - - return [value, setValue]; - }; -}; - -export default useStorage; diff --git a/src/useSwipe.js b/src/useSwipe.js deleted file mode 100644 index ce349f1a..00000000 --- a/src/useSwipe.js +++ /dev/null @@ -1,137 +0,0 @@ -import { useRef, useState } from 'react'; -import useMouseEvents from './useMouseEvents'; -import useTouchEvents from './useTouchEvents'; -import { getDirection, getHorizontalDirection, getPointerCoordinates, getVerticalDirection } from './utils/swipeUtils'; - -const defaultOptions = { - direction: 'both', - threshold: 10, - preventDefault: true, -}; - -const initialState = { swiping: false, direction: null, alphaX: 0, alphaY: 0, count: 0 }; - -const isEqual = (prev, next) => ( - prev.swiping === next.swiping - && prev.direction === next.direction - && prev.count === next.count - && prev.alphaX === next.alphaX - && prev.alphaY === next.alphaY -); - -/** - * useSwipe hook - */ -const useSwipe = (targetRef = null, options = defaultOptions) => { - const [state, setState] = useState(initialState); - const startingPointRef = useRef([-1, -1]); - const isDraggingRef = useRef(false); - const opts = { ...defaultOptions, ...(options || {}) }; - const { onMouseDown, onMouseMove, onMouseLeave, onMouseUp } = useMouseEvents(targetRef); - const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useTouchEvents(targetRef); - - const startSwipe = (event) => { - const [clientX, clientY] = getPointerCoordinates(event); - startingPointRef.current = [clientX, clientY]; - - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - }; - - const continueSwipe = (event) => { - const [clientX, clientY] = getPointerCoordinates(event); - - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - - if (isDraggingRef.current || (startingPointRef.current[0] !== -1 && startingPointRef.current[1] !== -1)) { - const alpha = [startingPointRef.current[0] - clientX, startingPointRef.current[1] - clientY]; - - if (opts.direction === 'both' && (Math.abs(alpha[0]) > opts.threshold || Math.abs(alpha[1]) > opts.threshold)) { - isDraggingRef.current = true; - - const nextState = { - alphaX: alpha[0], - alphaY: alpha[1], - count: state.count, - swiping: true, - direction: getDirection([clientX, clientY], startingPointRef.current, alpha), - }; - - if (!isEqual(nextState, state)) { - setState(nextState); - } - } - - if (opts.direction === 'horizontal' && Math.abs(alpha[0]) > opts.threshold) { - isDraggingRef.current = true; - - const nextState = { - alphaX: alpha[0], - alphaY: 0, - count: state.count, - swiping: true, - direction: getHorizontalDirection(alpha[0]), - }; - - if (!isEqual(nextState, state)) { - setState(nextState); - } - } - - if (opts.direction === 'vertical' && Math.abs(alpha[1]) > opts.threshold) { - isDraggingRef.current = true; - - const nextState = { - alphaY: alpha[1], - alphaX: 0, - count: state.count, - swiping: true, - direction: getVerticalDirection(alpha[1]), - }; - - if (!isEqual(nextState, state)) { - setState(nextState); - } - } - } - }; - - const endSwipe = (event) => { - if (isDraggingRef.current) { - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - - setState((prevState) => ({ - ...prevState, - swiping: false, - count: state.count + 1, - })); - } - - startingPointRef.current = [-1, -1]; - isDraggingRef.current = false; - }; - - onMouseDown(startSwipe); - onTouchStart(startSwipe); - - onMouseMove(continueSwipe); - onTouchMove(continueSwipe); - - onMouseUp(endSwipe); - onTouchEnd(endSwipe); - - onMouseLeave(endSwipe); - onTouchCancel(endSwipe); - - return state; -}; - -export default useSwipe; diff --git a/src/useSwipe.ts b/src/useSwipe.ts new file mode 100644 index 00000000..64e84804 --- /dev/null +++ b/src/useSwipe.ts @@ -0,0 +1,151 @@ +import { MutableRefObject, useRef, useState } from 'react' +import useMouseEvents from './useMouseEvents' +import useTouchEvents from './useTouchEvents' +import { Direction, getDirection, getHorizontalDirection, getPointerCoordinates, getVerticalDirection } from './shared/swipeUtils' + +export type UseSwipeOptions = { + direction?: 'both' | 'horizontal' | 'vertical', + threshold?: number, + preventDefault?: boolean, +} + +const defaultOptions: UseSwipeOptions = { + direction: 'both', + threshold: 10, + preventDefault: true, +} + +type LocalSwipeState = { + swiping: boolean, + direction?: Direction, + alphaX: number, + alphaY: number, + count: number, +} + +const initialState: LocalSwipeState = { swiping: false, direction: undefined, alphaX: 0, alphaY: 0, count: 0 } + +const isEqual = (prev: LocalSwipeState, next: LocalSwipeState): boolean => ( + prev.swiping === next.swiping + && prev.direction === next.direction + && prev.count === next.count + && prev.alphaX === next.alphaX + && prev.alphaY === next.alphaY +) + +/** + * useSwipe hook + */ +const useSwipe = (targetRef: MutableRefObject = null, options: UseSwipeOptions = defaultOptions) => { + const [state, setState] = useState(initialState) + const startingPointRef = useRef<[number, number]>([-1, -1]) + const isDraggingRef = useRef(false) + const opts = { ...defaultOptions, ...(options || {}) } + const { onMouseDown, onMouseMove, onMouseLeave, onMouseUp } = useMouseEvents(targetRef) + const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useTouchEvents(targetRef) + + const startSwipe = (event: MouseEvent | TouchEvent) => { + const [clientX, clientY] = getPointerCoordinates(event) + startingPointRef.current = [clientX, clientY] + + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + } + + const continueSwipe = (event: MouseEvent | TouchEvent) => { + const [clientX, clientY] = getPointerCoordinates(event) + + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + + if (isDraggingRef.current || (startingPointRef.current[0] !== -1 && startingPointRef.current[1] !== -1)) { + const alpha: [number, number] = [startingPointRef.current[0] - clientX, startingPointRef.current[1] - clientY] + + if (opts.direction === 'both' && (Math.abs(alpha[0]) > opts.threshold || Math.abs(alpha[1]) > opts.threshold)) { + isDraggingRef.current = true + + const nextState: LocalSwipeState = { + alphaX: alpha[0], + alphaY: alpha[1], + count: state.count, + swiping: true, + direction: getDirection([clientX, clientY], startingPointRef.current, alpha), + } + + if (!isEqual(nextState, state)) { + setState(nextState) + } + } + + if (opts.direction === 'horizontal' && Math.abs(alpha[0]) > opts.threshold) { + isDraggingRef.current = true + + const nextState: LocalSwipeState = { + alphaX: alpha[0], + alphaY: 0, + count: state.count, + swiping: true, + direction: getHorizontalDirection(alpha[0]), + } + + if (!isEqual(nextState, state)) { + setState(nextState) + } + } + + if (opts.direction === 'vertical' && Math.abs(alpha[1]) > opts.threshold) { + isDraggingRef.current = true + + const nextState: LocalSwipeState = { + alphaY: alpha[1], + alphaX: 0, + count: state.count, + swiping: true, + direction: getVerticalDirection(alpha[1]), + } + + if (!isEqual(nextState, state)) { + setState(nextState) + } + } + } + } + + const endSwipe = (event: MouseEvent | TouchEvent) => { + if (isDraggingRef.current) { + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + + setState((prevState) => ({ + ...prevState, + swiping: false, + count: state.count + 1, + })) + } + + startingPointRef.current = [-1, -1] + isDraggingRef.current = false + } + + onMouseDown(startSwipe) + onTouchStart(startSwipe) + + onMouseMove(continueSwipe) + onTouchMove(continueSwipe) + + onMouseUp(endSwipe) + onTouchEnd(endSwipe) + + onMouseLeave(endSwipe) + onTouchCancel(endSwipe) + + return state +} + +export default useSwipe diff --git a/src/useSwipeEvents.js b/src/useSwipeEvents.js deleted file mode 100644 index 394b1925..00000000 --- a/src/useSwipeEvents.js +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; -import useMouseEvents from './useMouseEvents'; -import useTouchEvents from './useTouchEvents'; -import { getDirection, getPointerCoordinates } from './utils/swipeUtils'; - -const defaultOptions = { - threshold: 15, - preventDefault: true, -}; - -/** - * Very similar to useSwipe but doesn't cause re-rendering during swipe - - */ -const useSilentSwipeState = (targetRef = null, options = defaultOptions, onSwipeStart, onSwipeMove, onSwipeEnd) => { - const startingPointRef = useRef([-1, -1]); - const directionRef = useRef(null); - const isDraggingRef = useRef(false); - const alphaRef = useRef(false); - const opts = { ...defaultOptions, ...(options || {}) }; - const { onMouseDown, onMouseMove, onMouseLeave, onMouseUp } = useMouseEvents(targetRef); - const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useTouchEvents(targetRef); - const [state, setState] = useState(); - - const startSwipe = (event) => { - const [clientX, clientY] = getPointerCoordinates(event); - startingPointRef.current = [clientX, clientY]; - directionRef.current = null; - - if (onSwipeStart) { - onSwipeStart({ clientX, clientY }); - } - - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - }; - - const continueSwipe = (event) => { - const [clientX, clientY] = getPointerCoordinates(event); - - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - - if (startingPointRef.current[0] !== -1 && startingPointRef.current[1] !== -1) { - const alpha = [startingPointRef.current[0] - clientX, startingPointRef.current[1] - clientY]; - - if (Math.abs(alpha[0]) > opts.threshold || Math.abs(alpha[1]) > opts.threshold) { - isDraggingRef.current = true; - directionRef.current = getDirection([clientX, clientY], startingPointRef.current, alpha); - alphaRef.current = alpha; - - if (onSwipeMove) { - onSwipeMove({ - clientX, - clientY, - direction: directionRef.current, - alphaX: alphaRef.current[0], - alphaY: alphaRef.current[1], - }); - } - } - } - }; - - const endSwipe = (event) => { - if (isDraggingRef.current && directionRef.current) { - if (opts.preventDefault) { - event.preventDefault(); - event.stopPropagation(); - } - - setState({ - direction: directionRef.current, - alphaX: alphaRef.current[0], - alphaY: alphaRef.current[1], - }); - - if (onSwipeEnd) { - onSwipeEnd({ - direction: directionRef.current, - alphaX: alphaRef.current[0], - alphaY: alphaRef.current[1], - }); - } - } - - startingPointRef.current = [-1, -1]; - isDraggingRef.current = false; - directionRef.current = null; - }; - - onMouseDown(startSwipe); - onTouchStart(startSwipe); - - onMouseMove(continueSwipe); - onTouchMove(continueSwipe); - - onMouseUp(endSwipe); - onTouchEnd(endSwipe); - - onMouseLeave(endSwipe); - onTouchCancel(endSwipe); - - return state; -}; - -/** - * useSwipeEvents - * @param targetRef - * @param options - */ -const useSwipeEvents = (targetRef = null, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}) }; - const [onSwipeLeft, setOnSwipeLeft] = createHandlerSetter(); - const [onSwipeRight, setOnSwipeRight] = createHandlerSetter(); - const [onSwipeUp, setOnSwipeUp] = createHandlerSetter(); - const [onSwipeDown, setOnSwipeDown] = createHandlerSetter(); - const [onSwipeStart, setOnSwipeStart] = createHandlerSetter(); - const [onSwipeMove, setOnSwipeMove] = createHandlerSetter(); - const [onSwipeEnd, setOnSwipeEnd] = createHandlerSetter(); - const state = useSilentSwipeState(targetRef, opts, onSwipeStart.current, onSwipeMove.current, onSwipeEnd.current); - - const fnMap = { - right: onSwipeRight.current, - left: onSwipeLeft.current, - up: onSwipeUp.current, - down: onSwipeDown.current, - }; - - useEffect(() => { - if (state && state.direction) { - const cb = fnMap[state.direction]; - - if (cb && typeof cb === 'function') { - cb(state); - } - } - }, [state]); - - return Object.freeze({ - onSwipeLeft: setOnSwipeLeft, - onSwipeRight: setOnSwipeRight, - onSwipeUp: setOnSwipeUp, - onSwipeDown: setOnSwipeDown, - onSwipeMove: setOnSwipeMove, - onSwipeStart: setOnSwipeStart, - onSwipeEnd: setOnSwipeEnd, - }); -}; - -export default useSwipeEvents; diff --git a/src/useSwipeEvents.ts b/src/useSwipeEvents.ts new file mode 100644 index 00000000..af5006d6 --- /dev/null +++ b/src/useSwipeEvents.ts @@ -0,0 +1,175 @@ +import { MutableRefObject, useEffect, useRef, useState } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import useMouseEvents from './useMouseEvents' +import useTouchEvents from './useTouchEvents' +import { getDirection, getPointerCoordinates } from './shared/swipeUtils' + +export type SwipeState = { + clientX?: number + clientY?: number + direction: 'right' | 'left' | 'up' | 'down', + alphaX: number, + alphaY: number, +} + +export type SwipeCallback = (state: SwipeState) => any + +export type UseEventsSwipeOptions = { + threshold?: number, + preventDefault?: boolean, +} + +const defaultOptions: UseEventsSwipeOptions = { + threshold: 15, + preventDefault: true, +} + +/** + * Very similar to useSwipe but doesn't cause re-rendering during swipe + */ +const useSilentSwipeState = ( + targetRef: MutableRefObject = null, + options: UseEventsSwipeOptions = defaultOptions, + onSwipeStart: (...args: any[]) => any, + onSwipeMove: (...args: any[]) => any, + onSwipeEnd: (...args: any[]) => any) => { + const startingPointRef = useRef<[number, number]>([-1, -1]) + const directionRef = useRef<'right' | 'left' | 'up' | 'down'>(null) + const isDraggingRef = useRef(false) + const alphaRef = useRef([]) + const opts = { ...defaultOptions, ...(options || {}) } + const { onMouseDown, onMouseMove, onMouseLeave, onMouseUp } = useMouseEvents(targetRef) + const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useTouchEvents(targetRef) + const [state, setState] = useState() + + const startSwipe = (event: MouseEvent | TouchEvent) => { + const [clientX, clientY] = getPointerCoordinates(event) + startingPointRef.current = [clientX, clientY] + directionRef.current = null + + if (onSwipeStart) { + onSwipeStart({ clientX, clientY }) + } + + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + } + + const continueSwipe = (event: MouseEvent | TouchEvent) => { + const [clientX, clientY] = getPointerCoordinates(event) + + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + + if (startingPointRef.current[0] !== -1 && startingPointRef.current[1] !== -1) { + const alpha: [number, number] = [startingPointRef.current[0] - clientX, startingPointRef.current[1] - clientY] + + if (Math.abs(alpha[0]) > opts.threshold || Math.abs(alpha[1]) > opts.threshold) { + isDraggingRef.current = true + directionRef.current = getDirection([clientX, clientY], startingPointRef.current, alpha) + alphaRef.current = alpha + + if (onSwipeMove) { + onSwipeMove({ + clientX, + clientY, + direction: directionRef.current, + alphaX: alphaRef.current[0], + alphaY: alphaRef.current[1], + }) + } + } + } + } + + const endSwipe = (event: MouseEvent | TouchEvent) => { + if (isDraggingRef.current && directionRef.current) { + if (opts.preventDefault) { + event.preventDefault() + event.stopPropagation() + } + + setState({ + direction: directionRef.current, + alphaX: alphaRef.current[0], + alphaY: alphaRef.current[1], + }) + + if (onSwipeEnd) { + onSwipeEnd({ + direction: directionRef.current, + alphaX: alphaRef.current[0], + alphaY: alphaRef.current[1], + }) + } + } + + startingPointRef.current = [-1, -1] + isDraggingRef.current = false + directionRef.current = null + } + + onMouseDown(startSwipe) + onTouchStart(startSwipe) + + onMouseMove(continueSwipe) + onTouchMove(continueSwipe) + + onMouseUp(endSwipe) + onTouchEnd(endSwipe) + + onMouseLeave(endSwipe) + onTouchCancel(endSwipe) + + return state +} + +/** + * useSwipeEvents + * @param targetRef + * @param options + */ +const useSwipeEvents = (targetRef: MutableRefObject = null, options: UseEventsSwipeOptions = defaultOptions) => { + const opts = { ...defaultOptions, ...(options || {}) } + const [onSwipeLeft, setOnSwipeLeft] = useHandlerSetterRef() + const [onSwipeRight, setOnSwipeRight] = useHandlerSetterRef() + const [onSwipeUp, setOnSwipeUp] = useHandlerSetterRef() + const [onSwipeDown, setOnSwipeDown] = useHandlerSetterRef() + const [onSwipeStart, setOnSwipeStart] = useHandlerSetterRef() + const [onSwipeMove, setOnSwipeMove] = useHandlerSetterRef() + const [onSwipeEnd, setOnSwipeEnd] = useHandlerSetterRef() + const state: SwipeState = useSilentSwipeState(targetRef, opts, onSwipeStart.current, onSwipeMove.current, onSwipeEnd.current) + + const fnMap = { + right: onSwipeRight, + left: onSwipeLeft, + up: onSwipeUp, + down: onSwipeDown, + } + + useEffect(() => { + if (state && state.direction) { + const cb = fnMap[state.direction].current + + if (cb && typeof cb === 'function') { + cb(state) + } + } + }, [state]) + + return Object.freeze({ + onSwipeLeft: setOnSwipeLeft, + onSwipeRight: setOnSwipeRight, + onSwipeUp: setOnSwipeUp, + onSwipeDown: setOnSwipeDown, + onSwipeMove: setOnSwipeMove, + onSwipeStart: setOnSwipeStart, + onSwipeEnd: setOnSwipeEnd, + }) +} + +export default useSwipeEvents diff --git a/src/useSystemVoices.js b/src/useSystemVoices.ts similarity index 52% rename from src/useSystemVoices.js rename to src/useSystemVoices.ts index ac298b7d..7835a7a7 100644 --- a/src/useSystemVoices.js +++ b/src/useSystemVoices.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react' /** * At the moment, the `window.speechSynthesis.getVoices` function returns all the available system voices, but since @@ -6,22 +6,22 @@ import { useEffect, useState } from 'react'; * * Check: https://w3c.github.io/speech-api/speechapi-errata.html. */ -const asyncGetSystemVoices = () => new Promise((resolve) => { - window.speechSynthesis.onvoiceschanged = () => resolve(window.speechSynthesis.getVoices()); - window.speechSynthesis.getVoices(); -}); +const asyncGetSystemVoices = (): Promise => new Promise((resolve) => { + window.speechSynthesis.onvoiceschanged = () => resolve(window.speechSynthesis.getVoices()) + window.speechSynthesis.getVoices() +}) /** * A side effect to retrieve all the available system voices using the Web_Speech_API */ -const useSystemVoices = () => { - const [voices, setVoices] = useState([]); +const useSystemVoices = (): SpeechSynthesisVoice[] => { + const [voices, setVoices] = useState([]) useEffect(() => { - asyncGetSystemVoices().then(setVoices); - }, []); + asyncGetSystemVoices().then(setVoices) + }, []) - return voices; -}; + return voices +} -export default useSystemVoices; +export default useSystemVoices diff --git a/src/useThrottledFn.js b/src/useThrottledFn.js deleted file mode 100644 index de54d9d3..00000000 --- a/src/useThrottledFn.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useCallback } from 'react'; -import throttle from 'lodash.throttle'; - -const defaultOptions = { - leading: false, - trailing: true, -}; - -/** - * Accepts a function and returns a new throttled yet memoized version of that same function that waits the defined time - * before allowing the next execution. - * If time is not defined, its default value will be 100ms. - */ -const useThrottledFn = (fn, wait = 100, options = defaultOptions, dependencies) => { - const throttled = throttle(fn, wait, options); - - return useCallback(throttled, dependencies); -}; - -export default useThrottledFn; diff --git a/src/useThrottledFn.ts b/src/useThrottledFn.ts new file mode 100644 index 00000000..7bc9a057 --- /dev/null +++ b/src/useThrottledFn.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react' +import throttle from 'lodash.throttle' +import { DebouncedFunc } from './shared/types' + +interface ThrottleSettings { + leading?: boolean | undefined; + trailing?: boolean | undefined; +} + +const defaultOptions: ThrottleSettings = { + leading: false, + trailing: true, +} + +/** + * Accepts a function and returns a new throttled yet memoized version of that same function that waits the defined time + * before allowing the next execution. + * If time is not defined, its default value will be 100ms. + */ +const useThrottledFn = any> + (fn: T, wait: number = 100, options: ThrottleSettings = defaultOptions, dependencies: any[]): DebouncedFunc => { + const throttled = throttle(fn, wait, options) + + return useCallback(throttled, dependencies) +} + +export default useThrottledFn diff --git a/src/useTimeout.js b/src/useTimeout.js deleted file mode 100644 index 5d72551c..00000000 --- a/src/useTimeout.js +++ /dev/null @@ -1,52 +0,0 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; - -const defaultOptions = { - cancelOnUnmount: true, -}; - -/** - * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then delays the - * execution of the given function by the defined time. - */ -const useTimeout = (fn, milliseconds, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}) }; - const timeout = useRef(); - const callback = useRef(fn); - const [isCleared, setIsCleared] = useState(false); - - // the clear method - const clear = useCallback(() => { - if (timeout.current) { - clearTimeout(timeout.current); - setIsCleared(true); - } - }, []); - - // if the provided function changes, change its reference - useEffect(() => { - if (typeof fn === 'function') { - callback.current = fn; - } - }, [fn]); - - // when the milliseconds change, reset the timeout - useEffect(() => { - if (typeof milliseconds === 'number') { - timeout.current = setTimeout(() => { - callback.current(); - }, milliseconds); - } - return clear; - }, [milliseconds]); - - // when component unmount clear the timeout - useEffect(() => () => { - if (opts.cancelOnUnmount) { - clear(); - } - }, []); - - return [isCleared, clear]; -}; - -export default useTimeout; diff --git a/src/useTimeout.ts b/src/useTimeout.ts new file mode 100644 index 00000000..1ad0c7b6 --- /dev/null +++ b/src/useTimeout.ts @@ -0,0 +1,57 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +export type UseTimeoutOptions = { + cancelOnUnmount?: boolean, +} + +const defaultOptions = { + cancelOnUnmount: true, +} + +/** + * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then delays the + * execution of the given function by the defined time. + */ +const useTimeout = any> + (fn: T, milliseconds: number, options: UseTimeoutOptions = defaultOptions): [boolean, () => void] => { + const opts = { ...defaultOptions, ...(options || {}) } + const timeout = useRef() + const callback = useRef(fn) + const [isCleared, setIsCleared] = useState(false) + + // the clear method + const clear = useCallback(() => { + if (timeout.current) { + clearTimeout(timeout.current) + setIsCleared(true) + } + }, []) + + // if the provided function changes, change its reference + useEffect(() => { + if (typeof fn === 'function') { + callback.current = fn + } + }, [fn]) + + // when the milliseconds change, reset the timeout + useEffect(() => { + if (typeof milliseconds === 'number') { + timeout.current = setTimeout(() => { + callback.current() + }, milliseconds) + } + return clear + }, [milliseconds]) + + // when component unmount clear the timeout + useEffect(() => () => { + if (opts.cancelOnUnmount) { + clear() + } + }, []) + + return [isCleared, clear] +} + +export default useTimeout diff --git a/src/useTouch.js b/src/useTouch.js deleted file mode 100644 index 093d17a5..00000000 --- a/src/useTouch.js +++ /dev/null @@ -1,16 +0,0 @@ -import useTouchEvents from './useTouchEvents'; -import useTouchState from './useTouchState'; - -/** - * Returns an array where the first item is the touch state from the `useTouchState` hook and the second item - * is the object of callback setters from the `useTouchEvents` hook. - * It is intended as a shortcut to those hooks. - */ -const useTouch = (ref = null) => { - const state = useTouchState(ref); - const events = useTouchEvents(ref); - - return [state, events]; -}; - -export default useTouch; diff --git a/src/useTouch.ts b/src/useTouch.ts new file mode 100644 index 00000000..878754df --- /dev/null +++ b/src/useTouch.ts @@ -0,0 +1,17 @@ +import { MutableRefObject } from 'react' +import useTouchEvents, { TouchEventsMap } from './useTouchEvents' +import useTouchState, { TouchState } from './useTouchState' + +/** + * Returns an array where the first item is the touch state from the `useTouchState` hook and the second item + * is the object of callback setters from the `useTouchEvents` hook. + * It is intended as a shortcut to those hooks. + */ +const useTouch = (targetRef: MutableRefObject = null): [TouchState, TouchEventsMap] => { + const state = useTouchState(targetRef) + const events = useTouchEvents(targetRef) + + return [state, events] +} + +export default useTouch diff --git a/src/useTouchEvents.js b/src/useTouchEvents.js deleted file mode 100644 index 785741ba..00000000 --- a/src/useTouchEvents.js +++ /dev/null @@ -1,44 +0,0 @@ -import createHandlerSetter from './utils/createHandlerSetter'; -import createCbSetterErrorProxy from './utils/createCbSetterErrorProxy'; -import hasOwnProperty from './utils/hasOwnProperty'; -import assignEventCallbackOnMountEffect from './utils/assignEventCallbackOnMountEffect'; - -/** - * Returns a frozen object of callback setters to handle the touch events.
- * It accepts a DOM ref representing the events target.
- * If a target is not provided the events will be globally attached to the document object. - *
- * ### Shall the `useTouchEvents` callbacks replace the standard mouse handler props? - * - * **They shall not!**
- * **useTouchEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: - * a drag n drop hook.
- * Using useTouchEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll - * lose the React SyntheticEvent performance boost.
- * If you were doing something like the following: - * - */ -const useTouchEvents = (targetRef = null) => { - const [onTouchStartHandler, setOnTouchStartHandler] = createHandlerSetter(); - const [onTouchEndHandler, setOnTouchEndHandler] = createHandlerSetter(); - const [onTouchCancelHandler, setOnTouchCancelHandler] = createHandlerSetter(); - const [onTouchMoveHandler, setOnTouchMoveHandler] = createHandlerSetter(); - - if (targetRef !== null && !hasOwnProperty(targetRef, 'current')) { - return createCbSetterErrorProxy('Unable to assign any touch event to the given ref'); - } - - assignEventCallbackOnMountEffect(targetRef, onTouchStartHandler, 'touchstart'); - assignEventCallbackOnMountEffect(targetRef, onTouchEndHandler, 'touchend'); - assignEventCallbackOnMountEffect(targetRef, onTouchCancelHandler, 'touchcancel'); - assignEventCallbackOnMountEffect(targetRef, onTouchMoveHandler, 'touchmove'); - - return Object.freeze({ - onTouchStart: setOnTouchStartHandler, - onTouchEnd: setOnTouchEndHandler, - onTouchCancel: setOnTouchCancelHandler, - onTouchMove: setOnTouchMoveHandler, - }); -}; - -export default useTouchEvents; diff --git a/src/useTouchEvents.ts b/src/useTouchEvents.ts new file mode 100644 index 00000000..37c2e203 --- /dev/null +++ b/src/useTouchEvents.ts @@ -0,0 +1,55 @@ +import { MutableRefObject } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import createCbSetterErrorProxy from './shared/createCbSetterErrorProxy' +import safeHasOwnProperty from './shared/safeHasOwnProperty' +import assignEventOnMount from './shared/assignEventOnMount' +import { CallbackSetter } from './shared/types' + +export type TouchCallback = (touchEvent: TouchEvent) => any; + +export type TouchEventsMap = { + onTouchStart: CallbackSetter, + onTouchEnd: CallbackSetter, + onTouchCancel: CallbackSetter, + onTouchMove: CallbackSetter, +} + +/** + * Returns a frozen object of callback setters to handle the touch events.
+ * It accepts a DOM ref representing the events target.
+ * If a target is not provided the events will be globally attached to the document object. + *
+ * ### Shall the `useTouchEvents` callbacks replace the standard mouse handler props? + * + * **They shall not!**
+ * **useTouchEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: + * a drag n drop hook.
+ * Using useTouchEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll + * lose the React SyntheticEvent performance boost.
+ * If you were doing something like the following: + * + */ +const useTouchEvents = (targetRef: MutableRefObject = null): TouchEventsMap => { + const [onTouchStartHandler, setOnTouchStartHandler] = useHandlerSetterRef() + const [onTouchEndHandler, setOnTouchEndHandler] = useHandlerSetterRef() + const [onTouchCancelHandler, setOnTouchCancelHandler] = useHandlerSetterRef() + const [onTouchMoveHandler, setOnTouchMoveHandler] = useHandlerSetterRef() + + if (targetRef !== null && !safeHasOwnProperty(targetRef, 'current')) { + return createCbSetterErrorProxy('Unable to assign any touch event to the given ref') + } + + assignEventOnMount(targetRef, onTouchStartHandler, 'touchstart') + assignEventOnMount(targetRef, onTouchEndHandler, 'touchend') + assignEventOnMount(targetRef, onTouchCancelHandler, 'touchcancel') + assignEventOnMount(targetRef, onTouchMoveHandler, 'touchmove') + + return Object.freeze({ + onTouchStart: setOnTouchStartHandler, + onTouchEnd: setOnTouchEndHandler, + onTouchCancel: setOnTouchCancelHandler, + onTouchMove: setOnTouchMoveHandler, + }) +} + +export default useTouchEvents diff --git a/src/useTouchState.js b/src/useTouchState.js deleted file mode 100644 index fa0bfe2b..00000000 --- a/src/useTouchState.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useState } from 'react'; -import useTouchEvents from './useTouchEvents'; - -/** - * Returns the current touches from the touch move event. - * It possibly accepts a DOM ref representing the mouse target. - * If a target is not provided the state will be caught globally. - */ -const useTouchState = (ref = null) => { - const [state, setState] = useState({ length: 0 }); - const { onTouchMove } = useTouchEvents(ref); - - onTouchMove((event) => { - setState(event.touches); - }); - - return state; -}; - -export default useTouchState; diff --git a/src/useTouchState.ts b/src/useTouchState.ts new file mode 100644 index 00000000..3e65aaea --- /dev/null +++ b/src/useTouchState.ts @@ -0,0 +1,22 @@ +import { MutableRefObject, useState } from 'react' +import useTouchEvents from './useTouchEvents' + +export type TouchState = TouchList | { length: 0 } + +/** + * Returns the current touches from the touch move event. + * It possibly accepts a DOM ref representing the mouse target. + * If a target is not provided the state will be caught globally. + */ +const useTouchState = (targetRef: MutableRefObject = null): TouchState => { + const [state, setState] = useState({ length: 0 }) + const { onTouchMove } = useTouchEvents(targetRef) + + onTouchMove((event: TouchEvent) => { + setState(event.touches) + }) + + return state +} + +export default useTouchState diff --git a/src/useValidatedState.js b/src/useValidatedState.js deleted file mode 100644 index cb1f439e..00000000 --- a/src/useValidatedState.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useState, useCallback, useRef } from 'react'; - -/** - * Returns a state that changes only if the next value pass its validator - */ -const useValidatedState = (validator, initialValue) => { - const [state, setState] = useState(initialValue); - const validation = useRef({ changed: false }); - - const onChange = useCallback((nextValue) => { - setState(nextValue); - validation.current = { changed: true, valid: validator(nextValue) }; - }, [validator]); - - return [state, onChange, validation.current]; -}; - -export default useValidatedState; diff --git a/src/useValidatedState.ts b/src/useValidatedState.ts new file mode 100644 index 00000000..296616bc --- /dev/null +++ b/src/useValidatedState.ts @@ -0,0 +1,19 @@ +import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react' + +/** + * Returns a state that changes only if the next value pass its validator + */ +const useValidatedState = boolean = (value: T) => boolean> + (validator: V, initialValue?: T): [T, Dispatch>, { changed: boolean, valid?: boolean }] => { + const [state, setState] = useState(initialValue) + const validation = useRef<{ changed: boolean, valid?: boolean }>({ changed: false }) + + const onChange = useCallback((nextValue) => { + setState(nextValue) + validation.current = { changed: true, valid: validator(nextValue) } + }, [validator]) + + return [state, onChange, validation.current] +} + +export default useValidatedState diff --git a/src/useValueHistory.js b/src/useValueHistory.js deleted file mode 100644 index 28ba0989..00000000 --- a/src/useValueHistory.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useRef, useEffect } from 'react'; - -const distinctValues = (value, current, array) => array.indexOf(value) === current; - -/** - * Accepts a variable (possibly a prop or a state) and returns its history (changes through updates). - */ -const useValueHistory = (value, distinct = false) => { - const history = useRef([]); - - // quite simple - useEffect(() => { - history.current.push(value); - - if (distinct) { - history.current.filter(distinctValues); - } - }, [value]); - - return history.current; -}; - -export default useValueHistory; diff --git a/src/useValueHistory.ts b/src/useValueHistory.ts new file mode 100644 index 00000000..00d3cae7 --- /dev/null +++ b/src/useValueHistory.ts @@ -0,0 +1,23 @@ +import { useEffect, useRef } from 'react' + +const distinctValues = (value: T, current: number, array: T[]): boolean => array.indexOf(value) === current + +/** + * Accepts a variable (possibly a prop or a state) and returns its history (changes through updates). + */ +const useValueHistory = (value: T, distinct = false): T[] => { + const history = useRef([]) + + // quite simple + useEffect(() => { + history.current.push(value) + + if (distinct) { + history.current.filter(distinctValues) + } + }, [value]) + + return history.current +} + +export default useValueHistory diff --git a/src/useVerticalSwipe.js b/src/useVerticalSwipe.js deleted file mode 100644 index d17244b6..00000000 --- a/src/useVerticalSwipe.js +++ /dev/null @@ -1,20 +0,0 @@ -import useSwipe from './useSwipe'; - -const defaultOptions = { - threshold: 15, - preventDefault: true, -}; - -/** - * A shortcut to useSwipe (with vertical options) - * @param ref - * @param options - * @return {{alpha: number, count: number, swiping: boolean, direction: null}} - */ -const useVerticalSwipe = (ref = null, options = defaultOptions) => { - const opts = { ...defaultOptions, ...(options || {}), ...{ direction: 'vertical' } }; - - return useSwipe(ref, opts); -}; - -export default useVerticalSwipe; diff --git a/src/useVerticalSwipe.ts b/src/useVerticalSwipe.ts new file mode 100644 index 00000000..bb8db406 --- /dev/null +++ b/src/useVerticalSwipe.ts @@ -0,0 +1,21 @@ +import { MutableRefObject } from 'react' +import useSwipe, { UseSwipeOptions } from './useSwipe' + +const defaultOptions: UseSwipeOptions = { + threshold: 15, + preventDefault: true, +} + +/** + * A shortcut to useSwipe (with vertical options) + * @param ref + * @param options + * @return {{alpha: number, count: number, swiping: boolean, direction: null}} + */ +const useVerticalSwipe = (ref: MutableRefObject = null, options: UseSwipeOptions = defaultOptions) => { + const opts: UseSwipeOptions = { ...defaultOptions, ...(options || {}), ...{ direction: 'vertical' } } + + return useSwipe(ref, opts) +} + +export default useVerticalSwipe diff --git a/src/useViewportSpy.js b/src/useViewportSpy.js deleted file mode 100644 index 102577ee..00000000 --- a/src/useViewportSpy.js +++ /dev/null @@ -1,43 +0,0 @@ -import { useLayoutEffect, useState } from 'react'; -import isClient from './utils/isClient'; -import isApiSupported from './utils/isAPISupported'; - -const defaultOptions = { - root: undefined, - rootMargin: '0px', - threshold: 0, -}; -const errorMessage = 'IntersectionObserver is not supported, this could happen both because' - + ' window.IntersectionObserver is not supported by' - + ' your current browser or you\'re using the useViewportSpy hook whilst server side rendering.'; - -/** - * Uses the IntersectionObserverMock API to tell whether the given DOM Element (from useRef) is visible within the - * viewport. - */ -const useViewportSpy = (elementRef, options = defaultOptions) => { - if (!isClient || !isApiSupported('IntersectionObserver')) { - // eslint-disable-next-line no-console - console.warn(errorMessage); - return null; - } - - const [isVisible, setIsVisible] = useState(); - - useLayoutEffect(() => { - const observer = new window.IntersectionObserver((entries) => entries.forEach((item) => { - const nextValue = item.isIntersecting; - setIsVisible(nextValue); - }), options); - - observer.observe(elementRef.current); - - return () => { - observer.disconnect(elementRef.current); - }; - }, [elementRef]); - - return isVisible; -}; - -export default useViewportSpy; diff --git a/src/useViewportSpy.ts b/src/useViewportSpy.ts new file mode 100644 index 00000000..7665cd08 --- /dev/null +++ b/src/useViewportSpy.ts @@ -0,0 +1,47 @@ +import { MutableRefObject, useLayoutEffect, useState } from 'react' +import isClient from './shared/isClient' +import isApiSupported from './shared/isAPISupported' +import isDevelopment from './shared/isDevelopment' + +const defaultOptions: IntersectionObserverInit = { + rootMargin: '0px', + threshold: 0, +} + +const errorMessage = 'IntersectionObserver is not supported, this could happen both because' + + ' window.IntersectionObserver is not supported by' + + ' your current browser or you\'re using the useViewportSpy hook whilst server side rendering.' + + ' This message is displayed only in development mode' + +/** + * Uses the IntersectionObserverMock API to tell whether the given DOM Element (from useRef) is visible within the + * viewport. + */ +const useViewportSpy = (elementRef: MutableRefObject, options: IntersectionObserverInit = defaultOptions) => { + if (!isClient || !isApiSupported('IntersectionObserver')) { + if (isDevelopment) { + // eslint-disable-next-line no-console + console.warn(errorMessage) + } + return null + } + + const [isVisible, setIsVisible] = useState() + + useLayoutEffect(() => { + const observer = new window.IntersectionObserver((entries) => entries.forEach((item) => { + const nextValue = item.isIntersecting + setIsVisible(nextValue) + }), options) + + observer.observe(elementRef.current) + + return () => { + observer.disconnect() + } + }, [elementRef]) + + return isVisible +} + +export default useViewportSpy diff --git a/src/useWillUnmount.js b/src/useWillUnmount.js deleted file mode 100644 index 1f6a2271..00000000 --- a/src/useWillUnmount.js +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react'; -import createHandlerSetter from './utils/createHandlerSetter'; - -/** - * Returns a callback setter for a callback to be performed when the component will unmount. - */ -const useWillUnmount = (handler) => { - const [onUnmountHandler, setOnUnmount] = createHandlerSetter(handler); - - useEffect(() => () => { - if (onUnmountHandler.current) { - onUnmountHandler.current(); - } - }, []); - - return setOnUnmount; -}; - -export default useWillUnmount; diff --git a/src/useWillUnmount.ts b/src/useWillUnmount.ts new file mode 100644 index 00000000..457e82a8 --- /dev/null +++ b/src/useWillUnmount.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react' +import useHandlerSetterRef from './shared/useHandlerSetterRef' +import { Noop } from './shared/types' + +/** + * Returns a callback setter for a callback to be performed when the component will unmount. + */ +const useWillUnmount = void = Noop>(callback?: T) => { + const [handler, setHandler] = useHandlerSetterRef(callback) + + useEffect(() => () => { + if (handler) { + handler.current() + } + }, []) + + return setHandler +} + +export default useWillUnmount diff --git a/src/useWindowResize.js b/src/useWindowResize.js deleted file mode 100644 index 7933d561..00000000 --- a/src/useWindowResize.js +++ /dev/null @@ -1,8 +0,0 @@ -import useGlobalEvent from './useGlobalEvent'; - -/** - * Returns a function that accepts a callback to be performed when the window resize. - */ -const useWindowResize = (handler) => useGlobalEvent('resize', null, handler); - -export default useWindowResize; diff --git a/src/useWindowResize.ts b/src/useWindowResize.ts new file mode 100644 index 00000000..609dc4c2 --- /dev/null +++ b/src/useWindowResize.ts @@ -0,0 +1,8 @@ +import useGlobalEvent from './useGlobalEvent' + +/** + * Returns a function that accepts a callback to be performed when the window resize. + */ +const useWindowResize = void>(handler?: T) => useGlobalEvent('resize', handler) + +export default useWindowResize diff --git a/src/useWindowScroll.js b/src/useWindowScroll.js deleted file mode 100644 index c7886f5d..00000000 --- a/src/useWindowScroll.js +++ /dev/null @@ -1,8 +0,0 @@ -import useGlobalEvent from './useGlobalEvent'; - -/** - * Returns a function that accepts a callback to be performed when the window scrolls. - */ -const useWindowScroll = (handler) => useGlobalEvent('scroll', null, handler); - -export default useWindowScroll; diff --git a/src/useWindowScroll.ts b/src/useWindowScroll.ts new file mode 100644 index 00000000..e6c59772 --- /dev/null +++ b/src/useWindowScroll.ts @@ -0,0 +1,8 @@ +import useGlobalEvent from './useGlobalEvent' + +/** + * Returns a function that accepts a callback to be performed when the window scrolls. + */ +const useWindowScroll = void>(handler?: T) => useGlobalEvent('scroll', handler) + +export default useWindowScroll diff --git a/src/utils/assignEventCallbackOnMountEffect.js b/src/utils/assignEventCallbackOnMountEffect.js deleted file mode 100644 index 3fb7e441..00000000 --- a/src/utils/assignEventCallbackOnMountEffect.js +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react'; - -const assignEventCallbackOnMountEffect = (targetRef, handlerRef, eventName) => { - useEffect(() => { - const cb = (mouseEvent) => { - if (handlerRef.current) { - handlerRef.current(mouseEvent); - } - }; - let target; - - if (targetRef !== null && !!targetRef.current) { - target = targetRef.current; - } - - if (targetRef === null) { - target = document; - } - - if (target && target.addEventListener) { - target.addEventListener(eventName, cb); - } - - return () => { - if (target && target.removeEventListener) { - target.removeEventListener(eventName, cb); - } - }; - }, []); -}; - -export default assignEventCallbackOnMountEffect; diff --git a/src/utils/createCbSetterErrorProxy.js b/src/utils/createCbSetterErrorProxy.js deleted file mode 100644 index 82a0d80f..00000000 --- a/src/utils/createCbSetterErrorProxy.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Create setter error proxy - */ -const createCbSetterErrorProxy = (errorMessage) => new Proxy(Object.create(null), { - get: (target, property) => { - if (property && typeof property === 'string' && property.slice(0, 2) === 'on') { - return () => { - throw new Error(errorMessage); - }; - } - - return { error: errorMessage }; - }, -}); - -export default createCbSetterErrorProxy; diff --git a/src/utils/createHandlerSetter.js b/src/utils/createHandlerSetter.js deleted file mode 100644 index 797cb0c8..00000000 --- a/src/utils/createHandlerSetter.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useCallback, useRef } from 'react'; - -/** - * Returns an array where the first item is the [ref](https://reactjs.org/docs/hooks-reference.html#useref) to a - * callback function and the second one is setter for that function.

- * - * Although it function looks quite similar to the [useState](https://reactjs.org/docs/hooks-reference.html#usestate), - * hook, in this case the setter just makes sure the given callback is indeed a new function.

- * **Setting a callback ref does not force your component to re-render.**

- * - * `createHandlerSetter` is useful when abstracting other hooks to possibly implement handlers setters. - */ -const createHandlerSetter = (handlerValue) => { - const handlerRef = useRef(handlerValue); - - const setHandler = useCallback((nextCallback) => { - if (typeof nextCallback !== 'function') { - throw new Error('the argument supplied to the \'setHandler\' function should be of type function'); - } - - handlerRef.current = nextCallback; - }, [handlerRef]); - - return [handlerRef, setHandler]; -}; - -export default createHandlerSetter; diff --git a/src/utils/geolocationStandardOptions.js b/src/utils/geolocationStandardOptions.js deleted file mode 100644 index a66ab8be..00000000 --- a/src/utils/geolocationStandardOptions.js +++ /dev/null @@ -1,7 +0,0 @@ -const geoStandardOptions = Object.create(null); - -geoStandardOptions.enableHighAccuracy = false; -geoStandardOptions.timeout = 0xFFFFFFFF; -geoStandardOptions.maximumAge = 0; - -export default Object.freeze(geoStandardOptions); diff --git a/src/utils/hasOwnProperty.js b/src/utils/hasOwnProperty.js deleted file mode 100644 index 308db02a..00000000 --- a/src/utils/hasOwnProperty.js +++ /dev/null @@ -1,3 +0,0 @@ -const hasOwnProperty = (obj, prop) => (obj ? Object.prototype.hasOwnProperty.call(obj, prop) : false); - -export default hasOwnProperty; diff --git a/src/utils/isAPISupported.js b/src/utils/isAPISupported.js deleted file mode 100644 index 0321aedc..00000000 --- a/src/utils/isAPISupported.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Exports a boolean value reporting whether the given API is supported or not - */ -const isApiSupported = (api) => (typeof window !== 'undefined' ? api in window : false); - -export default isApiSupported; diff --git a/src/utils/makePositionObject.js b/src/utils/makePositionObject.js deleted file mode 100644 index 70c3acdc..00000000 --- a/src/utils/makePositionObject.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Given a position object returns only its properties - */ -const makePositionObj = (position) => (!position ? null : ({ - timestamp: position.timestamp, - coords: { - latitude: position.coords.latitude, - longitude: position.coords.longitude, - altitude: position.coords.altitude, - accuracy: position.coords.accuracy, - altitudeAccuracy: position.coords.altitudeAccuracy, - heading: position.coords.heading, - speed: position.coords.speed, - }, -})); - -export default makePositionObj; diff --git a/src/utils/safelyParseJson.js b/src/utils/safelyParseJson.js deleted file mode 100644 index 95d86fb9..00000000 --- a/src/utils/safelyParseJson.js +++ /dev/null @@ -1,9 +0,0 @@ -const safelyParseJson = (parseString) => { - try { - return JSON.parse(parseString); - } catch (e) { - return null; - } -}; - -export default safelyParseJson; diff --git a/src/utils/swipeUtils.js b/src/utils/swipeUtils.js deleted file mode 100644 index 13068865..00000000 --- a/src/utils/swipeUtils.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Takes a mouse or a touch events and returns clientX and clientY values - * @param event - * @return {[undefined, undefined]} - */ -export const getPointerCoordinates = (event) => { - if (event.touches) { - const { clientX, clientY } = event.touches[0]; - return [clientX, clientY]; - } - - const { clientX, clientY } = event; - - return [clientX, clientY]; -}; - -export const getHorizontalDirection = (alpha) => (alpha < 0 ? 'right' : 'left'); - -export const getVerticalDirection = (alpha) => (alpha < 0 ? 'down' : 'up'); - -export const getDirection = (currentPoint, startingPoint, alpha) => { - const alphaX = startingPoint[0] - currentPoint[0]; - const alphaY = startingPoint[1] - currentPoint[1]; - if (Math.abs(alphaX) > Math.abs(alphaY)) { - return getHorizontalDirection(alpha[0]); - } - - return getVerticalDirection(alpha[1]); -}; diff --git a/styleguide.config.js b/styleguide.config.js new file mode 100644 index 00000000..7816c5c1 --- /dev/null +++ b/styleguide.config.js @@ -0,0 +1,70 @@ +const glob = require('glob') +const path = require('path') +const theme = require('./docs/utils/_styleguidist.theme.js') + +const srcPath = path.resolve(__dirname, 'src') +const docsPath = path.resolve(__dirname, 'docs') + +const getHooksDocFiles = () => glob.sync(path.join(__dirname, 'docs', '[use]*.md')).map((filePath) => { + const [filename] = filePath.match(/use[a-zA-Z]*/, 'gm') + + return ({ + name: filename, + content: `./docs/${filename}.md` + }) +}) + +module.exports = { + title: 'Beautiful React Hooks docs', + pagePerSection: true, + exampleMode: 'expand', + skipComponentsWithoutExample: true, + styleguideDir: 'dist-ghpages', + ribbon: { + url: 'https://github.com/beautifulinteractions/beautiful-react-hooks', + text: 'Fork me on GitHub' + }, + sections: [ + { name: 'Introduction', content: './docs/Introduction.md', sectionDepth: 1 }, + { name: 'Installation', content: './docs/Installation.md', sectionDepth: 1 }, + ...getHooksDocFiles() + ], + require: [path.join(docsPath, 'utils', '_setup.js'), path.join(docsPath, 'utils', '_custom.css')], + webpackConfig() { + return { + resolve: { + alias: { 'beautiful-react-hooks': srcPath } + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'] + }, + { + test: /\.png$/, + loader: 'url-loader' + } + ] + } + } + }, + styleguideComponents: { + LogoRenderer: path.join(docsPath, 'utils', '_CustomLogo'), + PathlineRenderer: path.join(docsPath, 'utils', '_EmptyComponent'), + ToolbarButtonRenderer: path.join(docsPath, 'utils', '_EmptyComponent') + // TableOfContentsRenderer: path.join(docsPath, 'CustomSidebar'), + // ComponentsListRenderer: path.join(docsPath, 'CustomComponentListRenderer'), + }, + ...theme +} diff --git a/test/createCbSetterErrorProxy.spec.js b/test/createCbSetterErrorProxy.spec.js index 0e3ff92e..eb84c063 100644 --- a/test/createCbSetterErrorProxy.spec.js +++ b/test/createCbSetterErrorProxy.spec.js @@ -1,4 +1,4 @@ -import createCbSetterErrorProxy from '../dist/utils/createCbSetterErrorProxy'; +import createCbSetterErrorProxy from '../dist/shared/createCbSetterErrorProxy'; describe('createCbSetterErrorProxy utility', () => { it('should be a function', () => { diff --git a/test/geolocationStandardOptions.spec.js b/test/geolocationStandardOptions.spec.js index 80611352..341fbe5a 100644 --- a/test/geolocationStandardOptions.spec.js +++ b/test/geolocationStandardOptions.spec.js @@ -1,4 +1,4 @@ -import geolocationStandardOptions from '../dist/utils/geolocationStandardOptions'; +import geolocationStandardOptions from '../dist/shared/geolocationStandardOptions'; describe('geolocationStandardOptions utility', () => { it('should be an object without any prototype', () => { diff --git a/test/isAPISupported.spec.js b/test/isAPISupported.spec.js index c9be034e..2587d3af 100644 --- a/test/isAPISupported.spec.js +++ b/test/isAPISupported.spec.js @@ -1,4 +1,4 @@ -import isAPISupported from '../dist/utils/isAPISupported'; +import isAPISupported from '../dist/shared/isAPISupported'; describe('isAPISupported utility', () => { it('should be a function', () => { diff --git a/test/isClient.spec.js b/test/isClient.spec.js index 232662f8..f3b3f1e6 100644 --- a/test/isClient.spec.js +++ b/test/isClient.spec.js @@ -1,4 +1,4 @@ -import isClient from '../dist/utils/isClient'; +import isClient from '../dist/shared/isClient'; describe('isClient utility', () => { it('should be a boolean', () => expect(isClient).to.be.a('boolean')); diff --git a/test/isSamePosition.spec.js b/test/isSamePosition.spec.js index d2a76691..0eb16546 100644 --- a/test/isSamePosition.spec.js +++ b/test/isSamePosition.spec.js @@ -1,4 +1,4 @@ -import isSamePosition from '../dist/utils/isSamePosition'; +import isSamePosition from '../dist/shared/isSamePosition'; import { positionMock } from './mocks/GeoLocationApi.mock'; describe('isSamePosition utility', () => { diff --git a/test/makePositionObject.spec.js b/test/makePositionObject.spec.js index 68439ad2..67d374df 100644 --- a/test/makePositionObject.spec.js +++ b/test/makePositionObject.spec.js @@ -1,4 +1,4 @@ -import makePositionObject from '../dist/utils/makePositionObject'; +import makePositionObject from '../dist/shared/makePositionObject'; import { positionMock } from './mocks/GeoLocationApi.mock'; describe('makePositionObject utility', () => { diff --git a/test/hasOwnProperty.spec.js b/test/safeHasOwnProperty.spec.js similarity index 54% rename from test/hasOwnProperty.spec.js rename to test/safeHasOwnProperty.spec.js index d4a22b2a..f0303786 100644 --- a/test/hasOwnProperty.spec.js +++ b/test/safeHasOwnProperty.spec.js @@ -1,24 +1,24 @@ -import hasOwnProperty from '../dist/utils/hasOwnProperty'; +import safeHasOwnProperty from '../dist/shared/safeHasOwnProperty'; -describe('hasOwnProperty utility', () => { +describe('safeHasOwnProperty utility', () => { it('should be a function', () => { - expect(hasOwnProperty).to.be.a('function'); + expect(safeHasOwnProperty).to.be.a('function'); }); it('should return false if nothing is provided', () => { - const result = hasOwnProperty(); + const result = safeHasOwnProperty(); expect(result).to.be.false; }); it('should return true if the given object has the defined property', () => { - const result = hasOwnProperty({ foo: 'bar' }, 'foo'); + const result = safeHasOwnProperty({ foo: 'bar' }, 'foo'); expect(result).to.be.true; }); it('should return false if the given object does not have the defined property', () => { - const result = hasOwnProperty({ foo: 'bar' }, 'bar'); + const result = safeHasOwnProperty({ foo: 'bar' }, 'bar'); expect(result).to.be.false; }); diff --git a/test/createHandlerSetter.spec.js b/test/useHandlerSetter.spec.js similarity index 72% rename from test/createHandlerSetter.spec.js rename to test/useHandlerSetter.spec.js index 9c68e3eb..b574a805 100644 --- a/test/createHandlerSetter.spec.js +++ b/test/useHandlerSetter.spec.js @@ -1,22 +1,22 @@ import { renderHook, act, cleanup } from '@testing-library/react-hooks'; -import createHandlerSetter from '../dist/utils/createHandlerSetter'; +import useHandlerSetterRef from '../dist/shared/useHandlerSetterRef'; -describe('createHandlerSetter', () => { +describe('useHandlerSetterRef', () => { beforeEach(cleanup); it('should be a function', () => { - expect(createHandlerSetter).to.be.a('function'); + expect(useHandlerSetterRef).to.be.a('function'); }); it('should return an array of 2 elements', () => { - const { result } = renderHook(() => createHandlerSetter()); + const { result } = renderHook(() => useHandlerSetterRef()); expect(result.current).to.be.an.instanceOf(Array); expect(result.current.length).to.equal(2); }); it('should return the reference to a handler', () => { - const { result } = renderHook(() => createHandlerSetter()); + const { result } = renderHook(() => useHandlerSetterRef()); const [handlerRef] = result.current; expect(handlerRef.current).to.be.undefined; @@ -24,7 +24,7 @@ describe('createHandlerSetter', () => { }); it('should return a handler setter', () => { - const { result } = renderHook(() => createHandlerSetter()); + const { result } = renderHook(() => useHandlerSetterRef()); const [handlerRef, setHandlerRef] = result.current; const fooCallback = () => undefined; @@ -39,7 +39,7 @@ describe('createHandlerSetter', () => { }); it('the setter should throw when changing the handler to an invalid value', () => { - const { result } = renderHook(() => createHandlerSetter()); + const { result } = renderHook(() => useHandlerSetterRef()); const [, setHandlerRef] = result.current; const shouldThrow = () => { diff --git a/test/useStorage.spec.js b/test/useStorage.spec.js index 1b799104..679c66a5 100644 --- a/test/useStorage.spec.js +++ b/test/useStorage.spec.js @@ -1,25 +1,25 @@ import React from 'react'; import { cleanup as cleanupHooks } from '@testing-library/react-hooks'; -import useStorage from '../dist/useStorage'; +import createStorageHook from '../dist/shared/createStorageHook'; -describe('useStorage', () => { +describe('createStorageHook', () => { beforeEach(cleanupHooks); afterEach(sinon.restore); it('should be a function', () => { - expect(useStorage).to.be.a('function'); + expect(createStorageHook).to.be.a('function'); }); it('should return a function', () => { - const useLocalStorage = useStorage('local'); + const useLocalStorage = createStorageHook('local'); expect(useLocalStorage).to.be.a('function'); }); it('should warn when an invalid storage name is provided', () => { const warnSpy = sinon.spy(console, 'warn'); - useStorage('foo'); + createStorageHook('foo'); expect(warnSpy.called).to.be.true; }); diff --git a/test/useSwipe.spec.js b/test/useSwipe.spec.js index 2d8faab5..55acc470 100644 --- a/test/useSwipe.spec.js +++ b/test/useSwipe.spec.js @@ -1,7 +1,7 @@ import { cleanup, renderHook } from '@testing-library/react-hooks'; import useSwipe from '../dist/useSwipe'; -import useHorizontalSwipe from '../src/useHorizontalSwipe'; -import useVerticalSwipe from '../src/useVerticalSwipe'; +import useHorizontalSwipe from '../dist/useHorizontalSwipe'; +import useVerticalSwipe from '../dist/useVerticalSwipe'; describe('useSwipe', () => { beforeEach(cleanup); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 00000000..ebb08d32 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/" + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 00000000..88049284 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "target": "es6", + "outDir": "./dist/esm" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1ee0eb3e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["./src/**/*.ts"], + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "target": "es5", + "jsx": "react", + "allowJs": false, + "declaration": true, + "moduleResolution": "node", + "esModuleInterop": true, + "rootDir": "./src", + "outDir": "./dist" + } +}