├── storybook ├── .storybook │ ├── addons.js │ ├── preview-head.html │ ├── theme.js │ ├── config.js │ └── manager-head.html └── stories │ ├── mouse-position │ ├── README.md │ └── mouse-position.stories.js │ ├── resize │ ├── README.md │ └── resize.stories.js │ ├── scroll │ ├── README.md │ └── scroll.stories.js │ ├── online │ ├── README.md │ └── online.stories.js │ ├── orientation │ ├── README.md │ └── orientation.stories.js │ ├── page-visibility │ ├── README.md │ └── page-visibility.stories.js │ ├── click-outside │ ├── click-outside.stories.js │ └── README.md │ ├── fullscreen │ ├── fullscreen.stories.js │ └── README.md │ ├── geolocation │ ├── geolocation.stories.js │ └── README.md │ └── media-controls │ ├── media-controls.stories.js │ └── README.md ├── umd.js ├── .huskyrc ├── .testcaferc.json ├── netlify.toml ├── .jest.babelconfig.json ├── renovate.json ├── .lintstagedrc ├── jest.transform.js ├── .prettierrc ├── test ├── acceptance │ ├── globals.js │ ├── util.js │ ├── index.acceptance.test.js │ ├── hooks │ │ ├── scroll.acceptance.test.js │ │ ├── resize.acceptance.test.js │ │ ├── mouse-position.acceptance.test.js │ │ ├── fullscreen.acceptance.test.js │ │ └── media-controls.acceptance.test.js │ └── storybook.js └── unit │ ├── setup.js │ └── hooks │ ├── mouse-position.unit.test.js │ ├── orientation.unit.test.js │ ├── scroll.unit.test.js │ ├── resize.unit.test.js │ ├── online.unit.test.js │ ├── geolocation.unit.test.js │ ├── page-visibility.unit.test.js │ ├── click-outside.test.js │ ├── fullscreen.unit.test.js │ └── media-controls.unit.test.js ├── src ├── constants.js ├── index.js └── hooks │ ├── mouse-position.js │ ├── geolocation.js │ ├── scroll.js │ ├── orientation.js │ ├── resize.js │ ├── click-outside.js │ ├── online.js │ ├── page-visibility.js │ ├── media-controls.js │ └── fullscreen.js ├── .gitignore ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Feature_Request.md │ └── Bug_Report.md └── PULL_REQUEST_TEMPLATE.md ├── jest.config.js ├── nwb.config.js ├── LICENSE ├── .eslintrc ├── CONTRIBUTING.md ├── .circleci └── config.yml ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── bin └── acceptance.js /storybook/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import 'storybook-readme/register' 2 | -------------------------------------------------------------------------------- /umd.js: -------------------------------------------------------------------------------- 1 | import * as hooks from './src/index' 2 | export default hooks 3 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.testcaferc.json: -------------------------------------------------------------------------------- 1 | { 2 | "assertionTimeout": 10000, 3 | "selectorTimeout": 15000 4 | } 5 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build-storybook" 3 | publish = "storybook-static" -------------------------------------------------------------------------------- /.jest.babelconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "rangeStrategy": "replace" 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.js": [ 3 | "eslint src", 4 | "prettier --write", 5 | "git add" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /jest.transform.js: -------------------------------------------------------------------------------- 1 | module.exports = require('babel-jest').createTransformer( 2 | require('./.jest.babelconfig.json') 3 | ) 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxBracketSameLine": true, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /storybook/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/acceptance/globals.js: -------------------------------------------------------------------------------- 1 | const globals = { 2 | url: process.env.ACCEPTANCE_URL || 'http://localhost:3000' 3 | } 4 | 5 | export default globals 6 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const IS_SERVER = !( 2 | typeof window !== 'undefined' && 3 | window.document && 4 | window.document.createElement 5 | ) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | 9 | package-lock.json 10 | .DS_Store 11 | settings.json 12 | /storybook-static 13 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | global.navigator.geolocation = { 2 | getCurrentPosition: jest.fn(), 3 | watchPosition: jest.fn(), 4 | clearWatch: jest.fn() 5 | } 6 | 7 | Date.now = jest.fn(() => 1549358005991) // 05/02/2019 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Environment 2 | 3 | - `react-browser-hooks` version: 4 | - `react` version: 5 | - Browser: 6 | 7 | ### Description 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/acceptance/util.js: -------------------------------------------------------------------------------- 1 | import { ClientFunction } from 'testcafe' 2 | 3 | export const getWindowAttribute = ClientFunction( 4 | (attribute) => window[attribute] 5 | ) 6 | 7 | export const scrollWindow = ClientFunction((x, y) => window.scrollTo(x, y)) 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // this file can be removed when we get rid of acceptance tests 2 | module.exports = { 3 | testPathIgnorePatterns: ['/node_modules/', 'test/acceptance'], 4 | setupFiles: ['/test/unit/setup.js'], 5 | transform: { 6 | '^.+\\.js$': '/jest.transform.js' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/acceptance/index.acceptance.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe' 2 | import globals from './globals' 3 | 4 | fixture('Storybook').page(globals.url) 5 | 6 | test('The storybook demo is rendered', async (t) => { 7 | const demo = Selector('#root') 8 | 9 | await t.expect(demo.exists).ok() 10 | }) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: I have a suggestion for a new feature! 4 | --- 5 | 6 | ### Description 7 | 8 | 9 | 10 | ### Suggested implementation 11 | 12 | 13 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'ReactBrowserHooks', 7 | entry: './umd.js', 8 | externals: { 9 | react: 'React' 10 | } 11 | } 12 | }, 13 | karma: { 14 | testFiles: 'test/unit/**/*.test.js' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './hooks/click-outside' 2 | export * from './hooks/fullscreen' 3 | export * from './hooks/geolocation' 4 | export * from './hooks/mouse-position' 5 | export * from './hooks/media-controls' 6 | export * from './hooks/orientation' 7 | export * from './hooks/resize' 8 | export * from './hooks/scroll' 9 | export * from './hooks/online' 10 | export * from './hooks/page-visibility' 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | 4 | 5 | ### Related issues 6 | 7 | 8 | 9 | ### Checklist 10 | 11 | - [ ] I have checked the [contributing document](../blob/master/CONTRIBUTING.md) 12 | - [ ] I have added or updated any relevant documentation 13 | - [ ] I have added or updated any relevant tests 14 | -------------------------------------------------------------------------------- /src/hooks/mouse-position.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function useMousePosition() { 4 | const [state, setState] = useState({ x: 0, y: 0 }) 5 | 6 | useEffect(() => { 7 | const handler = ({ clientX, clientY }) => 8 | setState({ x: clientX, y: clientY }) 9 | window.addEventListener('mousemove', handler) 10 | return () => window.removeEventListener('mousemove', handler) 11 | }, []) 12 | 13 | return state 14 | } 15 | -------------------------------------------------------------------------------- /storybook/stories/mouse-position/README.md: -------------------------------------------------------------------------------- 1 | ## MousePosition Hook 2 | 3 | The MousePosition hook listens for changes to mouse position. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useMousePosition } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { x, y } = useMousePosition(60) 15 |

X: {x}px, Y: {y}px

16 | ``` 17 | 18 | Returns an object containing: 19 | - x (int), y (int): the mouse pointer position -------------------------------------------------------------------------------- /storybook/stories/resize/README.md: -------------------------------------------------------------------------------- 1 | ## Resize Hook 2 | 3 | The Resize hook listens for changes to browser window size. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useResize } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { width, height } = useResize() 15 |

Width: {width}px, Height: {height}px

16 | ``` 17 | 18 | Returns an object containing: 19 | - width (int), height (int): dimensions of the screen -------------------------------------------------------------------------------- /storybook/stories/scroll/README.md: -------------------------------------------------------------------------------- 1 | ## Scroll Hook 2 | 3 | The Scroll hook listens for changes to the browser window scroll position. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useScroll } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { top, left } = useScroll() 15 |

Top: {top}px, Left: {left}px

16 | ``` 17 | 18 | Returns an object containing: 19 | - top (int), left (int): the current scroll position -------------------------------------------------------------------------------- /storybook/stories/online/README.md: -------------------------------------------------------------------------------- 1 | ## Online Hook 2 | 3 | The Online hook listens for browser online/offline events to determine if there is an internet connection. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useOnline } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const online = useOnline() 15 |

Status: {online ? 'online' : 'offline'}

16 | ``` 17 | 18 | Returns: 19 | 20 | - online (boolean): whether online or not 21 | -------------------------------------------------------------------------------- /storybook/stories/online/online.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useOnline } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function Online() { 7 | const online = useOnline() 8 | return ( 9 |
10 |

Online Demo

11 |

Status: {online ? 'online' : 'offline'}

12 |
13 | ) 14 | } 15 | 16 | storiesOf('Online', module) 17 | .addParameters({ readme: { sidebar: readme } }) 18 | .add('Default', () => ) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 nearForm 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /storybook/stories/orientation/README.md: -------------------------------------------------------------------------------- 1 | ## Orientation Hook 2 | 3 | The Orientation hook gives you device orientation. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useOrientation } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { angle, type } = useOrientation() 15 |

