diff --git a/.changeset/spotty-parents-beam.md b/.changeset/spotty-parents-beam.md
new file mode 100644
index 00000000000..af36538b516
--- /dev/null
+++ b/.changeset/spotty-parents-beam.md
@@ -0,0 +1,5 @@
+---
+"@primer/components": minor
+---
+
+Adds new `useSafeTimeout` helper Hook
diff --git a/docs/content/useSafeTimeout.mdx b/docs/content/useSafeTimeout.mdx
new file mode 100644
index 00000000000..c0c6ece8882
--- /dev/null
+++ b/docs/content/useSafeTimeout.mdx
@@ -0,0 +1,32 @@
+---
+title: useSafeTimeout
+---
+
+`useSafeTimeout` is a utility Hook that allows you to safely call `setTimeout` and `clearTimeout` within a component, ensuring that all timeouts are cleared when the component unmounts.
+
+
+### Usage
+
+```jsx live
+
+ {([]) => {
+ const {safeSetTimeout, safeClearTimeout} = useSafeTimeout()
+ let timeoutId = null
+
+ const handleOnClick = () => {
+ timeoutId = safeSetTimeout(() => window.alert('hello!'), 5000)
+ }
+
+ const cancelTimeout = () => {
+ safeClearTimeout(timeoutId)
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+ }}
+
+```
diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
index b133699c444..9a5ab38a2b6 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml
+++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
@@ -18,7 +18,10 @@
- title: Overriding Styles
url: /overriding-styles
-
+- title: Hooks
+ children:
+ - title: useSafeTimeout
+ url: /useSafeTimeout
- title: Components
children:
- title: Avatar
diff --git a/index.d.ts b/index.d.ts
index 21fff14e7dd..8813251a49f 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -100,6 +100,11 @@ declare module '@primer/components' {
setOpen: (open: boolean) => void
}
+ export const useSafeTimeout: () => {
+ safeSetTimeout: typeof window.setTimeout,
+ safeClearTimeout: typeof window.clearTimeout
+ }
+
export const useMouseIntent: () => void
export interface ButtonProps
@@ -663,6 +668,11 @@ declare module '@primer/components/lib/hooks/useMouseIntent' {
export default useMouseIntent
}
+declare module '@primer/components/lib/hooks/useMouseSafeTimeout' {
+ import {useSafeTimeout} from '@primer/components'
+ export default useSafeTimeout
+}
+
declare module '@primer/components/lib/BaseStyles' {
import {BaseStyles} from '@primer/components'
export default BaseStyles
diff --git a/package.json b/package.json
index fd43138d03f..ff2a58a4e3f 100644
--- a/package.json
+++ b/package.json
@@ -77,7 +77,9 @@
"@storybook/react": "^6.1.17",
"@testing-library/dom": "7.29.0",
"@testing-library/react": "11.2.2",
+ "@testing-library/react-hooks": "5.0.3",
"@testing-library/user-event": "12.6.0",
+ "@types/enzyme": "3.10.8",
"@types/jest": "26.0.20",
"@types/jest-axe": "3.5.1",
"@typescript-eslint/eslint-plugin": "4.14.1",
diff --git a/src/FilterList.tsx b/src/FilterList.tsx
index 614f42361b0..b77824f7f5a 100644
--- a/src/FilterList.tsx
+++ b/src/FilterList.tsx
@@ -94,7 +94,7 @@ FilterListItem.propTypes = {
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
children: PropTypes.node,
className: PropTypes.string,
- count: PropTypes.string,
+ count: PropTypes.number,
selected: PropTypes.bool,
theme: PropTypes.object,
...COMMON.propTypes,
diff --git a/src/__tests__/Avatar.tsx b/src/__tests__/Avatar.tsx
index 1df8968ef25..3f266f8be5a 100644
--- a/src/__tests__/Avatar.tsx
+++ b/src/__tests__/Avatar.tsx
@@ -9,7 +9,7 @@ import {COMMON} from '../constants'
expect.extend(toHaveNoViolations)
describe('Avatar', () => {
- behavesAsComponent(Avatar, [COMMON])
+ behavesAsComponent({Component: Avatar, systemPropArray: [COMMON]})
checkExports('Avatar', {
default: Avatar
diff --git a/src/__tests__/AvatarStack.tsx b/src/__tests__/AvatarStack.tsx
index da895eaacbe..d0ddd883322 100644
--- a/src/__tests__/AvatarStack.tsx
+++ b/src/__tests__/AvatarStack.tsx
@@ -26,7 +26,7 @@ const rightAvatarComp = (
)
describe('Avatar', () => {
- behavesAsComponent(AvatarStack, [COMMON], () => avatarComp)
+ behavesAsComponent({Component: AvatarStack, systemPropArray: [COMMON], toRender: () => avatarComp})
checkExports('AvatarStack', {
default: AvatarStack
diff --git a/src/__tests__/BorderBox.tsx b/src/__tests__/BorderBox.tsx
index 4fe8682d2ea..e546cdd4d13 100644
--- a/src/__tests__/BorderBox.tsx
+++ b/src/__tests__/BorderBox.tsx
@@ -9,7 +9,7 @@ import 'babel-polyfill'
expect.extend(toHaveNoViolations)
describe('BorderBox', () => {
- behavesAsComponent(BorderBox, [LAYOUT, COMMON, BORDER, FLEX])
+ behavesAsComponent({Component: BorderBox, systemPropArray: [LAYOUT, COMMON, BORDER, FLEX]})
checkExports('BorderBox', {
default: BorderBox
diff --git a/src/__tests__/Box.tsx b/src/__tests__/Box.tsx
index 9ee31478c8a..306e737eb4f 100644
--- a/src/__tests__/Box.tsx
+++ b/src/__tests__/Box.tsx
@@ -9,7 +9,7 @@ import {behavesAsComponent, checkExports, render} from '../utils/testing'
expect.extend(toHaveNoViolations)
describe('Box', () => {
- behavesAsComponent(Box, [COMMON, LAYOUT, FLEX])
+ behavesAsComponent({Component: Box, systemPropArray: [COMMON, LAYOUT, FLEX]})
checkExports('Box', {
default: Box,
diff --git a/src/__tests__/BranchName.tsx b/src/__tests__/BranchName.tsx
index 441d579d88c..6a1f6022f70 100644
--- a/src/__tests__/BranchName.tsx
+++ b/src/__tests__/BranchName.tsx
@@ -8,7 +8,7 @@ import 'babel-polyfill'
expect.extend(toHaveNoViolations)
describe('BranchName', () => {
- behavesAsComponent(BranchName, [COMMON])
+ behavesAsComponent({Component: BranchName, systemPropArray: [COMMON]})
checkExports('BranchName', {
default: BranchName
diff --git a/src/__tests__/Breadcrumb.tsx b/src/__tests__/Breadcrumb.tsx
index 13f2ea8e9db..e5d09e92e33 100644
--- a/src/__tests__/Breadcrumb.tsx
+++ b/src/__tests__/Breadcrumb.tsx
@@ -8,7 +8,7 @@ import 'babel-polyfill'
expect.extend(toHaveNoViolations)
describe('Breadcrumb', () => {
- behavesAsComponent(Breadcrumb, [COMMON])
+ behavesAsComponent({Component: Breadcrumb, systemPropArray: [COMMON]})
checkExports('Breadcrumb', {
default: Breadcrumb
@@ -28,12 +28,4 @@ describe('Breadcrumb', () => {
it('adds the Breadcrumb class', () => {
expect(rendersClass(, 'Breadcrumb')).toEqual(true)
})
-
- it('wraps its children in an li', () => {
- const children = yo
- const wrapper = mount({children})
- const list = wrapper.find('ol')
- expect(list.exists()).toEqual(true)
- expect(render(list.childAt(0)).type).toEqual('li')
- })
})
diff --git a/src/__tests__/BreadcrumbItem.tsx b/src/__tests__/BreadcrumbItem.tsx
index 0630d5efb70..5109642b54d 100644
--- a/src/__tests__/BreadcrumbItem.tsx
+++ b/src/__tests__/BreadcrumbItem.tsx
@@ -8,7 +8,7 @@ import {behavesAsComponent, render} from '../utils/testing'
expect.extend(toHaveNoViolations)
describe('Breadcrumb.Item', () => {
- behavesAsComponent(Breadcrumb.Item, [COMMON])
+ behavesAsComponent({Component: Breadcrumb.Item, systemPropArray:[COMMON]})
it('renders an by default', () => {
expect(render().type).toEqual('a')
diff --git a/src/__tests__/Button.tsx b/src/__tests__/Button.tsx
index 4c709efbf93..75110a095d9 100644
--- a/src/__tests__/Button.tsx
+++ b/src/__tests__/Button.tsx
@@ -20,7 +20,7 @@ expect.extend(toHaveNoViolations)
function noop() {}
describe('Button', () => {
- behavesAsComponent(Button, [COMMON, LAYOUT])
+ behavesAsComponent({Component: Button, systemPropArray: [COMMON, LAYOUT]})
checkExports('Button', {
default: Button,
@@ -69,7 +69,7 @@ describe('Button', () => {
})
describe('ButtonPrimary', () => {
- behavesAsComponent(ButtonPrimary, [COMMON, LAYOUT])
+ behavesAsComponent({Component: ButtonPrimary, systemPropArray: [COMMON, LAYOUT]})
it('renders a