From 665d999cb6e543a988e675d50fa7fdcaf7ce3027 Mon Sep 17 00:00:00 2001
From: Shiny <contact@shinychang.net>
Date: Sat, 30 May 2020 00:40:10 +0800
Subject: [PATCH] Add useOnline and useSize

---
 .storybook/main.js            |  4 ++--
 package.json                  |  3 ++-
 src/hooks/useFPS.js           |  5 +++--
 src/hooks/useOnline.js        | 18 ++++++++++++++++++
 src/hooks/useSize.js          | 30 ++++++++++++++++++++++++++++++
 src/index.js                  |  4 +++-
 stories/useFPS.stories.js     |  2 --
 stories/useOnline.stories.js  | 11 +++++++++++
 stories/useSize.stories.js    | 18 ++++++++++++++++++
 tests/hooks/useOnline.test.js | 28 ++++++++++++++++++++++++++++
 yarn.lock                     | 35 +++++++++++++++++++++++++++++++++++
 11 files changed, 150 insertions(+), 8 deletions(-)
 create mode 100644 src/hooks/useOnline.js
 create mode 100644 src/hooks/useSize.js
 create mode 100644 stories/useOnline.stories.js
 create mode 100644 stories/useSize.stories.js
 create mode 100644 tests/hooks/useOnline.test.js

diff --git a/.storybook/main.js b/.storybook/main.js
index 3902cf2..c1f6a60 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -1,9 +1,9 @@
 module.exports = {
   stories: ['../stories/**/*.stories.js'],
-  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
+  addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-storysource'],
   webpackFinal: async config => {
     // do mutation to the config
 
     return config;
-  },
+  }
 };
diff --git a/package.json b/package.json
index 48ce2e2..90b35e8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@shinychang/hooks",
-  "version": "0.1.3",
+  "version": "0.2.0",
   "module": "src/index.js",
   "main": "lib/index.js",
   "repository": "https://github.com/ShinyChang/hooks.git",
@@ -25,6 +25,7 @@
     "@babel/preset-react": "^7.10.1",
     "@storybook/addon-actions": "^5.3.19",
     "@storybook/addon-links": "^5.3.19",
+    "@storybook/addon-storysource": "^5.3.19",
     "@storybook/addons": "^5.3.19",
     "@storybook/react": "^5.3.19",
     "babel-loader": "^8.1.0",