Screen angle: {angle}°, Orientation type: {type}

16 | ``` 17 | 18 | Returns an object containing: 19 | - angle(double): current orientation angle 20 | - type(string): one of `portrait-primary`, `portrait-secondary`, `landscape-primary`, or `landscape-secondary` 21 | -------------------------------------------------------------------------------- /storybook/stories/page-visibility/README.md: -------------------------------------------------------------------------------- 1 | ## Page Visibility Hook 2 | 3 | The Page Visibilty hook determines if the current page is the active tab or not. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { usePageVisibility } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const visibility = usePageVisibility() 15 | useEffect(() => { 16 | document.title = visibility ? 'react-browser-hooks' : 'Hey! Come back!' 17 | }, [visibility]) 18 | ``` 19 | 20 | Returns an object containing: 21 | - visibility (boolean): whether the page is in view or not 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "prettier", 4 | "react-app" 5 | ], 6 | "plugins": [ 7 | "react-hooks", 8 | "prettier" 9 | ], 10 | "settings": { 11 | "react": { 12 | "version": "detect" 13 | } 14 | }, 15 | "rules": { 16 | "prettier/prettier": 2, 17 | "react-hooks/rules-of-hooks": "error", 18 | "react-hooks/exhaustive-deps": "warn", 19 | "quotes": [ 20 | "error", 21 | "single" 22 | ], 23 | "semi": [ 24 | 2, 25 | "never" 26 | ] 27 | }, 28 | "globals": { 29 | "fixture": true, 30 | "test": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/geolocation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function useGeolocation(options) { 4 | const [position, setPosition] = useState({ 5 | timestamp: Date.now(), 6 | coords: {} 7 | }) 8 | const [error, setError] = useState(null) 9 | 10 | useEffect(() => { 11 | navigator.geolocation.getCurrentPosition(setPosition, setError, options) 12 | const watchId = navigator.geolocation.watchPosition( 13 | setPosition, 14 | setError, 15 | options 16 | ) 17 | return () => navigator.geolocation.clearWatch(watchId) 18 | }, [options]) 19 | 20 | return { position, error } 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/scroll.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { IS_SERVER } from '../constants' 3 | 4 | export function useScroll() { 5 | const [pos, setPos] = useState({ 6 | top: IS_SERVER ? 0 : window.pageYOffset, 7 | left: IS_SERVER ? 0 : window.pageXOffset 8 | }) 9 | 10 | function handleScroll() { 11 | setPos({ top: window.pageYOffset, left: window.pageXOffset }) 12 | } 13 | 14 | useEffect(() => { 15 | window.addEventListener('scroll', handleScroll, false) 16 | 17 | return function cleanup() { 18 | window.removeEventListener('scroll', handleScroll) 19 | } 20 | }, []) 21 | 22 | return pos 23 | } 24 | -------------------------------------------------------------------------------- /storybook/.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming' 2 | 3 | const brand = { 4 | blue: '#2165e5', 5 | blueDark: '#194caa', 6 | grey: '#6d6d68', 7 | greyLight: '#efefef', 8 | greyLighter: '#f4f4f2', 9 | orange: '#fd775e', 10 | pink: '#fd7a9e', 11 | green: '#5EFB89', 12 | white: '#FFFFFF' 13 | } 14 | 15 | export default create({ 16 | base: 'light', 17 | 18 | brandTitle: 'React Browser Hooks', 19 | brandUrl: 'https://react-browser-hooks.netlify.com/', 20 | 21 | colorPrimary: brand.blue, 22 | colorSecondary: brand.blueDark, 23 | 24 | appBg: brand.blue, 25 | appContentBg: brand.white, 26 | 27 | barTextColor: brand.blueDark, 28 | barSelectedColor: brand.blueDark 29 | }) 30 | -------------------------------------------------------------------------------- /storybook/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { configure, addDecorator, addParameters } from '@storybook/react' 3 | import { addReadme } from 'storybook-readme' 4 | import theme from './theme' 5 | 6 | const req = require.context('../stories', true, /\.stories\.js$/) 7 | 8 | function loadStories() { 9 | req.keys().forEach((filename) => req(filename)) 10 | } 11 | 12 | addParameters({ 13 | options: { 14 | panelPosition: 'right', 15 | sortStoriesByKind: true, 16 | theme 17 | }, 18 | readme: { 19 | codeTheme: 'github' 20 | } 21 | }) 22 | 23 | const storyWrapper = (story) =>
{story()}
24 | 25 | addDecorator(addReadme) 26 | 27 | configure(loadStories, module) 28 | -------------------------------------------------------------------------------- /src/hooks/orientation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { IS_SERVER } from '../constants' 3 | 4 | const defaultState = { 5 | angle: 0, 6 | type: 'landscape-primary' 7 | } 8 | 9 | export function useOrientation() { 10 | const currentOrientation = 11 | !IS_SERVER && window.screen.orientation 12 | ? window.screen.orientation 13 | : defaultState 14 | 15 | const [state, setState] = useState(currentOrientation) 16 | 17 | useEffect(() => { 18 | const handler = () => setState(window.screen.orientation) 19 | 20 | window.addEventListener('orientationchange', handler) 21 | return () => window.removeEventListener('orientationchange', handler) 22 | }, []) 23 | 24 | return state 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/resize.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { IS_SERVER } from '../constants' 3 | 4 | const defaultState = { 5 | height: null, 6 | width: null 7 | } 8 | 9 | export function useResize() { 10 | const [size, setSize] = useState(IS_SERVER ? defaultState : getWindowSize()) 11 | 12 | function getWindowSize() { 13 | return { 14 | height: window.innerHeight, 15 | width: window.innerWidth 16 | } 17 | } 18 | 19 | useEffect(() => { 20 | function handleResize() { 21 | setSize(getWindowSize()) 22 | } 23 | 24 | window.addEventListener('resize', handleResize, false) 25 | return () => window.removeEventListener('resize', handleResize) 26 | }, [setSize]) 27 | 28 | return size 29 | } 30 | -------------------------------------------------------------------------------- /storybook/stories/orientation/orientation.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useOrientation } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function Orientation() { 7 | const { angle, type } = useOrientation() 8 | 9 | return ( 10 |
11 |

Orientation Demo

12 |

13 | Screen angle: {angle}° 14 |
15 | Orientation type: {type} 16 |

17 | Try using dev tools to rotate your screen, you should see these values 18 | change. 19 |
20 | ) 21 | } 22 | 23 | storiesOf('Orientation', module) 24 | .addParameters({ readme: { sidebar: readme } }) 25 | .add('Default', () => ) 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Bugs or any unexpected issues. 4 | --- 5 | 6 | ### Environment 7 | 8 | - `react-browser-hooks` version: 9 | - `react` version: 10 | - Browser: 11 | 12 | ### Description 13 | 14 | 15 | 16 | ### How to reproduce 17 | 18 | 19 | 20 | ```jsx 21 | Your code sample 22 | ``` 23 | 24 | ### Suggested solution (optional) 25 | 26 | 27 | -------------------------------------------------------------------------------- /storybook/.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 31 | -------------------------------------------------------------------------------- /src/hooks/click-outside.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export function useClickOutside(el, options = {}, onClick) { 4 | const els = [].concat(el) 5 | let active = true 6 | 7 | if (!onClick && typeof options === 'function') { 8 | onClick = options 9 | } else { 10 | active = options.active 11 | } 12 | 13 | const handler = (ev) => { 14 | const target = ev.target 15 | 16 | if (els.every((ref) => !ref.current || !ref.current.contains(target))) { 17 | onClick(ev) 18 | } 19 | } 20 | 21 | const cleanup = () => window.removeEventListener('click', handler) 22 | 23 | useEffect(() => { 24 | if (active) { 25 | window.addEventListener('click', handler) 26 | } else { 27 | cleanup() 28 | } 29 | 30 | return cleanup 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /storybook/stories/page-visibility/page-visibility.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { usePageVisibility } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function PageVisibility() { 7 | const visibility = usePageVisibility() 8 | 9 | useEffect(() => { 10 | window.parent.document.title = visibility 11 | ? 'react-browser-hooks' 12 | : 'Hey! Come back!' 13 | }, [visibility]) 14 | return ( 15 |
16 |

Page Visibility Demo

17 |

18 | Navigate away from this tab in your browser to see the tab text change. 19 |

20 |
21 | ) 22 | } 23 | 24 | storiesOf('PageVisibility', module) 25 | .addParameters({ readme: { sidebar: readme } }) 26 | .add('Default', () => ) 27 | -------------------------------------------------------------------------------- /test/unit/hooks/mouse-position.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { fireEvent } from '@testing-library/react' 3 | import { useMousePosition } from '../../../src' 4 | 5 | afterEach(cleanup) 6 | 7 | describe('useMousePosition', () => { 8 | it('sets initial state to 0, 0', () => { 9 | let x, y 10 | 11 | renderHook(() => ({ x, y } = useMousePosition())) 12 | 13 | expect(x).toBe(0) 14 | expect(y).toBe(0) 15 | }) 16 | 17 | it('updates state on "mousemove"', () => { 18 | let x, y 19 | 20 | act(() => { 21 | renderHook(() => ({ x, y } = useMousePosition())) 22 | }) 23 | 24 | act(() => { 25 | fireEvent.mouseMove(document.body, { clientX: 100, clientY: 100 }) 26 | }) 27 | 28 | expect(x).toBe(100) 29 | expect(y).toBe(100) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/hooks/online.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { IS_SERVER } from '../constants' 3 | 4 | //@todo: perhaps polling approach for older browsers as an option 5 | //e.g. favicon polling 6 | export function useOnline() { 7 | const [online, setOnline] = useState(getOnlineStatus()) 8 | 9 | function getOnlineStatus() { 10 | return IS_SERVER || (window.navigator && window.navigator.onLine) 11 | ? true 12 | : false 13 | } 14 | 15 | useEffect(() => { 16 | function handleChange() { 17 | setOnline(getOnlineStatus()) 18 | } 19 | 20 | window.addEventListener('offline', handleChange, false) 21 | window.addEventListener('online', handleChange, false) 22 | 23 | return function cleanup() { 24 | window.removeEventListener('online', handleChange) 25 | window.removeEventListener('offline', handleChange) 26 | } 27 | }, []) 28 | 29 | return online 30 | } 31 | -------------------------------------------------------------------------------- /storybook/stories/scroll/scroll.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { useScroll } from '../../../src' 5 | import readme from './README.md' 6 | 7 | function Scroll() { 8 | const scroll = useScroll() 9 | 10 | return ( 11 |
17 |
18 |
19 |

Scroll Demo

20 | Try scrolling within this frame... 21 |

22 | Top: {Math.round(scroll.top)}px, Left: {Math.round(scroll.left)}px 23 |
24 |

25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | storiesOf('Scroll', module) 32 | .addParameters({ readme: { sidebar: readme } }) 33 | .add('Default', () => ) 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= 6 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's loaded in storybook at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | 27 | ## Publishing 28 | - `npm publish --otp=[6-digit 2FA code]` will publish repo to npm, provided your npm account has been added to the 29 | project as a maintainer. 30 | -------------------------------------------------------------------------------- /storybook/stories/mouse-position/mouse-position.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useMousePosition } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function MousePosition() { 7 | const pos = useMousePosition() 8 | 9 | return ( 10 |
11 |

Mouse Position Demo

12 | The red dot shows this visually (offset by 5px) 13 |

14 | X: {pos.x}px, Y: {pos.y}px 15 |

16 |
28 |
29 | ) 30 | } 31 | 32 | storiesOf('Mouse Position', module) 33 | .addParameters({ readme: { sidebar: readme } }) 34 | .add('Default', () => ) 35 | -------------------------------------------------------------------------------- /storybook/stories/resize/resize.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useResize } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function Resize() { 7 | const size = useResize() 8 | 9 | return ( 10 |
11 |

Resize Demo

12 | The red border shows this visually. 13 |

14 | Width: {size.width}px, Height: {size.height}px 15 |
16 |

17 |
30 |
31 | ) 32 | } 33 | 34 | storiesOf('Resize', module) 35 | .addParameters({ readme: { sidebar: readme } }) 36 | .add('Default', () => ) 37 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | working_directory: ~/react-browser-hooks 9 | docker: 10 | - image: circleci/node:10.18-browsers 11 | environment: 12 | BROWSERSTACK_PARALLEL_RUNS: '5' 13 | BROWSERSTACK_USE_AUTOMATE: '1' 14 | BROWSERSTACK_PROJECT_NAME: 'react-browser-hooks' 15 | steps: 16 | - run: 17 | name: setup dynamic environment variables 18 | command: | 19 | echo 'export BROWSERSTACK_BUILD_ID="$CIRCLE_BRANCH"' >> $BASH_ENV 20 | - checkout 21 | - restore_cache: 22 | key: dependency-cache-{{ checksum "package.json" }} 23 | - run: npm install 24 | - save_cache: 25 | key: dependency-cache-{{ checksum "package.json" }} 26 | paths: 27 | - node_modules 28 | - run: npm run lint 29 | - run: npm run test:coverage 30 | - run: | 31 | npm install coveralls 32 | cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 33 | 34 | -------------------------------------------------------------------------------- /test/acceptance/hooks/scroll.acceptance.test.js: -------------------------------------------------------------------------------- 1 | import globals from '../globals' 2 | import Storybook from '../storybook' 3 | import { getWindowAttribute, scrollWindow } from '../util' 4 | 5 | const storybook = new Storybook() 6 | 7 | fixture('Scroll Hook') 8 | .page(globals.url + '/?path=/story/scroll--default') 9 | .beforeEach((t) => t.switchToIframe(storybook.iframe)) 10 | .afterEach((t) => t.switchToMainWindow()) 11 | 12 | test('The scroll demo is rendered', async (t) => { 13 | const { title } = storybook.hooks.scroll 14 | await t.expect(title.textContent).contains('Scroll Demo') 15 | }) 16 | 17 | test('The scroll demo defaults to window.scroll* values', async (t) => { 18 | const pageXOffset = await getWindowAttribute('pageXOffset') 19 | const pageYOffset = await getWindowAttribute('pageYOffset') 20 | 21 | const { description } = storybook.hooks.scroll 22 | 23 | await t 24 | .expect(description.textContent) 25 | .contains(`Top: ${pageYOffset}px, Left: ${pageXOffset}px`) 26 | }) 27 | 28 | test('The scroll demo updates state on scroll', async (t) => { 29 | const { description } = storybook.hooks.scroll 30 | 31 | await t.expect(description.textContent).contains('Top: 0px, Left: 0px') 32 | await scrollWindow(100, 100) 33 | await t.expect(description.textContent).contains('Top: 100px, Left: 100px') 34 | }) 35 | -------------------------------------------------------------------------------- /storybook/stories/click-outside/click-outside.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useClickOutside } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function ClickOutside() { 7 | const elRef = useRef(null) 8 | const [clickEvent, setClickEvent] = useState(null) 9 | useClickOutside(elRef, (ev) => { 10 | setClickEvent(ev) 11 | }) 12 | 13 | const message = 14 | clickEvent === null 15 | ? '' 16 | : `Clicked outside (x: ${clickEvent.clientX}, y: ${clickEvent.clientY})` 17 | 18 | return ( 19 | <> 20 |

Click Outside Demo

21 | Click outside target component receives full event 22 |
setClickEvent(null)}> 36 | {message} 37 |
38 | 39 | ) 40 | } 41 | 42 | storiesOf('Click Outside', module) 43 | .addParameters({ readme: { sidebar: readme } }) 44 | .add('Default', () => ) 45 | -------------------------------------------------------------------------------- /storybook/stories/fullscreen/fullscreen.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useFullScreen, useFullScreenBrowser } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function BrowserFullScreen() { 7 | const element = useRef(null) 8 | const fs = useFullScreen({ element }) 9 | const fsb = useFullScreenBrowser() 10 | return ( 11 |
15 |

Browser FullScreen Demo

16 |

17 | {fs.fullScreen && fsb.fullScreen && 'Browser in fullscreen mode'} 18 | {!fs.fullScreen && fsb.fullScreen && 'Browser in fullscreen mode (F11)'} 19 | {!fs.fullScreen && !fsb.fullScreen && 'Browser not in fullscreen mode'} 20 |

21 |
22 | 23 | 26 | 29 |
30 |

Fullscreen

31 |
{JSON.stringify(fs, null, 2)}
32 |

Fullscreen Browser

33 |
{JSON.stringify(fsb, null, 2)}
34 |
35 | ) 36 | } 37 | 38 | storiesOf('FullScreen', module) 39 | .addParameters({ readme: { sidebar: readme } }) 40 | .add('Default', () => ) 41 | -------------------------------------------------------------------------------- /storybook/stories/geolocation/geolocation.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useGeolocation } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function CurrentLocation() { 7 | const { position, error } = useGeolocation() 8 | 9 | return error ? ( 10 |

There was an error: {error.message}

11 | ) : ( 12 |
13 |       {JSON.stringify(
14 |         {
15 |           timestamp: position.timestamp,
16 |           coords: {
17 |             accuracy: position.coords.accuracy,
18 |             altitude: position.coords.altitude,
19 |             altitudeAccuracy: position.coords.altitudeAccuracy,
20 |             heading: position.coords.heading,
21 |             latitude: position.coords.latitude,
22 |             longitude: position.coords.longitude,
23 |             speed: position.coords.speed
24 |           }
25 |         },
26 |         null,
27 |         2
28 |       )}
29 |     
30 | ) 31 | } 32 | 33 | function Geolocation() { 34 | const [showLocation, setShowLocation] = useState(false) 35 | 36 | return ( 37 |
38 |

Geolocation demo

39 | {showLocation ? ( 40 | 41 | ) : ( 42 | 43 | )} 44 |
45 | ) 46 | } 47 | 48 | storiesOf('Geolocation', module) 49 | .addParameters({ readme: { sidebar: readme } }) 50 | .add('Default', () => ) 51 | -------------------------------------------------------------------------------- /storybook/stories/click-outside/README.md: -------------------------------------------------------------------------------- 1 | ## Click Outside Hook 2 | 3 | The Click Outside Hook attaches a listener which will callback the target component with the event object on any click which is not on the target component, or a child of the target component. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useClickOutside } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | useClickOutside(ref, onClick) 15 | ``` 16 | 17 | Callback is invoked with the original event as the only argument 18 | 19 | Also supported is passing an array of refs, where `onClick` will only be called if the click target is outside _all_ of the components referenced. 20 | 21 | ```javascript 22 | useClickOutside([ref, siblingRef], onClick) 23 | ``` 24 | 25 | Avoiding unnecessary callbacks: 26 | 27 | If you have a large app with many components using this hook, you may wish to avoid calling the callback when not necessary. In this situation you can pass options as the second argument, and include an `active` property. The callback will not be invoked if this is falsey. For example, if you you had a dropdown where you are only interested in receiving a callback if the options are visible, you might use like: 28 | 29 | ```javascript 30 | const [optionsVisible, setOptionsVisible] = useState(false) 31 | const hideOptions = () => setOptionsVisible(false) 32 | useClickOutside(ref, { active: optionsVisible }, hideOptions) 33 | ``` 34 | 35 | In this example, `hideOptions` will never be called if `optionsVisible` is already `false`. 36 | -------------------------------------------------------------------------------- /test/acceptance/hooks/resize.acceptance.test.js: -------------------------------------------------------------------------------- 1 | import globals from '../globals' 2 | import Storybook from '../storybook' 3 | 4 | const storybook = new Storybook() 5 | 6 | fixture('Resize Hook') 7 | .page(globals.url + '/?path=/story/resize--default') 8 | .beforeEach((t) => t.switchToIframe(storybook.iframe)) 9 | .afterEach((t) => t.switchToMainWindow()) 10 | 11 | test('The resize demo is rendered', async (t) => { 12 | const { title } = storybook.hooks.resize 13 | await t.expect(title.textContent).contains('Resize Demo') 14 | }) 15 | 16 | test('The resize demo defaults to element.offset* values', async (t) => { 17 | const { description, border } = storybook.hooks.resize 18 | const { offsetHeight, offsetWidth } = await border() 19 | await t 20 | .expect(description.textContent) 21 | .contains(`Width: ${offsetWidth}px, Height: ${offsetHeight}px`) 22 | }) 23 | 24 | test('The dimensions update when the browser is resized', async (t) => { 25 | const { description, border } = storybook.hooks.resize 26 | 27 | let { offsetHeight, offsetWidth } = await border() 28 | await t 29 | .expect(description.textContent) 30 | .contains(`Width: ${offsetWidth}px, Height: ${offsetHeight}px`) 31 | .resizeWindow(700, 700) 32 | 33 | offsetHeight = await border.offsetHeight 34 | offsetWidth = await border.offsetWidth 35 | 36 | await t 37 | .expect(offsetHeight) 38 | .lte(700) 39 | .expect(offsetWidth) 40 | .lte(700) 41 | .expect(description.textContent) 42 | .contains(`Width: ${offsetWidth}px, Height: ${offsetHeight}px`) 43 | .resizeWindow(1024, 768) 44 | }) 45 | -------------------------------------------------------------------------------- /test/unit/hooks/orientation.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { fireEvent } from '@testing-library/react' 3 | 4 | import { useOrientation } from '../../../src' 5 | 6 | afterEach(cleanup) 7 | 8 | describe('useOrientation', () => { 9 | it('sets initial state to the default state if window.screen.orientation is falsy', () => { 10 | let angle, type 11 | window.screen.orientation = null 12 | 13 | renderHook(() => ({ angle, type } = useOrientation())) 14 | 15 | expect(angle).toBe(0) 16 | expect(type).toBe('landscape-primary') 17 | }) 18 | 19 | it('sets initial state to window.screen.orientation', () => { 20 | let angle, type 21 | window.screen.orientation = { angle: 0, type: 'portrait-primary' } 22 | 23 | renderHook(() => ({ angle, type } = useOrientation())) 24 | 25 | expect(angle).toBe(0) 26 | expect(type).toBe('portrait-primary') 27 | }) 28 | 29 | it('updates state on "orientationchange"', () => { 30 | let angle, type 31 | window.screen.orientation = { angle: 0, type: 'portrait-primary' } 32 | 33 | act(() => { 34 | renderHook(() => ({ angle, type } = useOrientation())) 35 | }) 36 | 37 | window.screen.orientation = { angle: 90, type: 'landscape-primary' } 38 | 39 | act(() => { 40 | fireEvent( 41 | window, 42 | new Event('orientationchange', { 43 | bubbles: false, 44 | cancelable: false 45 | }) 46 | ) 47 | }) 48 | 49 | expect(angle).toBe(90) 50 | expect(type).toBe('landscape-primary') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/acceptance/hooks/mouse-position.acceptance.test.js: -------------------------------------------------------------------------------- 1 | import globals from '../globals' 2 | import Storybook from '../storybook' 3 | 4 | const storybook = new Storybook() 5 | 6 | fixture('Mouse Position Hook') 7 | .page(globals.url + '/?path=/story/mouse-position--default') 8 | .beforeEach((t) => t.switchToIframe(storybook.iframe)) 9 | .afterEach((t) => t.switchToMainWindow()) 10 | 11 | test('The mouse position demo is rendered', async (t) => { 12 | const { title } = storybook.hooks.mousePosition 13 | await t.expect(title.textContent).contains('Mouse Position Demo') 14 | }) 15 | 16 | test('The mouse position defaults to 0,0', async (t) => { 17 | const { description } = storybook.hooks.mousePosition 18 | await t.expect(description.textContent).contains('X: 0px, Y: 0px') 19 | }) 20 | 21 | test('The mouse position updates when the mouse moves', async (t) => { 22 | const { description, html } = storybook.hooks.mousePosition 23 | 24 | // move mouse to center of html element (fires mousemove event) 25 | await t.hover(html) 26 | 27 | // get reported mouse position from the hook 28 | const dimensions = await description.textContent 29 | const numbers = /\d+/g 30 | const [x, y] = dimensions.match(numbers) 31 | 32 | // get true mouse position (center of html element) 33 | const { offsetHeight, offsetWidth } = await html() 34 | const expectedX = Math.round(offsetWidth / 2) 35 | const expectedY = Math.round(offsetHeight / 2) 36 | 37 | // assert that the reported mouse position is the true mouse position 38 | await t 39 | .expect(parseInt(x)) 40 | .eql(expectedX) 41 | .expect(parseInt(y)) 42 | .eql(expectedY) 43 | }) 44 | -------------------------------------------------------------------------------- /test/unit/hooks/scroll.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { fireEvent } from '@testing-library/react' 3 | 4 | import { useScroll } from '../../../src' 5 | import * as constants from '../../../src/constants' 6 | 7 | afterEach(cleanup) 8 | 9 | describe('useScroll', () => { 10 | describe('when rendered on the server', () => { 11 | beforeAll(() => { 12 | constants.IS_SERVER = true 13 | }) 14 | 15 | afterAll(() => { 16 | constants.IS_SERVER = false 17 | }) 18 | 19 | it('defaults to 0, 0', () => { 20 | let top, left 21 | 22 | renderHook(() => ({ top, left } = useScroll())) 23 | 24 | expect(top).toBe(0) 25 | expect(left).toBe(0) 26 | }) 27 | }) 28 | 29 | it('sets initial state to window.scroll values', () => { 30 | let top, left 31 | 32 | window.pageXOffset = 0 33 | window.pageYOffset = 0 34 | 35 | renderHook(() => ({ top, left } = useScroll())) 36 | 37 | expect(top).toBe(0) 38 | expect(left).toBe(0) 39 | }) 40 | 41 | it('updates state on "scroll" event', () => { 42 | let top, left 43 | 44 | window.pageXOffset = 0 45 | window.pageYOffset = 0 46 | 47 | act(() => { 48 | renderHook(() => ({ top, left } = useScroll())) 49 | }) 50 | 51 | window.pageXOffset = 100 52 | window.pageYOffset = 100 53 | 54 | act(() => { 55 | fireEvent( 56 | window, 57 | new Event('scroll', { 58 | bubbles: false, 59 | cancelable: false 60 | }) 61 | ) 62 | }) 63 | 64 | expect(top).toBe(100) 65 | expect(left).toBe(100) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/hooks/page-visibility.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { IS_SERVER } from '../constants' 3 | 4 | /** 5 | * Function to grab the visibility prop strings 6 | * from the current browser 7 | * 8 | * @returns {object} - object containing both hidden 9 | * and visibilityChange properties 10 | */ 11 | const getVisibilityProps = () => { 12 | if (IS_SERVER) { 13 | return {} 14 | } 15 | 16 | let hidden 17 | let visibilityChange 18 | 19 | if (typeof document.hidden !== 'undefined') { 20 | // Opera 12.10 and Firefox 18 and later support 21 | hidden = 'hidden' 22 | visibilityChange = 'visibilitychange' 23 | } else if (typeof document.msHidden !== 'undefined') { 24 | hidden = 'msHidden' 25 | visibilityChange = 'msvisibilitychange' 26 | } else if (typeof document.webkitHidden !== 'undefined') { 27 | hidden = 'webkitHidden' 28 | visibilityChange = 'webkitvisibilitychange' 29 | } 30 | 31 | return { hidden, visibilityChange } 32 | } 33 | 34 | /** 35 | * Page Visibility API Hook 36 | * Hooks into page visibility API 37 | * @returns {boolean} - whether page is currently visible 38 | */ 39 | export const usePageVisibility = () => { 40 | const { hidden, visibilityChange } = getVisibilityProps() 41 | const [visible, setVisible] = useState(IS_SERVER || !document[hidden]) 42 | 43 | useEffect(() => { 44 | const handler = () => setVisible(!document[hidden]) 45 | 46 | document.addEventListener(visibilityChange, handler) 47 | return () => { 48 | document.removeEventListener(visibilityChange, handler) 49 | } 50 | }, [hidden, visibilityChange]) 51 | 52 | return visible 53 | } 54 | -------------------------------------------------------------------------------- /test/unit/hooks/resize.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { fireEvent } from '@testing-library/react' 3 | 4 | import { useResize } from '../../../src' 5 | import * as constants from '../../../src/constants' 6 | 7 | afterEach(cleanup) 8 | 9 | describe('useResize', () => { 10 | describe('when rendered on the server', () => { 11 | beforeAll(() => { 12 | constants.IS_SERVER = true 13 | }) 14 | 15 | afterAll(() => { 16 | constants.IS_SERVER = false 17 | }) 18 | 19 | it('defaults to null, null', () => { 20 | let width, height 21 | 22 | renderHook(() => ({ width, height } = useResize())) 23 | 24 | expect(width).toBe(null) 25 | expect(height).toBe(null) 26 | }) 27 | }) 28 | 29 | it('sets initial state to window.inner* values', () => { 30 | let width, height 31 | 32 | window.innerWidth = 100 33 | window.innerHeight = 100 34 | 35 | renderHook(() => ({ width, height } = useResize())) 36 | 37 | expect(width).toBe(100) 38 | expect(height).toBe(100) 39 | }) 40 | 41 | it('updates state on "resize"', () => { 42 | let width, height 43 | 44 | window.innerWidth = 100 45 | window.innerHeight = 100 46 | 47 | act(() => { 48 | renderHook(() => ({ width, height } = useResize())) 49 | }) 50 | 51 | window.innerWidth = 200 52 | window.innerHeight = 200 53 | 54 | act(() => { 55 | fireEvent( 56 | window, 57 | new Event('resize', { 58 | bubbles: false, 59 | cancelable: false 60 | }) 61 | ) 62 | }) 63 | 64 | expect(width).toBe(200) 65 | expect(height).toBe(200) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /storybook/stories/fullscreen/README.md: -------------------------------------------------------------------------------- 1 | ## FullScreen Hook 2 | 3 | The FullScreen hook allows a page or element to occupy the full screen. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useFullScreen } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { toggle, fullScreen } = useFullScreen() 15 | 16 | ``` 17 | 18 | Parameters: 19 | 20 | - options (object): set { element } property using object returned from the useRef() React hook (if options not set, fullscreen defaults to root document element) 21 | 22 | Returns an object containing: 23 | 24 | - toggle (function): toggles full screen mode 25 | - open (function): opens full screen mode 26 | - close (function): closes full screen mode 27 | - fullScreen (boolean): whether in full screen mode or not 28 | 29 | See the /demo/src/components/fullscreen component for a full usage example. 30 | 31 | ## useFullScreenBrowser Hook 32 | 33 | The useFullScreenBrowser Hook detects if the user has entered fullscreen using the browser menu. 34 | 35 | Import as follows: 36 | 37 | ```javascript 38 | import { useFullScreenBrowser } from 'react-browser-hooks' 39 | ``` 40 | 41 | Example of usage: 42 | 43 | ```javascript 44 | const fsb = useFullScreenBrowser() 45 |

