From ba541672e844c0b7b4a925447c8f452c6128034c Mon Sep 17 00:00:00 2001 From: Arjun S Date: Thu, 5 Oct 2023 16:26:09 +0530 Subject: [PATCH 1/3] Init hook:useBattery --- src/index.ts | 2 ++ src/useBattery/__docs__/example.stories.tsx | 12 +++++++++ src/useBattery/__docs__/story.mdx | 27 +++++++++++++++++++++ src/useBattery/__tests__/dom.ts | 13 ++++++++++ src/useBattery/__tests__/ssr.ts | 13 ++++++++++ src/useBattery/index.ts | 19 +++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 src/useBattery/__docs__/example.stories.tsx create mode 100644 src/useBattery/__docs__/story.mdx create mode 100644 src/useBattery/__tests__/dom.ts create mode 100644 src/useBattery/__tests__/ssr.ts create mode 100644 src/useBattery/index.ts diff --git a/src/index.ts b/src/index.ts index 94b995a51..a802eb2b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,3 +77,5 @@ export { resolveHookState } from './util/resolveHookState'; // Types export * from './types'; + +export * from './useBattery'; diff --git a/src/useBattery/__docs__/example.stories.tsx b/src/useBattery/__docs__/example.stories.tsx new file mode 100644 index 000000000..d44665c22 --- /dev/null +++ b/src/useBattery/__docs__/example.stories.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { useBattery } from '../..'; + +export function Example() { + const batteryStats = useBattery(); + return ( +
+
Your battery state:
+
{JSON.stringify(batteryStats, null, 2)}
+
+ ); +} diff --git a/src/useBattery/__docs__/story.mdx b/src/useBattery/__docs__/story.mdx new file mode 100644 index 000000000..061fbcd2f --- /dev/null +++ b/src/useBattery/__docs__/story.mdx @@ -0,0 +1,27 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs'; +import { Example } from './example.stories'; +import { ImportPath } from '../../__docs__/ImportPath'; + + + +# useBattery + +#### Example + + + + + +## Reference + +```ts + +``` + +#### Importing + + + +#### Arguments + +#### Return diff --git a/src/useBattery/__tests__/dom.ts b/src/useBattery/__tests__/dom.ts new file mode 100644 index 000000000..4f541f02c --- /dev/null +++ b/src/useBattery/__tests__/dom.ts @@ -0,0 +1,13 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { useBattery } from '../..'; + +describe('useBattery', () => { + it('should be defined', () => { + expect(useBattery).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useBattery()); + expect(result.error).toBeUndefined(); + }); +}); diff --git a/src/useBattery/__tests__/ssr.ts b/src/useBattery/__tests__/ssr.ts new file mode 100644 index 000000000..3717d633d --- /dev/null +++ b/src/useBattery/__tests__/ssr.ts @@ -0,0 +1,13 @@ +import { renderHook } from '@testing-library/react-hooks/server'; +import { useBattery } from '../..'; + +describe('useBattery', () => { + it('should be defined', () => { + expect(useBattery).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useBattery()); + expect(result.error).toBeUndefined(); + }); +}); diff --git a/src/useBattery/index.ts b/src/useBattery/index.ts new file mode 100644 index 000000000..8cb11bebb --- /dev/null +++ b/src/useBattery/index.ts @@ -0,0 +1,19 @@ +import { isEqual } from '@react-hookz/deep-equal'; +import { isBrowser } from '../util/const'; +import { useState } from 'react'; + +export type BatteryState = { + charging: boolean | undefined; + chargingTime: number | undefined; + dischargingTime: number | undefined; + level: number | undefined; +}; +export function useBattery(): BatteryState { + const [state, setState] = useState({ + charging: undefined, + chargingTime: undefined, + dischargingTime: undefined, + level: undefined, + }); + return state; +} From 784a65d071dc371dcded994ec95c12b54b02ae72 Mon Sep 17 00:00:00 2001 From: Arjun S Date: Thu, 5 Oct 2023 19:37:06 +0530 Subject: [PATCH 2/3] JSDOC and implementation --- src/useBattery/index.ts | 91 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/useBattery/index.ts b/src/useBattery/index.ts index 8cb11bebb..be2a34f0a 100644 --- a/src/useBattery/index.ts +++ b/src/useBattery/index.ts @@ -1,13 +1,50 @@ import { isEqual } from '@react-hookz/deep-equal'; import { isBrowser } from '../util/const'; -import { useState } from 'react'; +import { off, on } from '../util/misc'; +import { useEffect, useState } from 'react'; +/** + * The BatteryState interface is the return type of the useBattery Hook. + * + * provides information about the system's battery charge level + * and whether the device is charging, discharging, or fully charged. + * + * Uses the [BatteryManager](https://developer.mozilla.org/en-US/docs/Web/API/BatteryManager) interface. + * + * In server-side rendering (SSR) environments, the returned values will be undefined. + */ export type BatteryState = { charging: boolean | undefined; chargingTime: number | undefined; dischargingTime: number | undefined; level: number | undefined; }; + +type BatteryManager = { + readonly charging: boolean; + readonly chargingTime: number; + readonly dischargingTime: number; + readonly level: number; +} & EventTarget; + +type NavigatorWithPossibleBattery = Navigator & { + getBattery?: () => Promise; +}; + +const nav: NavigatorWithPossibleBattery | undefined = isBrowser ? navigator : undefined; +/** + * React hook that tracks battery state. + * + * @see https://react-hookz.github.io/web/?path=/docs/navigator-usebattery + * + * @returns + * - {charging} - whether the system is charging + * - {chargingTime} - time remaining in seconds until the system is fully charged + * - {dischargingTime} - time remaining in seconds until the system is fully discharged + * - {level} - battery level in percent + * + * In server-side rendering (SSR) environments, the returned values will be undefined. + */ export function useBattery(): BatteryState { const [state, setState] = useState({ charging: undefined, @@ -15,5 +52,57 @@ export function useBattery(): BatteryState { dischargingTime: undefined, level: undefined, }); + + useEffect(() => { + let isMounted = true; + let battery: BatteryManager | null = null; + + const handleChange = () => { + if (!isMounted || !battery) { + return; + } + + const newState: BatteryState = { + level: battery.level, + charging: battery.charging, + dischargingTime: battery.dischargingTime, + chargingTime: battery.chargingTime, + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + !isEqual(state, newState) && setState(newState); + }; + + nav + ?.getBattery?.() + .then((bat: BatteryManager) => { + // eslint-disable-next-line promise/always-return + if (!isMounted) { + return; + } + + battery = bat; + on(battery, 'chargingchange', handleChange); + on(battery, 'chargingtimechange', handleChange); + on(battery, 'dischargingtimechange', handleChange); + on(battery, 'levelchange', handleChange); + handleChange(); + }) + .catch(() => { + // ignore + }); + + return () => { + isMounted = false; + if (battery) { + off(battery, 'chargingchange', handleChange); + off(battery, 'chargingtimechange', handleChange); + off(battery, 'dischargingtimechange', handleChange); + off(battery, 'levelchange', handleChange); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return state; } From 3b66b08d427b1ac7413c58b24f3eea4c9243854c Mon Sep 17 00:00:00 2001 From: Arjun S Date: Thu, 5 Oct 2023 19:54:07 +0530 Subject: [PATCH 3/3] Adds useBattery Hook --- src/useBattery/__docs__/story.mdx | 28 +++++++++++++++++++++++----- src/useBattery/__tests__/dom.ts | 11 +++++++++++ src/useBattery/__tests__/ssr.ts | 19 +++++++++++++++++++ src/useBattery/index.ts | 22 +++++++++++++++++----- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/useBattery/__docs__/story.mdx b/src/useBattery/__docs__/story.mdx index 061fbcd2f..91daf8c3d 100644 --- a/src/useBattery/__docs__/story.mdx +++ b/src/useBattery/__docs__/story.mdx @@ -6,6 +6,11 @@ import { ImportPath } from '../../__docs__/ImportPath'; # useBattery +Hook that tracks battery state. + +- Automatically updates on battery state changes. +- SSR compatible. (Properties return `undefined` on server.) + #### Example @@ -15,13 +20,26 @@ import { ImportPath } from '../../__docs__/ImportPath'; ## Reference ```ts - +export type BatteryState = { + /** + * @desc {true} if the battery is charging, {false} otherwise. + */ + readonly charging: boolean | undefined; + /** + * @desc The time remaining in seconds until the system's battery is fully charged. + */ + readonly chargingTime: number | undefined; + /** + * @desc The time remaining in seconds until the system's battery is fully discharged. + */ + readonly dischargingTime: number | undefined; + /** + * @desc The battery level of the system as a number between 0 and 1. + */ + readonly level: number | undefined; +}; ``` #### Importing - -#### Arguments - -#### Return diff --git a/src/useBattery/__tests__/dom.ts b/src/useBattery/__tests__/dom.ts index 4f541f02c..6328fadcf 100644 --- a/src/useBattery/__tests__/dom.ts +++ b/src/useBattery/__tests__/dom.ts @@ -10,4 +10,15 @@ describe('useBattery', () => { const { result } = renderHook(() => useBattery()); expect(result.error).toBeUndefined(); }); + it('should return an object of certain structure', () => { + const hook = renderHook(() => useBattery(), { initialProps: false }); + + expect(typeof hook.result.current).toEqual('object'); + expect(Object.keys(hook.result.current)).toEqual([ + 'charging', + 'chargingTime', + 'dischargingTime', + 'level', + ]); + }); }); diff --git a/src/useBattery/__tests__/ssr.ts b/src/useBattery/__tests__/ssr.ts index 3717d633d..6731d1058 100644 --- a/src/useBattery/__tests__/ssr.ts +++ b/src/useBattery/__tests__/ssr.ts @@ -10,4 +10,23 @@ describe('useBattery', () => { const { result } = renderHook(() => useBattery()); expect(result.error).toBeUndefined(); }); + it('should return an object of certain structure', () => { + const hook = renderHook(() => useBattery(), { initialProps: false }); + + expect(typeof hook.result.current).toEqual('object'); + expect(Object.keys(hook.result.current)).toEqual([ + 'charging', + 'chargingTime', + 'dischargingTime', + 'level', + ]); + }); + it('should return undefined values', () => { + const hook = renderHook(() => useBattery(), { initialProps: false }); + + expect(hook.result.current.charging).toBeUndefined(); + expect(hook.result.current.chargingTime).toBeUndefined(); + expect(hook.result.current.dischargingTime).toBeUndefined(); + expect(hook.result.current.level).toBeUndefined(); + }); }); diff --git a/src/useBattery/index.ts b/src/useBattery/index.ts index be2a34f0a..13543d776 100644 --- a/src/useBattery/index.ts +++ b/src/useBattery/index.ts @@ -14,10 +14,22 @@ import { useEffect, useState } from 'react'; * In server-side rendering (SSR) environments, the returned values will be undefined. */ export type BatteryState = { - charging: boolean | undefined; - chargingTime: number | undefined; - dischargingTime: number | undefined; - level: number | undefined; + /** + * @desc {true} if the battery is charging, {false} otherwise. + */ + readonly charging: boolean | undefined; + /** + * @desc The time remaining in seconds until the system's battery is fully charged. + */ + readonly chargingTime: number | undefined; + /** + * @desc The time remaining in seconds until the system's battery is fully discharged. + */ + readonly dischargingTime: number | undefined; + /** + * @desc The battery level of the system as a number between 0 and 1. + */ + readonly level: number | undefined; }; type BatteryManager = { @@ -33,7 +45,7 @@ type NavigatorWithPossibleBattery = Navigator & { const nav: NavigatorWithPossibleBattery | undefined = isBrowser ? navigator : undefined; /** - * React hook that tracks battery state. + * Hook that tracks battery state. * * @see https://react-hookz.github.io/web/?path=/docs/navigator-usebattery *