Repository: laboratoriobridge/bold
Pull request #424 (2019-07-03 13:33:15)

Button loading fix


Patch:
diff --git a/package.json b/package.json
index b8e34c446..b7f838d64 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
     "@storybook/react": "5.1.8",
     "@svgr/cli": "4.3.0",
     "@testing-library/react": "8.0.1",
+    "@testing-library/react-hooks": "1.1.0",
     "@types/classnames": "2.2.8",
     "@types/highlight.js": "9.12.3",
     "@types/hoist-non-react-statics": "3.3.1",
@@ -123,7 +124,6 @@
     "react-dom": "16.8.6",
     "react-final-form": "6.2.1",
     "react-ga": "2.5.7",
-    "react-hooks-testing-library": "0.5.1",
     "react-test-renderer": "16.8.6",
     "remark-highlight.js": "5.1.1",
     "remark-slug": "5.1.2",
diff --git a/site/pages/components/button/ButtonDanger.demo.tsx b/site/pages/components/button/ButtonDanger.demo.tsx
deleted file mode 100644
index ee33c1bde..000000000
--- a/site/pages/components/button/ButtonDanger.demo.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Button, HFlow, Icon, Text } from '../../../../lib'
-
-function ButtonDanger() {
-  return (
-    <HFlow alignItems='flex-end'>
-      <Button kind='danger' size='large'>
-        <Icon icon='trashOutline' style={{ marginRight: '0.5rem' }} />
-        <Text color='inherit'>Delete</Text>
-      </Button>
-      <Button kind='danger' size='large' disabled>
-        Delete
-      </Button>
-      <Button kind='danger' skin='outline' size='small'>
-        Delete
-      </Button>
-      <Button kind='danger' skin='outline' size='small' disabled>
-        <Icon icon='trashOutline' style={{ marginRight: '0.5rem' }} />
-        <Text color='inherit'>Delete</Text>
-      </Button>
-    </HFlow>
-  )
-}
-
-export default ButtonDanger
diff --git a/site/pages/components/button/ButtonLoading.demo.tsx b/site/pages/components/button/ButtonLoading.demo.tsx
new file mode 100644
index 000000000..b8e10eedf
--- /dev/null
+++ b/site/pages/components/button/ButtonLoading.demo.tsx
@@ -0,0 +1,21 @@
+import { Button, HFlow } from '../../../../lib'
+
+function ButtonLoading() {
+  const handleClick = () => {
+    // Simulate a delayed function
+    return new Promise(resolve => {
+      setTimeout(() => resolve(), 2000)
+    })
+  }
+
+  return (
+    <HFlow alignItems='flex-end'>
+      <Button loading>Always loading</Button>
+      <Button onClick={handleClick} kind='primary'>
+        Click me
+      </Button>
+    </HFlow>
+  )
+}
+
+export default ButtonLoading
diff --git a/site/pages/components/button/ButtonSizes.demo.tsx b/site/pages/components/button/ButtonSizes.demo.tsx
index a5dea55d5..e9d829cca 100644
--- a/site/pages/components/button/ButtonSizes.demo.tsx
+++ b/site/pages/components/button/ButtonSizes.demo.tsx
@@ -1,18 +1,23 @@
-import { Button, HFlow } from '../../../../lib'
+import { Button, HFlow, VFlow } from '../../../../lib'
 
 function ButtonSizes() {
   return (
-    <HFlow alignItems='flex-end'>
-      <Button kind='primary' size='large'>
-        Large
+    <VFlow>
+      <HFlow alignItems='flex-end'>
+        <Button kind='primary' size='large'>
+          Large
+        </Button>
+        <Button kind='primary' size='medium'>
+          Medium
+        </Button>
+        <Button kind='primary' size='small'>
+          Small
+        </Button>
+      </HFlow>
+      <Button kind='primary' block>
+        Block button
       </Button>
-      <Button kind='primary' size='medium'>
-        Medium
-      </Button>
-      <Button kind='primary' size='small'>
-        Small
-      </Button>
-    </HFlow>
+    </VFlow>
   )
 }
 