Full Screen: {fsb.fullScreen ? 'open' : 'closed'}

46 | ``` 47 | 48 | Returns an object containing: 49 | 50 | - fullScreen (boolean): whether in full screen mode or not 51 | - info (object): some information as to why we are in fullScreen mode 52 | - reason (string): why we are in full screen mode e.g. borderless full screen as innerWidth and innerHeight are the same size as the screen 53 | - sizeInfo (object): the sizeInfo object used to determine if in full screen, this is returned so that users can make further judgement as to whether in fullscreen mode or not 54 | -------------------------------------------------------------------------------- /storybook/stories/geolocation/README.md: -------------------------------------------------------------------------------- 1 | ## Geolocation Hook 2 | 3 | The Geolocation hook gives you current location. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useGeolocation } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const { position, error } = useGeolocation() 15 | if (error) { 16 | return

There was an error: {error.message}

17 | } 18 | 19 | return ( 20 |
21 |     {JSON.stringify(
22 |       {
23 |         timestamp: position.timestamp,
24 |         coords: {
25 |           accuracy: position.coords.accuracy,
26 |           altitude: position.coords.altitude,
27 |           altitudeAccuracy: position.coords.altitudeAccuracy,
28 |           heading: position.coords.heading,
29 |           latitude: position.coords.latitude,
30 |           longitude: position.coords.longitude,
31 |           speed: position.coords.speed
32 |         }
33 |       },
34 |       null,
35 |       2
36 |     )}
37 |   
38 | ) 39 | ``` 40 | 41 | Parameters: 42 | 43 | - options (object): see https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions for more info 44 | 45 | Returns an object containing: 46 | 47 | - position(object): https://developer.mozilla.org/en-US/docs/Web/API/Position 48 | - coords(object): https://developer.mozilla.org/en-US/docs/Web/API/Coordinates 49 | - accuracy(double): the accuracy of the latitude and longitude properties, expressed in meters, 50 | - altitude(double): the position's altitude in meters, relative to sea level., 51 | - altitudeAccuracy(double): the accuracy of the altitude expressed in meters, 52 | - heading(double): the direction in which the device is traveling, 53 | - latitude(double): the position's latitude in decimal degrees, 54 | - longitude(double): the position's longitude in decimal degrees, 55 | - speed(double): the velocity of the device in meters per second 56 | - timestamp(string): time when the location was retrieved 57 | - error(object): error object 58 | -------------------------------------------------------------------------------- /test/unit/hooks/online.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { fireEvent } from '@testing-library/react' 3 | 4 | import { useOnline } from '../../../src' 5 | import * as constants from '../../../src/constants' 6 | 7 | let onLineGetter 8 | 9 | beforeEach(() => { 10 | onLineGetter = jest.spyOn(window.navigator, 'onLine', 'get') 11 | }) 12 | 13 | afterEach(cleanup) 14 | 15 | describe('useOnline', () => { 16 | describe('when rendered on the server', () => { 17 | beforeAll(() => { 18 | constants.IS_SERVER = true 19 | }) 20 | 21 | afterAll(() => { 22 | constants.IS_SERVER = false 23 | }) 24 | 25 | it('defaults to true', () => { 26 | let online 27 | renderHook(() => (online = useOnline())) 28 | 29 | expect(online).toBe(true) 30 | }) 31 | }) 32 | 33 | it('sets initial state to window.navigator.onLine', () => { 34 | let online 35 | 36 | onLineGetter.mockReturnValue(true) 37 | 38 | renderHook(() => (online = useOnline())) 39 | 40 | expect(online).toBe(true) 41 | }) 42 | 43 | it('updates state on "online"', () => { 44 | let online 45 | 46 | onLineGetter.mockReturnValue(false) 47 | 48 | act(() => { 49 | renderHook(() => (online = useOnline())) 50 | }) 51 | 52 | onLineGetter.mockReturnValue(true) 53 | 54 | act(() => { 55 | fireEvent( 56 | window, 57 | new Event('online', { 58 | bubbles: false, 59 | cancelable: false 60 | }) 61 | ) 62 | }) 63 | 64 | expect(online).toBe(true) 65 | }) 66 | }) 67 | 68 | it('updates state on "offline"', () => { 69 | let online 70 | 71 | onLineGetter.mockReturnValue(true) 72 | 73 | act(() => { 74 | renderHook(() => (online = useOnline())) 75 | }) 76 | 77 | onLineGetter.mockReturnValue(false) 78 | 79 | act(() => { 80 | fireEvent( 81 | window, 82 | new Event('online', { 83 | bubbles: false, 84 | cancelable: false 85 | }) 86 | ) 87 | }) 88 | 89 | expect(online).toBe(false) 90 | }) 91 | -------------------------------------------------------------------------------- /test/acceptance/hooks/fullscreen.acceptance.test.js: -------------------------------------------------------------------------------- 1 | import globals from '../globals' 2 | import Storybook from '../storybook' 3 | 4 | const storybook = new Storybook() 5 | 6 | fixture('Fullscreen Hook') 7 | .page(globals.url + '/?path=/story/fullscreen--default') 8 | .beforeEach((t) => t.switchToIframe(storybook.iframe)) 9 | .afterEach((t) => t.switchToMainWindow()) 10 | 11 | test('The fullscreen demo is rendered', async (t) => { 12 | const { title } = storybook.hooks.fullscreen 13 | await t.expect(title.textContent).contains('FullScreen Demo') 14 | }) 15 | 16 | test('The fullscreen demo defaults to fullScreen === false', async (t) => { 17 | const { description, fullscreen } = storybook.hooks.fullscreen 18 | await t 19 | .expect(description.textContent) 20 | .contains('Browser not in fullscreen mode') 21 | .expect(fullscreen.textContent) 22 | .contains('"fullScreen": false') 23 | }) 24 | 25 | // fullscreen not yet supported by testcafe: https://github.com/DevExpress/testcafe/issues/3514 26 | 27 | // test('The toggle button enters fullscreen mode from non-fullscreen', async (t) => { 28 | // const { description, fullscreen, toggleButton } = storybook.hooks.fullscreen 29 | // await t 30 | // .expect(description.textContent) 31 | // .contains('Browser not in fullscreen mode') 32 | // .expect(fullscreen.textContent) 33 | // .contains('"fullScreen": false') 34 | // .click(toggleButton) 35 | // .expect(description.textContent) 36 | // .contains('Browser in fullscreen mode') 37 | // .expect(fullscreen.textContent) 38 | // .contains('"fullScreen": true') 39 | // }) 40 | 41 | // test('The toggle button exits fullscreen mode from fullscreen', async (t) => { 42 | // const { description, fullscreen, toggleButton } = storybook.hooks.fullscreen 43 | // await t 44 | // .click(toggleButton) 45 | // .expect(description.textContent) 46 | // .contains('Browser in fullscreen mode') 47 | // .expect(fullscreen.textContent) 48 | // .contains('"fullScreen": true') 49 | // .click(toggleButton) 50 | // .expect(description.textContent) 51 | // .contains('Browser not in fullscreen mode') 52 | // .expect(fullscreen.textContent) 53 | // .contains('"fullScreen": false') 54 | // }) 55 | -------------------------------------------------------------------------------- /test/acceptance/storybook.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe' 2 | 3 | class Hook { 4 | constructor(className) { 5 | this.html = Selector('html') 6 | this.demo = Selector(className) 7 | this.title = this.demo.child('h2') 8 | } 9 | } 10 | 11 | class FullscreenHook extends Hook { 12 | constructor(className) { 13 | super(className) 14 | 15 | const buttons = this.demo.find('button') 16 | this.toggleButton = buttons.nth(0) 17 | this.openButton = buttons.nth(1) 18 | this.closeButton = buttons.nth(2) 19 | 20 | this.description = this.demo.find('p') 21 | this.fullscreen = this.demo.find('pre') 22 | } 23 | } 24 | 25 | class MediaControlsHook extends Hook { 26 | constructor(className) { 27 | super(className) 28 | 29 | const buttons = this.demo.find('button') 30 | this.playStopButton = buttons.nth(0) 31 | this.restartButton = buttons.nth(1) 32 | this.seekBackButton = buttons.nth(2) 33 | this.seekForwardButton = buttons.nth(3) 34 | this.playPauseButton = buttons.nth(4) 35 | this.muteButton = buttons.nth(5) 36 | this.volumeDownButton = buttons.nth(6) 37 | this.volumeUpButton = buttons.nth(7) 38 | 39 | const descriptions = this.demo.find('p') 40 | this.videoPausedState = descriptions.nth(0) 41 | this.currentTimeState = descriptions.nth(1) 42 | this.audioPausedState = descriptions.nth(2) 43 | this.volumeState = descriptions.nth(3) 44 | this.mutedState = descriptions.nth(4) 45 | } 46 | } 47 | 48 | class MousePositionHook extends Hook { 49 | constructor(className) { 50 | super(className) 51 | this.description = this.demo.child('p') 52 | } 53 | } 54 | 55 | class ResizeHook extends Hook { 56 | constructor(className) { 57 | super(className) 58 | this.description = this.demo.find('p') 59 | this.border = this.demo.find('#follow-cursor') 60 | } 61 | } 62 | 63 | class ScrollHook extends Hook { 64 | constructor(className) { 65 | super(className) 66 | this.description = this.demo.find('p') 67 | } 68 | } 69 | 70 | export default class Storybook { 71 | constructor() { 72 | this.iframe = Selector('iframe#storybook-preview-iframe') 73 | this.hooks = { 74 | fullscreen: new FullscreenHook('.fullscreen-demo'), 75 | mediaControls: new MediaControlsHook('.media-controls-demo'), 76 | mousePosition: new MousePositionHook('.mouse-position-demo'), 77 | resize: new ResizeHook('.resize-demo'), 78 | scroll: new ScrollHook('.scroll-demo') 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-browser-hooks", 3 | "version": "2.2.4", 4 | "description": "react-browser-hooks React component", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "acceptance": "testcafe chrome test/acceptance/", 15 | "acceptance-ci": "testcafe browserstack:chrome,browserstack:firefox,browserstack:ie,browserstack:safari test/acceptance/", 16 | "build": "nwb build-react-component", 17 | "clean": "nwb clean-module && nwb clean-demo", 18 | "format": "prettier --write **/*.js", 19 | "lint": "eslint ./*.js src test", 20 | "lint-fix": "eslint ./*.js src test --fix", 21 | "prepublishOnly": "npm run build", 22 | "start": "npm run storybook", 23 | "storybook": "start-storybook -p 3000 -c storybook/.storybook --ci", 24 | "build-storybook": "build-storybook -c storybook/.storybook", 25 | "test": "npm run test:unit && npm run test:acceptance", 26 | "test:unit": "jest", 27 | "test:acceptance": "node bin/acceptance.js", 28 | "test:coverage": "jest --coverage", 29 | "test:watch": "jest --watch" 30 | }, 31 | "peerDependencies": { 32 | "react": ">= 16.8 < 17" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.4.3", 36 | "@babel/preset-env": "^7.4.3", 37 | "@babel/preset-react": "^7.0.0", 38 | "@emotion/core": "^10.0.27", 39 | "@storybook/addons": "^5.2.8", 40 | "@storybook/react": "^5.2.8", 41 | "@testing-library/dom": "^6.11.0", 42 | "@testing-library/jest-dom": "^4.2.4", 43 | "@testing-library/react": "^9.4.0", 44 | "@testing-library/react-hooks": "^3.2.1", 45 | "@typescript-eslint/eslint-plugin": "^2.15.0", 46 | "@typescript-eslint/parser": "^2.15.0", 47 | "axios": "^0.19.1", 48 | "babel-eslint": "^10.0.1", 49 | "babel-loader": "^8.0.5", 50 | "eslint": "^6.0.0", 51 | "eslint-config-prettier": "^6.9.0", 52 | "eslint-config-react-app": "^5.1.0", 53 | "eslint-plugin-flowtype": "^3.13.0", 54 | "eslint-plugin-import": "^2.16.0", 55 | "eslint-plugin-jsx-a11y": "^6.2.1", 56 | "eslint-plugin-prettier": "^3.0.1", 57 | "eslint-plugin-react": "^7.12.4", 58 | "eslint-plugin-react-hooks": "^1.7.0", 59 | "husky": "^4.0.6", 60 | "jest": "^24.7.1", 61 | "lint-staged": "^9.5.0", 62 | "prettier": "^1.16.4", 63 | "react": "^16.8.6", 64 | "react-test-renderer": "^16.12.0", 65 | "storybook-readme": "^5.0.8", 66 | "testcafe": "^1.1.1", 67 | "testcafe-browser-provider-browserstack": "^1.8.0" 68 | }, 69 | "author": "NearForm", 70 | "homepage": "https://github.com/nearform/react-browser-hooks", 71 | "license": "Apache-2.0", 72 | "repository": "https://github.com/nearform/react-browser-hooks", 73 | "keywords": [ 74 | "react", 75 | "hooks", 76 | "browser", 77 | "events", 78 | "listeners", 79 | "fullscreen", 80 | "resize", 81 | "scroll", 82 | "mouse", 83 | "web api", 84 | "orientation", 85 | "geolocation" 86 | ], 87 | "dependencies": {} 88 | } 89 | -------------------------------------------------------------------------------- /test/unit/hooks/geolocation.unit.test.js: -------------------------------------------------------------------------------- 1 | import { act, renderHook, cleanup } from '@testing-library/react-hooks' 2 | 3 | import { useGeolocation } from '../../../src' 4 | 5 | afterEach(cleanup) 6 | 7 | describe('useGeolocation', () => { 8 | it('sets initial state to mock Position', () => { 9 | let position, error 10 | renderHook(() => ({ position, error } = useGeolocation())) 11 | 12 | expect(position).toEqual({ 13 | coords: {}, 14 | timestamp: 1549358005991 15 | }) 16 | 17 | expect(error).toBe(null) 18 | }) 19 | 20 | it('calls getCurrentPosition with options', () => { 21 | act(() => { 22 | renderHook(() => useGeolocation({ foo: 'bar' })) 23 | }) 24 | 25 | expect(navigator.geolocation.getCurrentPosition).toHaveBeenCalledWith( 26 | expect.any(Function), 27 | expect.any(Function), 28 | { 29 | foo: 'bar' 30 | } 31 | ) 32 | }) 33 | 34 | it('returns a position on a successful getCurrentPosition', () => { 35 | navigator.geolocation.getCurrentPosition = jest 36 | .fn() 37 | .mockImplementationOnce((onSuccess) => onSuccess('position')) 38 | 39 | let position, error 40 | act(() => { 41 | renderHook(() => ({ position, error } = useGeolocation())) 42 | }) 43 | 44 | expect(position).toBe('position') 45 | expect(error).toBe(null) 46 | }) 47 | 48 | it('returns an error on an unsuccessful getCurrentPosition', () => { 49 | const mockError = new Error('Where are you?') 50 | 51 | navigator.geolocation.getCurrentPosition = jest 52 | .fn() 53 | .mockImplementationOnce((_, onError) => onError(mockError)) 54 | 55 | let error 56 | act(() => { 57 | renderHook(() => ({ error } = useGeolocation())) 58 | }) 59 | 60 | expect(error).toBe(mockError) 61 | }) 62 | 63 | it('calls watchPosition with options', () => { 64 | act(() => { 65 | renderHook(() => useGeolocation({ foo: 'bar' })) 66 | }) 67 | 68 | expect(navigator.geolocation.watchPosition).toHaveBeenCalledWith( 69 | expect.any(Function), 70 | expect.any(Function), 71 | { 72 | foo: 'bar' 73 | } 74 | ) 75 | }) 76 | 77 | it('returns a position on a successful watchPosition', () => { 78 | navigator.geolocation.watchPosition = jest 79 | .fn() 80 | .mockImplementationOnce((onSuccess) => onSuccess('position')) 81 | 82 | let position, error 83 | act(() => { 84 | renderHook(() => ({ position, error } = useGeolocation())) 85 | }) 86 | 87 | expect(position).toBe('position') 88 | expect(error).toBe(null) 89 | }) 90 | 91 | it('returns an error on an unsuccessful watchPosition', () => { 92 | const mockError = new Error('Cannot find you!') 93 | 94 | navigator.geolocation.watchPosition = jest 95 | .fn() 96 | .mockImplementationOnce((_, onError) => onError(mockError)) 97 | 98 | let error 99 | act(() => { 100 | renderHook(() => ({ error } = useGeolocation())) 101 | }) 102 | 103 | expect(error).toBe(mockError) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@nearform.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/hooks/media-controls.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function useMediaControls(element) { 4 | const [currentTime, setCurrentTime] = useState(null) 5 | const [muted, setMuted] = useState(null) 6 | const [paused, setPaused] = useState(null) 7 | const [volume, adjustVolume] = useState(null) 8 | const [cachedVolume, setCachedVolume] = useState(null) 9 | 10 | function pause() { 11 | element.current.pause() 12 | } 13 | 14 | function play() { 15 | return element.current.play() 16 | } 17 | 18 | function setVolume(value) { 19 | let volume 20 | 21 | if (value < 0) { 22 | volume = 0 23 | } else if (value > 1) { 24 | volume = 1 25 | } else { 26 | volume = value 27 | } 28 | 29 | if (volume === 0) { 30 | setCachedVolume(element.current.volume) 31 | mute() 32 | } else { 33 | unmute() 34 | } 35 | 36 | element.current.volume = volume 37 | } 38 | 39 | function mute() { 40 | element.current.muted = true 41 | } 42 | 43 | function unmute() { 44 | element.current.muted = false 45 | if (cachedVolume) { 46 | element.current.volume = cachedVolume 47 | setCachedVolume(null) 48 | } 49 | } 50 | 51 | function seek(value) { 52 | element.current.currentTime = value 53 | } 54 | 55 | function stop() { 56 | pause() 57 | seek(0) 58 | } 59 | 60 | function restart() { 61 | seek(0) 62 | return play() 63 | } 64 | 65 | useEffect(() => { 66 | const currEl = element.current 67 | const isPaused = () => currEl.paused || currEl.ended 68 | 69 | setCurrentTime(currEl.currentTime) 70 | setPaused(isPaused()) 71 | adjustVolume(currEl.volume) 72 | setMuted(currEl.muted) 73 | 74 | const playPauseHandler = () => setPaused(isPaused()) 75 | currEl.addEventListener('play', playPauseHandler) // fired by play method or autoplay attribute 76 | currEl.addEventListener('playing', playPauseHandler) // fired by resume after being paused due to lack of data 77 | currEl.addEventListener('pause', playPauseHandler) // fired by pause method 78 | currEl.addEventListener('waiting', playPauseHandler) // fired by pause due to lack of data 79 | 80 | const volumeHandler = () => { 81 | setMuted(currEl.muted) 82 | adjustVolume(currEl.volume) 83 | } 84 | currEl.addEventListener('volumechange', volumeHandler) // fired by a change of volume 85 | 86 | const seekHandler = () => setCurrentTime(currEl.currentTime) 87 | currEl.addEventListener('seeked', seekHandler) // fired on seek completed 88 | currEl.addEventListener('timeupdate', seekHandler) // fired on currentTime update 89 | 90 | return () => { 91 | currEl.removeEventListener('play', playPauseHandler) 92 | currEl.removeEventListener('playing', playPauseHandler) 93 | currEl.removeEventListener('pause', playPauseHandler) 94 | currEl.removeEventListener('waiting', playPauseHandler) 95 | 96 | currEl.removeEventListener('volumechange', volumeHandler) 97 | 98 | currEl.removeEventListener('seeked', seekHandler) 99 | currEl.removeEventListener('timeupdate', seekHandler) 100 | } 101 | }, [element, muted]) 102 | 103 | return { 104 | currentTime, 105 | mute, 106 | muted, 107 | unmute, 108 | pause, 109 | paused, 110 | play, 111 | restart, 112 | seek, 113 | setVolume, 114 | stop, 115 | volume 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /storybook/stories/media-controls/media-controls.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { useMediaControls } from '../../../src' 4 | import readme from './README.md' 5 | 6 | function Audio() { 7 | const player = useRef(null) 8 | const { 9 | mute, 10 | muted, 11 | unmute, 12 | pause, 13 | paused, 14 | play, 15 | setVolume, 16 | volume 17 | } = useMediaControls(player) 18 | 19 | return ( 20 |
21 |