diff --git a/src/hooks/useFPS.js b/src/hooks/useFPS.js
index 1f57894..1a6becf 100644
--- a/src/hooks/useFPS.js
+++ b/src/hooks/useFPS.js
@@ -10,8 +10,9 @@ const useFPS = (maxDisplayFrameRate = 60) => {
     const frame = () => {
       const now = performance.now();
       if (lastTime + 1000 <= now) {
-        if (mounted && count !== fps) {
-          setFPS(Math.min(count, maxDisplayFrameRate));
+        const displayFps = Math.min(count, maxDisplayFrameRate);
+        if (mounted && displayFps !== fps) {
+          setFPS(displayFps);
         }
         lastTime = now;
         count = 0;
diff --git a/src/hooks/useOnline.js b/src/hooks/useOnline.js
new file mode 100644
index 0000000..1b8ae00
--- /dev/null
+++ b/src/hooks/useOnline.js
@@ -0,0 +1,18 @@
+import { useEffect, useState } from 'react';
+
+const useOnline = () => {
+  const [online, setOnline] = useState(navigator.onLine);
+  useEffect(() => {
+    const onOnline = () => setOnline(true);
+    const onOffline = () => setOnline(false);
+    window.addEventListener('online', onOnline);
+    window.addEventListener('offline', onOffline);
+    return () => {
+      window.removeEventListener('online', onOnline);
+      window.removeEventListener('offline', onOffline);
+    };
+  });
+  return online;
+};
+
+export default useOnline;
diff --git a/src/hooks/useSize.js b/src/hooks/useSize.js
new file mode 100644
index 0000000..06fc0b2
--- /dev/null
+++ b/src/hooks/useSize.js
@@ -0,0 +1,30 @@
+import { useEffect, useState } from 'react';
+
+const useSize = ref => {
+  const [size, setSize] = useState(() => {
+    if (!ref.current) {
+      return [0, 0];
+    }
+    const { width, height } = ref.current.getBoundingClientRect();
+    return [width, height];
+  });
+
+  useEffect(() => {
+    const resizeObserver = new ResizeObserver(entries => {
+      const { width, height } = entries[0].contentRect;
+      setSize([width, height]);
+    });
+    if (ref.current) {
+      resizeObserver.observe(ref.current);
+    }
+    return () => {
+      if (ref.current) {
+        resizeObserver.unobserve(ref.current);
+      }
+    };
+  }, [ref.current]);
+
+  return size;
+};
+
+export default useSize;
diff --git a/src/index.js b/src/index.js
index 4bb636b..7ba8707 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,5 @@
 import useFPS from './hooks/useFPS';
+import useOnline from './hooks/useOnline';
+import useSize from './hooks/useSize';
 
-export { useFPS };
+export { useFPS, useOnline, useSize };
diff --git a/stories/useFPS.stories.js b/stories/useFPS.stories.js
index 2e0aa38..37cc9d6 100644
--- a/stories/useFPS.stories.js
+++ b/stories/useFPS.stories.js
@@ -1,5 +1,3 @@
-import React from 'react';
-
 import { useFPS } from '../src';
 
 export default {
diff --git a/stories/useOnline.stories.js b/stories/useOnline.stories.js
new file mode 100644
index 0000000..06b25d6
--- /dev/null
+++ b/stories/useOnline.stories.js
@@ -0,0 +1,11 @@
+import { useOnline } from '../src';
+
+export default {
+  title: 'useOnline',
+  component: useOnline
+};
+
+export const Basic = () => {
+  const online = useOnline();
+  return `Online: ${online}`;
+};
diff --git a/stories/useSize.stories.js b/stories/useSize.stories.js
new file mode 100644
index 0000000..0b31a9d
--- /dev/null
+++ b/stories/useSize.stories.js
@@ -0,0 +1,18 @@
+import React, { useRef } from 'react';
+import { useSize } from '../src';
+
+export default {
+  title: 'useSize',
+  component: useSize
+};
+
+export const Basic = () => {
+  const ref = useRef(null);
+  const size = useSize(ref);
+  const noop = () => {};
+  return (
+    <div>
+      <textarea style={{ resize: 'both', overflow: 'auto' }} ref={ref} value={size} onChange={noop} />
+    </div>
+  );
+};
diff --git a/tests/hooks/useOnline.test.js b/tests/hooks/useOnline.test.js
new file mode 100644
index 0000000..1173bc1
--- /dev/null
+++ b/tests/hooks/useOnline.test.js
@@ -0,0 +1,28 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+
+import { useOnline } from '../../src';
+
+jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(false);
+
+test('default online is navigator.onLine', () => {
+  const { result } = renderHook(() => useOnline());
+  expect(result.current).toBe(false);
+});
+
+test('change online when online/offline event fired', () => {
+  const map = {};
+  window.addEventListener = jest.fn((name, cb) => {
+    map[name] = cb;
+  });
+  const { result } = renderHook(() => useOnline());
+
+  act(() => {
+    map['online']();
+  });
+  expect(result.current).toBe(true);
+
+  act(() => {
+    map['offline']();
+  });
+  expect(result.current).toBe(false);
+});
diff --git a/yarn.lock b/yarn.lock
index 20542f0..f2e835f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1500,6 +1500,25 @@
     qs "^6.6.0"
     ts-dedent "^1.1.0"
 
+"@storybook/addon-storysource@^5.3.19":
+  version "5.3.19"
+  resolved "https://registry.yarnpkg.com/@storybook/addon-storysource/-/addon-storysource-5.3.19.tgz#ae693e88db5d220cb256a9ef4a2366c300e8d88c"
+  integrity sha512-W7mIAHuxYT+b1huaHCHLkBAh2MbeWmF8CxeBCFiOgZaYYQUTDEh018HJF8u2AqiWSouRhcfzhTnGxOo0hNRBgw==
+  dependencies:
+    "@storybook/addons" "5.3.19"
+    "@storybook/components" "5.3.19"
+    "@storybook/router" "5.3.19"
+    "@storybook/source-loader" "5.3.19"
+    "@storybook/theming" "5.3.19"
+    core-js "^3.0.1"
+    estraverse "^4.2.0"
+    loader-utils "^1.2.3"
+    prettier "^1.16.4"
+    prop-types "^15.7.2"
+    react-syntax-highlighter "^11.0.2"
+    regenerator-runtime "^0.13.3"
+    util-deprecate "^1.0.2"
+
 "@storybook/addons@5.3.19", "@storybook/addons@^5.3.19":
   version "5.3.19"
   resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4"
@@ -1805,6 +1824,22 @@
     qs "^6.6.0"
     util-deprecate "^1.0.2"
 
+"@storybook/source-loader@5.3.19":
+  version "5.3.19"
+  resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-5.3.19.tgz#ff0a00731c24c61721d8b9d84152f8542913a3b7"
+  integrity sha512-srSZRPgEOUse8nRVnlazweB2QGp63mPqM0uofg8zYARyaYSOzkC155ymdeiHsmiBTS3X3I0FQE4+KnwiH7iLtw==
+  dependencies:
+    "@storybook/addons" "5.3.19"
+    "@storybook/client-logger" "5.3.19"
+    "@storybook/csf" "0.0.1"
+    core-js "^3.0.1"
+    estraverse "^4.2.0"
+    global "^4.3.2"
+    loader-utils "^1.2.3"
+    prettier "^1.16.4"
+    prop-types "^15.7.2"
+    regenerator-runtime "^0.13.3"
+
 "@storybook/theming@5.3.19":
   version "5.3.19"
   resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.3.19.tgz#177d9819bd64f7a1a6ea2f1920ffa5baf9a5f467"