diff --git a/site/pages/components/button/ButtonSkins.demo.tsx b/site/pages/components/button/ButtonSkins.demo.tsx
new file mode 100644
index 000000000..fed156991
--- /dev/null
+++ b/site/pages/components/button/ButtonSkins.demo.tsx
@@ -0,0 +1,40 @@
+import { Button, HFlow, VFlow } from '../../../../lib'
+
+function ButtonSkins() {
+  return (
+    <VFlow>
+      <HFlow alignItems='flex-end'>
+        <Button kind='normal' skin='default'>
+          Normal / Default
+        </Button>
+        <Button kind='normal' skin='ghost'>
+          Normal / Ghost
+        </Button>
+      </HFlow>
+      <HFlow alignItems='flex-end'>
+        <Button kind='primary' skin='default'>
+          Primary / Default
+        </Button>
+        <Button kind='primary' skin='outline'>
+          Primary / Outline
+        </Button>
+        <Button kind='primary' skin='ghost'>
+          Primary / Ghost
+        </Button>
+      </HFlow>
+      <HFlow alignItems='flex-end'>
+        <Button kind='danger' skin='default'>
+          Danger / Default
+        </Button>
+        <Button kind='danger' skin='outline'>
+          Danger / Outline
+        </Button>
+        <Button kind='danger' skin='ghost'>
+          Danger / Ghost
+        </Button>
+      </HFlow>
+    </VFlow>
+  )
+}
+
+export default ButtonSkins
diff --git a/site/pages/components/button/index.mdx b/site/pages/components/button/index.mdx
index d503ac00a..d1cc8bea0 100644
--- a/site/pages/components/button/index.mdx
+++ b/site/pages/components/button/index.mdx
@@ -56,12 +56,16 @@ Buttons express what action will occur when the user clicks it. Buttons are used
 
 <Demo src='pages/components/button/ButtonSizes' />
 
-## Using icons with buttons
+## Button Kinds / Skins
+
+<Demo src='pages/components/button/ButtonSkins' />
+
+## Button with icons
 
 <Demo src='pages/components/button/ButtonIcons' />
 
-## Examples
+## Loading indicator
 
-### Danger
+The loading indicator can be manually toggled on/off with the `loading` prop or automatically by returning a promise to the `onClick` prop.
 
-<Demo src='pages/components/button/ButtonDanger' />
+<Demo src='pages/components/button/ButtonLoading' />
diff --git a/src/components/Button/Button/Button.tsx b/src/components/Button/Button/Button.tsx
index 25aa18d43..266633d09 100644
--- a/src/components/Button/Button/Button.tsx
+++ b/src/components/Button/Button/Button.tsx
@@ -1,5 +1,6 @@
-import React from 'react'
+import React, { MouseEvent, useState } from 'react'
 
+import { useIsMounted } from '../../../hooks/useIsMounted'
 import { ExternalStyles, useStyles } from '../../../styles'
 import { Omit } from '../../../util'
 import { ButtonBase, ButtonBaseProps } from '../ButtonBase'