Audio

22 |
23 |
24 | 31 |
32 |
33 | 36 | 39 | 46 | 53 |
54 |

The audio is paused: {paused !== null ? paused.toString() : ''}

55 |

The audio is volume: {volume !== null ? volume.toString() : ''}

56 |

The audio is muted: {muted !== null ? muted.toString() : ''}

57 |
58 |
59 | ) 60 | } 61 | 62 | function Video() { 63 | const player = useRef(null) 64 | const { currentTime, paused, play, restart, seek, stop } = useMediaControls( 65 | player 66 | ) 67 | 68 | return ( 69 |
70 |

Video

71 |
72 | 79 |
80 |
81 | 82 | 83 | 84 | 85 |
86 |

The video is paused: {paused !== null ? paused.toString() : ''}

87 |

88 | The video currentTime:{' '} 89 | {currentTime !== null ? currentTime.toString() : ''} 90 |

91 |
92 | ) 93 | } 94 | 95 | function MediaControls() { 96 | return ( 97 |
98 |

Media Controls Demo

99 |
102 | ) 103 | } 104 | 105 | storiesOf('MediaControls', module) 106 | .addParameters({ readme: { sidebar: readme } }) 107 | .add('Default', () => ) 108 | -------------------------------------------------------------------------------- /storybook/stories/media-controls/README.md: -------------------------------------------------------------------------------- 1 | ## MediaControls Hook 2 | 3 | The MediaControls hook gives you access to the `HTMLMediaElement` API. Create custom controls for your media. 4 | 5 | Import as follows: 6 | 7 | ```javascript 8 | import { useMediaControls } from 'react-browser-hooks' 9 | ``` 10 | 11 | Example of usage: 12 | 13 | ```javascript 14 | const player = useRef(null) 15 | const { 16 | pause, 17 | paused, 18 | play 19 | } = useMediaControls(player) 20 | 24 | 25 | ``` 26 | 27 | Accepts a ref to an 28 | [`HTMLMediaElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) such as [`