Skip to content

Commit

Permalink
feat: new hook usePermission
Browse files Browse the repository at this point in the history
  • Loading branch information
xobotyi committed Jun 21, 2021
1 parent 131d98e commit 4aa95e3
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ import { useMountEffect } from "@react-hookz/web/esnext";

- [**`useNetworkState`**](https://react-hookz.github.io/web/?path=/docs/navigator-usenetwork)
— Tracks the state of browser's network connection.
- [**`usePermission`**](https://react-hookz.github.io/web/?path=/docs/navigator-usepermission)
— Tracks a permission state.

- #### Miscellaneous

Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export {
IUseNetworkState,
INetworkInformation,
} from './useNetworkState/useNetworkState';
export {
usePermission,
IAnyPermissionDescriptor,
IUsePermissionState,
} from './usePermission/usePermission';

// Miscellaneous
export { useSyncedRef } from './useSyncedRef/useSyncedRef';
Expand All @@ -63,7 +68,6 @@ export {
IUseResizeObserverCallback,
} from './useResizeObserver/useResizeObserver';
export { useMeasure } from './useMeasure/useMeasure';

export { useMediaQuery } from './useMediaQuery/useMediaQuery';

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

export const Example: React.FC = () => {
const status = usePermission({ name: 'notifications' });

return (
<div>
<div>
<em>
We do not use any notifications, notifications permission requested only for presentation
purposes.
</em>
</div>
<br />
<div>
Notifications status: <code>{status}</code>
</div>
<div>
{status === 'prompt' && (
<button
onClick={() => {
Notification.requestPermission();
}}>
Request notifications permission
</button>
)}
</div>
</div>
);
};
35 changes: 35 additions & 0 deletions src/usePermission/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './example.stories';

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

# usePermission

Tracks a permission state.

- SSR-compatible
- Automatically updates on permission state change.

#### Example

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

## Reference

```ts
export function usePermission(descriptor: IAnyPermissionDescriptor): IUsePermissionState;
```

#### Arguments

#### Return

0. **state** - Permission state, can be one of the following strings:
- **`not-requested`** - State not requested yet, yielded only on mount, before permission
state requested.
- **`requested`** - State requested, but request promise is pending yet.
- **`prompt`** - Permission not requested from user, and can be requested via target API.
- **`denied`** - User denied permission.
- **`granted`** - User granted permission.
13 changes: 13 additions & 0 deletions src/usePermission/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { usePermission } from '../..';

describe('usePermission', () => {
it('should be defined', () => {
expect(usePermission).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => usePermission());
expect(result.error).toBeUndefined();
});
});
13 changes: 13 additions & 0 deletions src/usePermission/__tests__/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { usePermission } from '../..';

describe('usePermission', () => {
it('should be defined', () => {
expect(usePermission).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => usePermission());
expect(result.error).toBeUndefined();
});
});
66 changes: 66 additions & 0 deletions src/usePermission/usePermission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useIsMounted } from '../useIsMounted/useIsMounted';
import { isBrowser, noop } from '../util/const';
import { useSafeState } from '../useSafeState/useSafeState';
import { off, on } from '../util/misc';

export type IAnyPermissionDescriptor =
| PermissionDescriptor
| DevicePermissionDescriptor
| MidiPermissionDescriptor
| PushPermissionDescriptor;

export type IUsePermissionState = PermissionState | 'not-requested' | 'requested';

/**
* Tracks a permission state.
*
* @param descriptor Permission request descriptor that passed to `navigator.permissions.query`
*/
export function usePermission(descriptor: IAnyPermissionDescriptor): IUsePermissionState {
const isMounted = useIsMounted();
const [state, setState] = useSafeState<IUsePermissionState>('not-requested');
const permissionStatus = useRef<PermissionStatus>();

const handleChange = useMemo(
() =>
// eslint-disable-next-line func-names
function () {
if (isMounted()) setState(this.state);
} as NonNullable<PermissionStatus['onchange']>,
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

const processStatus = useCallback((newStatus?: PermissionStatus) => {
if (permissionStatus.current) {
off(permissionStatus.current, 'change', handleChange);
permissionStatus.current = undefined;
}

if (newStatus && isMounted()) {
permissionStatus.current = newStatus;

setState(newStatus.state);
on(newStatus, 'change', handleChange, { passive: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// eslint-disable-next-line consistent-return
const requestState = useCallback(async (): Promise<PermissionStatus | void> => {
if (isBrowser) {
setState('requested');
return navigator.permissions.query(descriptor).then(processStatus).catch(noop);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [descriptor]);

useEffect(() => {
requestState();

return processStatus;
}, []);

return state;
}

0 comments on commit 4aa95e3

Please sign in to comment.