Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pr/use battery #1394

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ export { resolveHookState } from './util/resolveHookState';

// Types
export * from './types';

export * from './useBattery';
12 changes: 12 additions & 0 deletions src/useBattery/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';
import { useBattery } from '../..';

export function Example() {
const batteryStats = useBattery();
return (
<div>
<div>Your battery state:</div>
<pre>{JSON.stringify(batteryStats, null, 2)}</pre>
</div>
);
}
45 changes: 45 additions & 0 deletions src/useBattery/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Example } from './example.stories';
import { ImportPath } from '../../__docs__/ImportPath';

<Meta title="Navigator/useBattery" component={Example} />

# useBattery

Hook that tracks battery state.

- Automatically updates on battery state changes.
- SSR compatible. (Properties return `undefined` on server.)

#### Example

<Canvas>
<Story story={Example} inline />
</Canvas>

## 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

<ImportPath />
24 changes: 24 additions & 0 deletions src/useBattery/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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();
});
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',
]);
});
});
32 changes: 32 additions & 0 deletions src/useBattery/__tests__/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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();
});
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();
});
});
120 changes: 120 additions & 0 deletions src/useBattery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { isEqual } from '@react-hookz/deep-equal';
import { isBrowser } from '../util/const';
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 = {
/**
* @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 = {
readonly charging: boolean;
readonly chargingTime: number;
readonly dischargingTime: number;
readonly level: number;
} & EventTarget;

type NavigatorWithPossibleBattery = Navigator & {
getBattery?: () => Promise<BatteryManager>;
};

const nav: NavigatorWithPossibleBattery | undefined = isBrowser ? navigator : undefined;
/**
* 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<BatteryState>({
charging: undefined,
chargingTime: undefined,
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;
}