@@ -15,8 +16,14 @@ export interface ButtonProps extends SkinProps, Omit<ButtonBaseProps, 'style'> {
 export function Button(props: ButtonProps) {
   const { loading, block, style, skin, size, kind, onClick, children, ...rest } = props
 
-  const [isLoading, setIsLoading] = React.useState<boolean>(false)
-  const onLoadingChange = (value: boolean) => setIsLoading(value)
+  const isMounted = useIsMounted()
+
+  const [isLoading, setIsLoading] = useState<boolean>(false)
+  const onLoadingChange = (value: boolean) => {
+    if (isMounted.current) {
+      setIsLoading(value)
+    }
+  }
   const isReallyLoading = isLoading || loading
 
   const { theme, css } = useStyles()
@@ -38,7 +45,7 @@ export function Button(props: ButtonProps) {
     style
   )
 
-  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
     if (!isReallyLoading) {
       return onClick && onClick(e)
     }
diff --git a/src/components/Button/Button/skins/default.ts b/src/components/Button/Button/skins/default.ts
index c30920c42..6bce946c8 100644
--- a/src/components/Button/Button/skins/default.ts
+++ b/src/components/Button/Button/skins/default.ts
@@ -1,4 +1,4 @@
-import { focusBoxShadow, shade, Theme } from '../../../../styles'
+import { focusBoxShadow, Theme } from '../../../../styles'
 import { Skin } from '../ButtonSkins'
 
 export const createStyles = (theme: Theme): Skin => ({
@@ -15,7 +15,7 @@ export const createStyles = (theme: Theme): Skin => ({
       boxShadow: focusBoxShadow(theme),
     },
     ':not(:disabled):hover': {
-      backgroundColor: shade(-0.08, theme.pallete.surface.main),
+      backgroundColor: theme.pallete.gray.c90,
     },
   },
   primary: {
@@ -23,7 +23,7 @@ export const createStyles = (theme: Theme): Skin => ({
     color: theme.pallete.surface.main,
     border: `1px solid ${theme.pallete.primary.main}`,
     ':not(:disabled):hover': {
-      backgroundColor: shade(-0.08, theme.pallete.primary.main),
+      backgroundColor: theme.pallete.primary.c30,
     },
   },
   danger: {
@@ -31,7 +31,7 @@ export const createStyles = (theme: Theme): Skin => ({
     color: theme.pallete.status.danger.onColor,
     border: `1px solid ${theme.pallete.status.danger.main}`,
     ':not(:disabled):hover': {
-      backgroundColor: shade(-0.08, theme.pallete.status.danger.main),
+      backgroundColor: theme.pallete.status.danger.c30,
     },
     ':focus': {
       boxShadow: focusBoxShadow(theme, 'danger'),
diff --git a/src/components/Button/ButtonBase.tsx b/src/components/Button/ButtonBase.tsx
index 46c95808e..66c49cdb9 100644
--- a/src/components/Button/ButtonBase.tsx
+++ b/src/components/Button/ButtonBase.tsx
@@ -23,9 +23,7 @@ export function ButtonBase(props: ButtonBaseProps) {
   }
 
   const stopLoading = () => {
-    setTimeout(() => {
-      onLoadingChange && onLoadingChange(false)
-    })
+    onLoadingChange && onLoadingChange(false)
   }
 
   const handleClick = (event: React.MouseEvent<any>) => {
@@ -33,12 +31,7 @@ export function ButtonBase(props: ButtonBaseProps) {
       const promise = onClick(event)
       if (isPromise(promise)) {
         startLoading()
-        promise
-          .then(() => stopLoading())
-          .catch(error => {
-            stopLoading()
-            throw new Error(error)
-          })
+        promise.finally(() => stopLoading())
       }
     }
   }
diff --git a/src/form/Field/withField.tsx b/src/form/Field/withField.tsx
index 2da9006de..7f060a3fc 100644
--- a/src/form/Field/withField.tsx
+++ b/src/form/Field/withField.tsx
@@ -1,4 +1,4 @@
-import hoistNonReactStatics = require('hoist-non-react-statics')
+import hoistNonReactStatics from 'hoist-non-react-statics'
 import React from 'react'
 
 import { InputStatus } from '../../components/TextInput'
diff --git a/src/hooks/useIsMounted.ts b/src/hooks/useIsMounted.ts
new file mode 100644
index 000000000..355099c26
--- /dev/null
+++ b/src/hooks/useIsMounted.ts
@@ -0,0 +1,17 @@
+import { useEffect, useRef } from 'react'
+
+/**
+ * Check if the component is still mounted.
+ * Does not use `useState` hook so it does not trigger additional renders.
+ * This might be used to avoid the "set state on unmounted component" react warning.
+ */
+export function useIsMounted() {
+  const isMounted = useRef<boolean>(false)
+
+  useEffect(() => {
+    isMounted.current = true
+    return () => (isMounted.current = false)
+  }, [])
+
+  return isMounted
+}
diff --git a/src/styles/theme/createPallete.ts b/src/styles/theme/createPallete.ts
index b9527dc12..b4085ed8d 100644
--- a/src/styles/theme/createPallete.ts
+++ b/src/styles/theme/createPallete.ts
@@ -3,7 +3,7 @@ import { Color } from 'csstype'
 import { merge } from '../../util'
 import { blue, ColorScale, gray, green, orange, red, yellow } from '../colors'
 
-export interface StatusColorMap {
+export interface StatusColorMap extends ColorScale {
   main: Color
   background: Color
   onColor: Color
@@ -66,21 +66,25 @@ export const createPallete = (userConfig?: PalleteConfig): Pallete => {
     },
     status: {
       danger: {
+        ...config.dangerScale,
         main: config.dangerScale.c40,
         background: config.dangerScale.c90,
         onColor: config.dangerScale.c100,
       },
       success: {
+        ...config.successScale,
         main: config.successScale.c40,
         background: config.successScale.c90,
         onColor: config.successScale.c100,
       },
       info: {
+        ...config.infoScale,
         main: config.infoScale.c40,
         background: config.infoScale.c90,
         onColor: config.infoScale.c100,
       },
       alert: {
+        ...config.alertScale,
         main: config.alertScale.c40,
         background: config.alertScale.c90,
         onColor: config.alertScale.c100,
diff --git a/src/styles/utils.ts b/src/styles/utils.ts
index f25322dd0..a9f90cf39 100644
--- a/src/styles/utils.ts
+++ b/src/styles/utils.ts
@@ -26,28 +26,3 @@ export const invertColorScale = (scale: ColorScale): ColorScale => {
     c100: scale.c10,
   }
 }
-
-// https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
-// tslint:disable
-// prettier-ignore
-export function shade(p, from, to = undefined) {
-    if (typeof (p) != "number" || p < -1 || p > 1 || typeof (from) != "string" || (from[0] != 'r' && from[0] != '#') || (typeof (to) != "string" && typeof (to) != "undefined")) return null; //ErrorCheck
-    if (!this.sbcRip) this.sbcRip = (d) => {
-        let l = d.length, RGB = new Object();
-        if (l > 9) {
-            d = d.split(",");
-            if (d.length < 3 || d.length > 4) return null;//ErrorCheck
-            RGB[0] = i(d[0].slice(4)), RGB[1] = i(d[1]), RGB[2] = i(d[2]), RGB[3] = d[3] ? parseFloat(d[3]) : -1;
-        } else {
-            if (l == 8 || l == 6 || l < 4) return null; //ErrorCheck
-            if (l < 6) d = "#" + d[1] + d[1] + d[2] + d[2] + d[3] + d[3] + (l > 4 ? d[4] + "" + d[4] : ""); //3 digit
-            d = i(d.slice(1), 16), RGB[0] = d >> 16 & 255, RGB[1] = d >> 8 & 255, RGB[2] = d & 255, RGB[3] = l == 9 || l == 5 ? r(((d >> 24 & 255) / 255) * 10000) / 10000 : -1;
-        }
-        return RGB;
-    }
-    var i = parseInt, r = Math.round, h = from.length > 9, h = typeof (to) == "string" ? to.length > 9 ? true : to == "c" ? !h : false : h, b = p < 0, p = b ? p * -1 : p as any, to = to && to != "c" ? to : b ? "#000000" as any : "#FFFFFF" as any, f = this.sbcRip(from), t = this.sbcRip(to);
-    if (!f || !t) return null; //ErrorCheck
-    if (h) return "rgb(" + r((t[0] - f[0]) * p + f[0]) + "," + r((t[1] - f[1]) * p + f[1]) + "," + r((t[2] - f[2]) * p + f[2]) + (f[3] < 0 && t[3] < 0 ? ")" : "," + (f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * p + f[3]) * 10000) / 10000 : t[3] < 0 ? f[3] : t[3]) + ")");
-    else return "#" + (0x100000000 + (f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * p + f[3]) * 255) : t[3] > -1 ? r(t[3] * 255) : f[3] > -1 ? r(f[3] * 255) : 255) * 0x1000000 + r((t[0] - f[0]) * p + f[0]) * 0x10000 + r((t[1] - f[1]) * p + f[1]) * 0x100 + r((t[2] - f[2]) * p + f[2])).toString(16).slice(f[3] > -1 || t[3] > -1 ? 1 : 3);
-}
-// tslint:enable
diff --git a/src/util/index.ts b/src/util/index.ts
index 63479bf7b..71429e1ce 100644
--- a/src/util/index.ts
+++ b/src/util/index.ts
@@ -1,10 +1,10 @@
 export * from './Observable'
 export * from './types'
 
-import _isEmpty = require('lodash/isEmpty')
-import _isEqual = require('lodash/isEqual')
-import _merge = require('lodash/merge')
-import _some = require('lodash/some')
+import _isEmpty from 'lodash/isEmpty'
+import _isEqual from 'lodash/isEqual'
+import _merge from 'lodash/merge'
+import _some from 'lodash/some'
 
 export const isEmpty = _isEmpty
 export const isEqual = _isEqual
@@ -15,5 +15,5 @@ export const some = _some
  * Check whether the object is a promise.
  */
 export const isPromise = (obj: any): obj is Promise<any> => {
-    return obj && typeof obj.then === 'function'
+  return obj && typeof obj.then === 'function'
 }
diff --git a/tsconfig.json b/tsconfig.json
index 99ef9eee7..8f5df45c4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
 {
   "compilerOptions": {
-    "moduleResolution": "node",
-    "module": "commonjs",
     "target": "es5",
-    "lib": ["es5", "es6", "es7", "es2017", "dom"],
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "module": "esnext",
+    "moduleResolution": "node",
     "types": ["webpack-env", "jest"],
     "outDir": "lib",
     "suppressImplicitAnyIndexErrors": true,
diff --git a/yarn.lock b/yarn.lock
index ccba4f25e..5f45a8a2e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1943,6 +1943,15 @@
     pretty-format "^24.8.0"
     wait-for-expect "^1.2.0"
 
+"@testing-library/react-hooks@1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-1.1.0.tgz#14b6b5c7c3d0e2cb3e55e9cbb248b44321641c64"
+  integrity sha512-piE/ceQoNf134FFVXBABDbttBJ8eLPD4eg7zIciVJv92RyvoIsBHCvvG8Vd4IG5pyuWYrkLsZTO8ucZBwa4twA==
+  dependencies:
+    "@babel/runtime" "^7.4.2"
+    "@types/react" "^16.8.22"
+    "@types/react-test-renderer" "^16.8.2"
+
 "@testing-library/react@8.0.1":
   version "8.0.1"
   resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-8.0.1.tgz#91c254adf855b13de50020613cb5d3915f9f7875"
@@ -2123,6 +2132,13 @@
     "@types/react" "*"
     "@types/webpack" "*"
 
+"@types/react-test-renderer@^16.8.2":
+  version "16.8.2"
+  resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.2.tgz#ad544b5571ebfc5f182c320376f1431a2b725c5e"
+  integrity sha512-cm42QR9S9V3aOxLh7Fh7PUqQ8oSfSdnSni30T7TiTmlKvE6aUlo+LhQAzjnZBPriD9vYmgG8MXI8UDK4Nfb7gg==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-text-mask@5.4.6":
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/@types/react-text-mask/-/react-text-mask-5.4.6.tgz#3a81e9de472beb939038e78cb16d737ae94ba14a"
@@ -2145,6 +2161,14 @@
     "@types/prop-types" "*"
     csstype "^2.2.0"
 
+"@types/react@^16.8.22":
+  version "16.8.22"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.22.tgz#7f18bf5ea0c1cad73c46b6b1c804a3ce0eec6d54"
+  integrity sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA==
+  dependencies:
+    "@types/prop-types" "*"
+    csstype "^2.2.0"
+
 "@types/stack-utils@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -10001,13 +10025,6 @@ react-helmet-async@^1.0.2:
     react-fast-compare "2.0.4"
     shallowequal "1.1.0"
 
-react-hooks-testing-library@0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.5.1.tgz#bb5e725fe8d35f3d682212039824dd2f6c7ff4c3"
-  integrity sha512-wSwnVPceof2AOpdvZpiC3EX4gZiVVvY78foz0/44yYGXjC2AeaWvNAjyRH1UjlPqCfHTaEANvXCq+m9YCbWBfQ==
-  dependencies:
-    "@babel/runtime" "^7.4.2"
-
 react-hotkeys@2.0.0-pre4:
   version "2.0.0-pre4"
   resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0-pre4.tgz#a1c248a51bdba4282c36bf3204f80d58abc73333"


Test patch:
diff --git a/src/components/Button/Button/Button.test.tsx b/src/components/Button/Button/Button.test.tsx
index d30a9fd0e..da7fa2258 100644
--- a/src/components/Button/Button/Button.test.tsx
+++ b/src/components/Button/Button/Button.test.tsx
@@ -1,5 +1,6 @@
 import { fireEvent, render } from '@testing-library/react'
 import React from 'react'
+import waait from 'waait'
 
 import { Icon } from '../../Icon'
 
@@ -111,6 +112,22 @@ it('should have a "loading" animation when onClick return is a Promise', () => {
   expect(button.getAttribute('data-loading')).toEqual('true')
 })
 
+it('shoud not emit a "setState while unmounted" react warning when component is unmounted while loading', async () => {
+  const delayedFunction = () => {
+    return new Promise((resolve, reject) => {
+      setTimeout(resolve, 10)
+    })
+  }
+
+  const { container, unmount } = render(<Button onClick={delayedFunction}>Button</Button>)
+  const button = container.querySelector('button')
+
+  fireEvent.click(button)
+  unmount()
+
+  await waait(20)
+})
+
 it('should NOT have animation when onClick return is not a Promise', () => {
   const func = () => undefined
   const { container } = render(<Button onClick={func}>Button</Button>)
diff --git a/src/components/Button/Button/__snapshots__/Button.test.tsx.snap b/src/components/Button/Button/__snapshots__/Button.test.tsx.snap
index 37b24c939..f514d038a 100644
--- a/src/components/Button/Button/__snapshots__/Button.test.tsx.snap
+++ b/src/components/Button/Button/__snapshots__/Button.test.tsx.snap
@@ -63,7 +63,7 @@ exports[`should accept the loading prop 1`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-0 span {
@@ -147,7 +147,7 @@ exports[`should render correctly with icon 1`] = `
 }
 
 .emotion-1:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-0 {
@@ -227,7 +227,7 @@ exports[`should render correctly with label 1`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 <div>
@@ -291,7 +291,7 @@ exports[`should render the default skin 1`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 <div>
@@ -358,11 +358,11 @@ exports[`should render the default skin 2`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #0061bf;
+  background-color: #0051A2;
 }
 
 <div>
@@ -429,11 +429,11 @@ exports[`should render the default skin 3`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #bf1c26;
+  background-color: #A4151E;
 }
 
 .emotion-0:focus {
diff --git a/src/components/ButtonGroup/__snapshots__/ButtonGroup.test.tsx.snap b/src/components/ButtonGroup/__snapshots__/ButtonGroup.test.tsx.snap
index 4748cccac..c3c4ce5ce 100644
--- a/src/components/ButtonGroup/__snapshots__/ButtonGroup.test.tsx.snap
+++ b/src/components/ButtonGroup/__snapshots__/ButtonGroup.test.tsx.snap
@@ -66,7 +66,7 @@ exports[`should render correctly 1`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 <div>
diff --git a/src/components/Modal/auto/__snapshots__/ModalAuto.test.tsx.snap b/src/components/Modal/auto/__snapshots__/ModalAuto.test.tsx.snap
index ee9a7c8b2..11ddcc3ba 100644
--- a/src/components/Modal/auto/__snapshots__/ModalAuto.test.tsx.snap
+++ b/src/components/Modal/auto/__snapshots__/ModalAuto.test.tsx.snap
@@ -187,7 +187,7 @@ exports[`should render correctly 1`] = `
 }
 
 .emotion-3:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-9 {
diff --git a/src/form/SubmitButton/__snapshots__/SubmitButton.test.tsx.snap b/src/form/SubmitButton/__snapshots__/SubmitButton.test.tsx.snap
index 6897d6965..78f08605e 100644
--- a/src/form/SubmitButton/__snapshots__/SubmitButton.test.tsx.snap
+++ b/src/form/SubmitButton/__snapshots__/SubmitButton.test.tsx.snap
@@ -52,11 +52,11 @@ exports[`should render correctly 1`] = `
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #ebebeb;
+  background-color: #F0F0F5;
 }
 
 .emotion-0:not(:disabled):hover {
-  background-color: #0061bf;
+  background-color: #0051A2;
 }
 
 <div>
diff --git a/src/hooks/__tests__/useIsMounted.test.ts b/src/hooks/__tests__/useIsMounted.test.ts
new file mode 100644
index 000000000..ebac3747c
--- /dev/null
+++ b/src/hooks/__tests__/useIsMounted.test.ts
@@ -0,0 +1,16 @@
+import { renderHook } from '@testing-library/react-hooks'
+
+import { useIsMounted } from '../useIsMounted'
+
+it('should track the mount state of the component', () => {
+  const { result, unmount, rerender } = renderHook(() => useIsMounted())
+  expect(result.current.current).toEqual(true)
+
+  rerender()
+  expect(result.current.current).toEqual(true)
+
+  unmount()
+  expect(result.current.current).toEqual(false)
+
+  expect(result.error).toBeUndefined()
+})
diff --git a/src/styles/hooks/__tests__/useCss.test.tsx b/src/styles/hooks/__tests__/useCss.test.tsx
index 992782c0e..37a9225e2 100644
--- a/src/styles/hooks/__tests__/useCss.test.tsx
+++ b/src/styles/hooks/__tests__/useCss.test.tsx
@@ -1,6 +1,6 @@
+import { renderHook } from '@testing-library/react-hooks'
 import { css as emotionCss } from 'emotion'
 import React from 'react'
-import { renderHook } from 'react-hooks-testing-library'
 
 import { createTheme } from '../../theme/createTheme'
 import { ThemeContext } from '../../theme/ThemeContext'
diff --git a/src/styles/hooks/__tests__/useStyles.test.tsx b/src/styles/hooks/__tests__/useStyles.test.tsx
index 7ccc1428d..a0f204dd1 100644
--- a/src/styles/hooks/__tests__/useStyles.test.tsx
+++ b/src/styles/hooks/__tests__/useStyles.test.tsx
@@ -1,6 +1,6 @@
+import { renderHook } from '@testing-library/react-hooks'
 import { css as emotionCss } from 'emotion'
 import React from 'react'
-import { renderHook } from 'react-hooks-testing-library'
 
 import { createTheme } from '../../theme/createTheme'
 import { ThemeContext } from '../../theme/ThemeContext'
diff --git a/src/styles/hooks/__tests__/useTheme.test.tsx b/src/styles/hooks/__tests__/useTheme.test.tsx
index 6c1f9852c..3a1221674 100644
--- a/src/styles/hooks/__tests__/useTheme.test.tsx
+++ b/src/styles/hooks/__tests__/useTheme.test.tsx
@@ -1,5 +1,5 @@
+import { renderHook } from '@testing-library/react-hooks'
 import React from 'react'
-import { renderHook } from 'react-hooks-testing-library'
 
 import { createTheme } from '../../theme/createTheme'
 import { ThemeContext } from '../../theme/ThemeContext'