From c7cd87be009c34361617abb4dc4fe26badfc052f Mon Sep 17 00:00:00 2001 From: lukKowalski Date: Thu, 12 Feb 2026 22:36:33 +0100 Subject: [PATCH] feat: Added nwe hook, useWindowFocus --- docs/useWindowFocus.md | 29 ++++++++++++ src/index.ts | 1 + src/useWindowFocus.ts | 22 +++++++++ stories/useWindowFocus.story.tsx | 22 +++++++++ tests/useWindowFocus.test.ts | 78 ++++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 docs/useWindowFocus.md create mode 100644 src/useWindowFocus.ts create mode 100644 stories/useWindowFocus.story.tsx create mode 100644 tests/useWindowFocus.test.ts diff --git a/docs/useWindowFocus.md b/docs/useWindowFocus.md new file mode 100644 index 0000000000..abdfa28443 --- /dev/null +++ b/docs/useWindowFocus.md @@ -0,0 +1,29 @@ +# `useWindowFocus` + +React sensor hook that tracks if the browser window is focused. + +## Usage + +```jsx +import {useWindowFocus} from 'react-use'; + +const Demo = () => { + const defaultState = document.hasFocus(); + const isFocused = useWindowFocus(defaultState); + + return ( +
+ Window is {isFocused ? 'focused' : 'not focused'} +
+ ); +}; +``` + +## Reference + +```js +const isFocused = useWindowFocus(initialState); +``` + +- `initialState` — `boolean`, optional initial state before the actual focus is determined, defaults to `false`. +- `isFocused` — `boolean`, whether the browser window is currently focused. diff --git a/src/index.ts b/src/index.ts index 62b69356b7..464d9af55d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,6 +106,7 @@ export { default as useVideo } from './useVideo'; export { default as useStateValidator } from './useStateValidator'; export { useScrollbarWidth } from './useScrollbarWidth'; export { useMultiStateValidator } from './useMultiStateValidator'; +export { default as useWindowFocus } from './useWindowFocus'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; export { default as useMeasure } from './useMeasure'; diff --git a/src/useWindowFocus.ts b/src/useWindowFocus.ts new file mode 100644 index 0000000000..8109cfdc64 --- /dev/null +++ b/src/useWindowFocus.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +const useWindowFocus = (defaultState: boolean = false) => { + const [isFocused, setIsFocused] = useState(defaultState); + + useEffect(() => { + const handleFocus = () => setIsFocused(true); + const handleBlur = () => setIsFocused(false); + + window.addEventListener('focus', handleFocus); + window.addEventListener('blur', handleBlur); + + return () => { + window.removeEventListener('focus', handleFocus); + window.removeEventListener('blur', handleBlur); + }; + }, []); + + return isFocused; +}; + +export default useWindowFocus; diff --git a/stories/useWindowFocus.story.tsx b/stories/useWindowFocus.story.tsx new file mode 100644 index 0000000000..f662cd537a --- /dev/null +++ b/stories/useWindowFocus.story.tsx @@ -0,0 +1,22 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import useWindowFocus from '../src/useWindowFocus'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const defaultState = document.hasFocus(); + const isFocused = useWindowFocus(defaultState); + + return ( +
+

Click outside this window or switch to another tab to see the focus state change.

+
+ Window is {isFocused ? '✅ Focused' : '❌ Not Focused'} +
+
+ ); +}; + +storiesOf('Sensors/useWindowFocus', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/tests/useWindowFocus.test.ts b/tests/useWindowFocus.test.ts new file mode 100644 index 0000000000..ebfe8257b5 --- /dev/null +++ b/tests/useWindowFocus.test.ts @@ -0,0 +1,78 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import useWindowFocus from '../src/useWindowFocus'; + +describe('useWindowFocus', () => { + it('should be defined', () => { + expect(useWindowFocus).toBeDefined(); + }); + + it('should return false initially', () => { + const { result } = renderHook(() => useWindowFocus()); + + expect(result.current).toBe(false); + }); + + it('should return true initially when defaultState is true', () => { + // Mock document.hasFocus() to return true + const hasFocusSpy = jest.spyOn(document, 'hasFocus').mockReturnValue(true); + + const { result } = renderHook(() => useWindowFocus(true)); + + expect(result.current).toBe(true); + + hasFocusSpy.mockRestore(); + }); + + it('should return false initially when initialState is false', () => { + const { result } = renderHook(() => useWindowFocus(false)); + + expect(result.current).toBe(false); + }); + + it('should return true when window receives focus', () => { + const { result } = renderHook(() => useWindowFocus()); + + act(() => { + window.dispatchEvent(new Event('focus')); + }); + + expect(result.current).toBe(true); + }); + + it('should return false when window loses focus', () => { + const { result } = renderHook(() => useWindowFocus()); + + act(() => { + window.dispatchEvent(new Event('focus')); + }); + expect(result.current).toBe(true); + + act(() => { + window.dispatchEvent(new Event('blur')); + }); + expect(result.current).toBe(false); + }); + + it('should add event listeners on mount', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + + renderHook(() => useWindowFocus()); + + expect(addEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('blur', expect.any(Function)); + + addEventListenerSpy.mockRestore(); + }); + + it('should remove event listeners on unmount', () => { + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const { unmount } = renderHook(() => useWindowFocus()); + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('blur', expect.any(Function)); + + removeEventListenerSpy.mockRestore(); + }); +});