/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 | Toggle
23 |
24 | {'Open'}
25 |
26 |
27 | {'Close'}
28 |
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 | setShowLocation(true)}>Show my location
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 | {fullScreen ? 'Close' : 'Open'}
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 |
29 | Your browser does not support the audio element.
30 |
31 |
32 |
33 |
34 | {paused ? 'Play' : 'Pause'}
35 |
36 |
37 | {muted ? 'Unmute' : 'Mute'}
38 |
39 |
41 | setVolume(Math.max(0, Math.round((volume - 0.1) * 10) / 10))
42 | }
43 | disabled={volume === 0}>
44 | Volume down
45 |
46 |
48 | setVolume(Math.min(1, Math.round((volume + 0.1) * 10) / 10))
49 | }
50 | disabled={volume === 1}>
51 | Volume up
52 |
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 |
77 | Your browser does not support the video element.
78 |
79 |
80 |
81 | Play/Stop
82 | Restart
83 | seek(currentTime - 2)}>Seek back
84 | seek(currentTime + 2)}>Seek forward
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 |
100 |
101 |
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 |
23 |
24 | Play/Pause
25 | ```
26 |
27 | Accepts a ref to an
28 | [`HTMLMediaElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) such as [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video) or [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio).
29 |
30 | Returns an object containing:
31 |
32 | - paused(boolean): Tells whether the media element is paused. From [`HTMLMediaElement.paused`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused). Initialises to `null` until the media element has been rendered
33 | - pause(function): Will pause playback of the media, if the media is already in
34 | a paused state this method will have no effect. Calls
35 | [`HTMLMediaElement.pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause)
36 | - play(function): Attempts to begin playback of the media. It returns a Promise
37 | which is resolved when playback has been successfully started. Failure to
38 | begin playback for any reason, such as permission issues, result in the
39 | promise being rejected. Calls [`HTMLMediaElement.play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play)
40 |
41 | - volume(number): The volume at which the media will be played. From
42 | [`HTMLMediaElement.volume`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume). Initialises to `null` until the media element has been rendered
43 | - setVolume(function): Sets the HTMLMediaElement.volume to the passed value (a
44 | number between 0-1)
45 | - muted(boolean): Indicates whether the media element muted. From
46 | [`HTMLMediaElement.muted`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted). Initialises to `null` until the media element has been rendered
47 | - mute(function): Mutes the media element, if already muted this method will
48 | have no effect
49 | - unmute(function): Unmutes the media element, if already muted this method will
50 | have no effect
51 |
52 | - currentTime(number): Gives the current playback time in seconds. From
53 | [`HTMLMediaElement.currentTime`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentTime). Initialises to `null` until the media element has been rendered
54 | - seek(function): Sets the HTMLMediaElement.currentTime to the passed value (a
55 | number in seconds). If the number is higher than the total length of the
56 | media, it will skip to the end
57 |
58 | - stop(function): Will seek to the beginning and pause the video. Calls
59 | [`HTMLMediaElement.pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause)
60 | - restart(function): Will seek to the beginning and play the video. It returns a
61 | Promise which is resolved when playback has been successfully started. Failure
62 | to begin playback for any reason, such as permission issues, result in the
63 | promise being rejected. Calls
64 | [`HTMLMediaElement.play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play)
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-browser-hooks
2 |
3 | React Browser Hooks
4 |
5 | [](https://circleci.com/gh/nearform/react-browser-hooks)
6 | [](https://www.npmjs.com/package/react-browser-hooks)
7 | [](https://coveralls.io/github/nearform/react-browser-hooks?branch=master)
8 | [](https://app.netlify.com/sites/react-browser-hooks/deploys)
9 | 
10 |
11 |
12 | A simple utility library that provides custom hooks for some common browser events.
13 |
14 | ## Installation
15 |
16 | [npm](https://www.npmjs.com/package/react-browser-hooks):
17 |
18 | ```bash
19 | npm install react-browser-hooks
20 | ```
21 |
22 | ## Documentation & Demo
23 |
24 | You can find documentation and demo on https://react-browser-hooks.netlify.com/
25 |
26 | ## Example Usage
27 |
28 | E.g. The FullScreen hook:
29 |
30 | ```javascript
31 | import { useFullScreen } from 'react-browser-hooks'
32 |
33 | const fs = useFullScreen()
34 | {fs.fullScreen ? 'Close' : 'Open'}
35 | ```
36 |
37 | ### Server-side rendering
38 |
39 | Sensible defaults are provided to allow each hook to be safely used when rendering on the server.
40 |
41 | [build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square
42 | [build]: https://travis-ci.org/user/repo
43 | [npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square
44 | [npm]: https://www.npmjs.org/package/npm-package
45 | [coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square
46 | [coveralls]: https://coveralls.io/github/user/repo
47 |
48 | ## License
49 |
50 | Copyright 2019 NearForm
51 |
52 | Licensed under the Apache License, Version 2.0 (the "License");
53 | you may not use this file except in compliance with the License.
54 | You may obtain a copy of the License at
55 |
56 | http://www.apache.org/licenses/LICENSE-2.0
57 |
58 | Unless required by applicable law or agreed to in writing, software
59 | distributed under the License is distributed on an "AS IS" BASIS,
60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
61 | See the License for the specific language governing permissions and
62 | limitations under the License.
63 |
64 | [](http://browserstack.com/)
65 |
66 | We use BrowserStack to support as many browsers and devices as possible
67 |
--------------------------------------------------------------------------------
/bin/acceptance.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const Axios = require('axios')
4 | const ChildProcess = require('child_process')
5 |
6 | const { CI, CIRCLE_BRANCH, NETLIFY_ACCESS_TOKEN } = process.env
7 |
8 | const netlify = Axios.create({
9 | baseURL: 'https://api.netlify.com/api/v1/',
10 | headers: {
11 | Authorization: 'Bearer ' + NETLIFY_ACCESS_TOKEN,
12 | 'content-type': 'application/json'
13 | }
14 | })
15 |
16 | async function main() {
17 | if (CI === 'true') {
18 | console.log('ci detected. checking netlify status')
19 |
20 | console.log('getting site id')
21 | const sites = await netlify
22 | .get('sites?name=react-browser-hooks&filter=all')
23 | .then(({ data }) => data)
24 |
25 | if (!sites.length) {
26 | throw new Error('react-browser-hooks site not found')
27 | }
28 |
29 | const [site] = sites
30 | console.log(`site found: ${site.id}`)
31 |
32 | console.log('getting deploy id')
33 | const deploys = await netlify
34 | .get(`sites/${site.id}/deploys?branch=${CIRCLE_BRANCH}`)
35 | .then(({ data }) => data)
36 |
37 | if (!deploys.length) {
38 | throw new Error(`no deploys found for branch: ${CIRCLE_BRANCH}`)
39 | }
40 |
41 | const [deploy] = deploys // assumes most recent deploy is the right one
42 | console.log(`deploy found: ${deploy.id}`)
43 |
44 | async function waitForNetlifyDeploy(deployId, options = {}) {
45 | options.rate = options.rate || 30000 // 30s default
46 | options.timeout = options.timeout || 600000 // 10m default
47 | options.total = options.total || 0
48 |
49 | const { summary } = await netlify
50 | .get(`sites/${site.id}/deploys/${deployId}`)
51 | .then(({ data }) => data)
52 |
53 | console.log('waiting for deploy to complete...')
54 | console.log(`status: ${summary.status}`)
55 |
56 | if (options.total >= options.timeout) {
57 | throw new Error(
58 | `polling ended after ${options.total}ms (max wait reached)`
59 | )
60 | }
61 |
62 | if (summary.status === 'ready') {
63 | return
64 | }
65 |
66 | await wait(options.rate)
67 | options.total += options.rate
68 |
69 | await waitForNetlifyDeploy(deployId, options)
70 | }
71 |
72 | await waitForNetlifyDeploy(deploy.id)
73 | console.log(`netlify deployed: ${deploy.deploy_ssl_url}`)
74 | console.log('starting acceptance')
75 |
76 | const test = ChildProcess.spawn('npm', ['run', 'acceptance-ci'], {
77 | env: Object.assign({}, process.env, {
78 | ACCEPTANCE_URL: deploy.deploy_ssl_url
79 | }),
80 | stdio: 'inherit'
81 | })
82 |
83 | test.on('close', (code) => {
84 | if (code > 0) {
85 | console.log('the test closed with a non-zero exit code')
86 | process.exit(code)
87 | }
88 | })
89 |
90 | test.on('error', (error) => {
91 | console.error(error)
92 | process.exit(1)
93 | })
94 | } else {
95 | console.log('local development detected. starting with npm start')
96 |
97 | const app = ChildProcess.spawn('npm', ['start'], {
98 | detach: true,
99 | stdio: 'inherit'
100 | })
101 |
102 | const test = ChildProcess.spawn('npm', ['run', 'acceptance'], {
103 | detach: true,
104 | stdio: 'inherit'
105 | })
106 |
107 | app.on('error', (err) => {
108 | console.log('Failed to start subprocess:', err)
109 | test.kill()
110 | })
111 |
112 | test.on('close', () => {
113 | app.kill()
114 | })
115 | }
116 | }
117 |
118 | function wait(ms) {
119 | return new Promise((resolve) => setTimeout(resolve, ms))
120 | }
121 |
122 | main().catch((error) => {
123 | console.error(error)
124 | process.exit(1)
125 | })
126 |
--------------------------------------------------------------------------------
/src/hooks/fullscreen.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { IS_SERVER } from '../constants'
3 | import { useResize } from './resize'
4 |
5 | // determine if we are in fullscreen mode and why
6 | // don't set any state in here as called on init too
7 | export function isFullScreenElement(el) {
8 | if (el && el.current) {
9 | return Boolean(
10 | document.fullscreenElement === el.current ||
11 | document.mozFullScreenElement === el.current ||
12 | document.webkitFullscreenElement === el.current ||
13 | document.msFullscreenElement === el.current
14 | )
15 | }
16 |
17 | return Boolean(
18 | document.fullscreenElement ||
19 | document.mozFullScreenElement ||
20 | document.webkitFullscreenElement ||
21 | document.msFullscreenElement ||
22 | document.fullscreen ||
23 | document.mozFullScreen ||
24 | document.webkitIsFullScreen ||
25 | document.fullScreenMode
26 | )
27 | }
28 |
29 | export function useFullScreen(options = {}) {
30 | const fsEl = options && options.element
31 | const initialState = IS_SERVER ? false : isFullScreenElement(fsEl)
32 | const [fullScreen, setFullScreen] = useState(initialState)
33 |
34 | // access various open fullscreen methods
35 | function openFullScreen() {
36 | const el = (fsEl && fsEl.current) || document.documentElement
37 |
38 | if (el.requestFullscreen) return el.requestFullscreen()
39 | if (el.mozRequestFullScreen) return el.mozRequestFullScreen()
40 | if (el.webkitRequestFullscreen) return el.webkitRequestFullscreen()
41 | if (el.msRequestFullscreen) return el.msRequestFullscreen()
42 | }
43 |
44 | // access various exit fullscreen methods
45 | function closeFullScreen() {
46 | if (document.exitFullscreen) return document.exitFullscreen()
47 | if (document.mozCancelFullScreen) return document.mozCancelFullScreen()
48 | if (document.webkitExitFullscreen) return document.webkitExitFullscreen()
49 | if (document.msExitFullscreen) return document.msExitFullscreen()
50 | }
51 |
52 | useEffect(() => {
53 | function handleChange() {
54 | setFullScreen(isFullScreenElement(fsEl))
55 | }
56 |
57 | document.addEventListener('webkitfullscreenchange', handleChange, false)
58 | document.addEventListener('mozfullscreenchange', handleChange, false)
59 | document.addEventListener('msfullscreenchange', handleChange, false)
60 | document.addEventListener('MSFullscreenChange', handleChange, false) // IE11
61 | document.addEventListener('fullscreenchange', handleChange, false)
62 |
63 | return () => {
64 | document.removeEventListener('webkitfullscreenchange', handleChange)
65 | document.removeEventListener('mozfullscreenchange', handleChange)
66 | document.removeEventListener('msfullscreenchange', handleChange)
67 | document.removeEventListener('MSFullscreenChange', handleChange)
68 | document.removeEventListener('fullscreenchange', handleChange)
69 | }
70 | }, [options.element, fsEl])
71 |
72 | return {
73 | fullScreen,
74 | open: openFullScreen,
75 | close: closeFullScreen,
76 | toggle: fullScreen ? closeFullScreen : openFullScreen
77 | }
78 | }
79 |
80 | export function getSizeInfo() {
81 | if (IS_SERVER) return {}
82 | return {
83 | screenTop: window.screenTop,
84 | screenY: window.screenY,
85 | screenWidth: window.screen.width,
86 | screenHeight: window.screen.height,
87 | innerWidth: window.innerWidth,
88 | innerHeight: window.innerHeight
89 | }
90 | }
91 |
92 | export function isFullScreenSize(sizeInfo) {
93 | if (
94 | sizeInfo.screenWidth === sizeInfo.innerWidth &&
95 | sizeInfo.screenHeight === sizeInfo.innerHeight
96 | ) {
97 | return true
98 | } else if (!sizeInfo.screenTop && !sizeInfo.screenY) {
99 | return true
100 | }
101 |
102 | return false
103 | }
104 |
105 | export function useFullScreenBrowser() {
106 | const size = useResize()
107 | const initialSizeInfo = getSizeInfo()
108 |
109 | const [fullScreen, setFullScreen] = useState(
110 | IS_SERVER ? false : isFullScreenSize(initialSizeInfo)
111 | )
112 | const [sizeInfo, setSizeInfo] = useState(initialSizeInfo)
113 |
114 | useEffect(() => {
115 | const sizeInfo = getSizeInfo()
116 | setFullScreen(isFullScreenSize(sizeInfo))
117 | setSizeInfo(sizeInfo)
118 | }, [size.width, size.height])
119 |
120 | return {
121 | fullScreen: fullScreen,
122 | info: sizeInfo
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/test/unit/hooks/page-visibility.unit.test.js:
--------------------------------------------------------------------------------
1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks'
2 | import { fireEvent } from '@testing-library/react'
3 |
4 | import { usePageVisibility } from '../../../src'
5 | import * as constants from '../../../src/constants'
6 |
7 | afterEach(cleanup)
8 |
9 | describe('usePageVisibility', () => {
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 visible', () => {
20 | let visible
21 | renderHook(() => (visible = usePageVisibility()))
22 |
23 | expect(visible).toBe(true)
24 | })
25 | })
26 |
27 | describe('document.hidden', () => {
28 | beforeEach(() => {
29 | let hidden = false
30 | Object.defineProperty(document, 'hidden', {
31 | configurable: true,
32 | get() {
33 | return hidden
34 | },
35 | set(bool) {
36 | hidden = Boolean(bool)
37 | }
38 | })
39 | })
40 |
41 | afterEach(() => {
42 | Object.defineProperty(document, 'hidden', {
43 | get() {
44 | return undefined
45 | }
46 | })
47 | })
48 |
49 | it('is true if document is not initially hidden', () => {
50 | let visible
51 | document.hidden = false
52 | renderHook(() => (visible = usePageVisibility()))
53 |
54 | expect(visible).toBe(true)
55 | })
56 |
57 | it('is false if document is initially hidden', () => {
58 | let visible
59 | document.hidden = true
60 | renderHook(() => (visible = usePageVisibility()))
61 |
62 | expect(visible).toBe(false)
63 | })
64 |
65 | it('updates state on the "visibilitychange" event', () => {
66 | let visible
67 | document.hidden = false
68 |
69 | act(() => {
70 | renderHook(() => (visible = usePageVisibility()))
71 | })
72 |
73 | document.hidden = true
74 |
75 | act(() => {
76 | fireEvent(
77 | document,
78 | new Event('visibilitychange', {
79 | bubbles: false,
80 | cancelable: false
81 | })
82 | )
83 | })
84 |
85 | expect(visible).toBe(false)
86 | })
87 | })
88 |
89 | describe('document.msHidden', () => {
90 | beforeEach(() => {
91 | let hidden = false
92 | Object.defineProperty(document, 'msHidden', {
93 | configurable: true,
94 | get() {
95 | return hidden
96 | },
97 | set(bool) {
98 | hidden = Boolean(bool)
99 | }
100 | })
101 | })
102 |
103 | afterEach(() => {
104 | Object.defineProperty(document, 'msHidden', {
105 | get() {
106 | return undefined
107 | }
108 | })
109 | })
110 |
111 | it('is true if document is not initially hidden', () => {
112 | let visible
113 | document.msHidden = false
114 | renderHook(() => (visible = usePageVisibility()))
115 |
116 | expect(visible).toBe(true)
117 | })
118 |
119 | it('is false if document is initially hidden', () => {
120 | let visible
121 | document.msHidden = true
122 | renderHook(() => (visible = usePageVisibility()))
123 |
124 | expect(visible).toBe(false)
125 | })
126 |
127 | it('updates state on the "msvisibilitychange" event', () => {
128 | let visible
129 | document.msHidden = false
130 |
131 | act(() => {
132 | renderHook(() => (visible = usePageVisibility()))
133 | })
134 |
135 | document.msHidden = true
136 | act(() => {
137 | fireEvent(
138 | document,
139 | new Event('msvisibilitychange', {
140 | bubbles: false,
141 | cancelable: false
142 | })
143 | )
144 | })
145 |
146 | expect(visible).toBe(false)
147 | })
148 | })
149 |
150 | describe('document.webkitHidden', () => {
151 | beforeEach(() => {
152 | let hidden = false
153 | Object.defineProperty(document, 'webkitHidden', {
154 | configurable: true,
155 | get() {
156 | return hidden
157 | },
158 | set(bool) {
159 | hidden = Boolean(bool)
160 | }
161 | })
162 | })
163 |
164 | afterEach(() => {
165 | Object.defineProperty(document, 'webkitHidden', {
166 | get() {
167 | return undefined
168 | }
169 | })
170 | })
171 |
172 | it('is true if document is not initially hidden', () => {
173 | let visible
174 | document.webkitHidden = false
175 | renderHook(() => (visible = usePageVisibility()))
176 |
177 | expect(visible).toBe(true)
178 | })
179 |
180 | it('is false if document is initially hidden', () => {
181 | let visible
182 | document.webkitHidden = true
183 | renderHook(() => (visible = usePageVisibility()))
184 |
185 | expect(visible).toBe(false)
186 | })
187 |
188 | it('updates state on the "webkitvisibilitychange" event', () => {
189 | let visible
190 | document.webkitHidden = false
191 |
192 | act(() => {
193 | renderHook(() => (visible = usePageVisibility()))
194 | })
195 |
196 | document.webkitHidden = true
197 | act(() => {
198 | fireEvent(
199 | document,
200 | new Event('webkitvisibilitychange', {
201 | bubbles: false,
202 | cancelable: false
203 | })
204 | )
205 | })
206 |
207 | expect(visible).toBe(false)
208 | })
209 | })
210 | })
211 |
--------------------------------------------------------------------------------
/test/unit/hooks/click-outside.test.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, createRef } from 'react'
2 | import { cleanup, fireEvent, render } from '@testing-library/react'
3 | import { act } from 'react-dom/test-utils'
4 |
5 | import { useClickOutside } from '../../../src'
6 |
7 | let callback
8 | let testElementRef
9 | let childElementRef
10 | let siblingRef
11 | let testHook
12 | let testHookWithSibling
13 | let testHookWithActiveState
14 | let active
15 |
16 | beforeEach(() => {
17 | testElementRef = createRef()
18 | childElementRef = createRef()
19 | siblingRef = createRef()
20 | const TestChildComponent = forwardRef((props, ref) => {
21 | return
22 | })
23 |
24 | const TestHook = forwardRef(({ callback }, ref) => {
25 | useClickOutside(ref, callback)
26 | return (
27 |
28 |
29 |
30 | )
31 | })
32 |
33 | const TestHookWithActiveState = forwardRef(({ callback }, ref) => {
34 | useClickOutside(ref, { active }, callback)
35 | return (
36 |
37 |
38 |
39 | )
40 | })
41 |
42 | const TestHookWithSibling = forwardRef(({ callback }, ref) => {
43 | useClickOutside([ref, siblingRef], callback)
44 | return (
45 | <>
46 |
47 |
48 |
49 |
50 | >
51 | )
52 | })
53 |
54 | testHook = (callback) => {
55 | render( )
56 | }
57 |
58 | testHookWithActiveState = (callback) => {
59 | render( )
60 | }
61 |
62 | testHookWithSibling = (callback) => {
63 | render( )
64 | }
65 |
66 | callback = jest.fn()
67 | })
68 |
69 | afterEach(cleanup)
70 |
71 | describe('useClickOutside', () => {
72 | it('calls callback with click event on clicking outside the component', () => {
73 | testHook(callback)
74 | act(() => {
75 | fireEvent(
76 | document.body,
77 | new Event('click', {
78 | bubbles: true,
79 | cancelable: false
80 | })
81 | )
82 | })
83 |
84 | expect(callback).toBeCalledTimes(1)
85 | expect(callback.mock.calls[0].length).toBe(1)
86 | expect(callback.mock.calls[0][0] instanceof Event).toBe(true)
87 | expect(callback.mock.calls[0][0].type).toBe('click')
88 | })
89 |
90 | it('calls callback if calling component is active', () => {
91 | active = true
92 | testHookWithActiveState(callback)
93 | act(() => {
94 | fireEvent(
95 | document.body,
96 | new Event('click', {
97 | bubbles: true,
98 | cancelable: false
99 | })
100 | )
101 | })
102 |
103 | expect(callback).toBeCalledTimes(1)
104 | expect(callback.mock.calls[0].length).toBe(1)
105 | expect(callback.mock.calls[0][0] instanceof Event).toBe(true)
106 | expect(callback.mock.calls[0][0].type).toBe('click')
107 | })
108 |
109 | it('does not call callback if calling component is inactive', () => {
110 | active = false
111 | testHookWithActiveState(callback)
112 | act(() => {
113 | fireEvent(
114 | document.body,
115 | new Event('click', {
116 | bubbles: true,
117 | cancelable: false
118 | })
119 | )
120 | })
121 |
122 | expect(callback).toBeCalledTimes(0)
123 | })
124 |
125 | it('does not call callback when the component itself receives a click', () => {
126 | testHook(callback)
127 | act(() => {
128 | fireEvent(
129 | testElementRef.current,
130 | new Event('click', {
131 | bubbles: true,
132 | cancelable: false
133 | })
134 | )
135 | })
136 |
137 | expect(callback).toBeCalledTimes(0)
138 | })
139 |
140 | it('does not call callback when a child receives a click', () => {
141 | testHook(callback)
142 | act(() => {
143 | fireEvent(
144 | childElementRef.current,
145 | new Event('click', {
146 | bubbles: true,
147 | cancelable: false
148 | })
149 | )
150 | })
151 |
152 | expect(callback).toBeCalledTimes(0)
153 | })
154 |
155 | it('supports array of refs, and will call callback if target is not contained by any', () => {
156 | testHookWithSibling(callback)
157 | act(() => {
158 | fireEvent(
159 | document.body,
160 | new Event('click', {
161 | bubbles: true,
162 | cancelable: false
163 | })
164 | )
165 | })
166 |
167 | expect(callback).toBeCalledTimes(1)
168 | expect(callback.mock.calls[0].length).toBe(1)
169 | expect(callback.mock.calls[0][0] instanceof Event).toBe(true)
170 | expect(callback.mock.calls[0][0].type).toBe('click')
171 | })
172 |
173 | it('handles null ref.current', () => {
174 | siblingRef.current = null
175 | testHookWithSibling(callback)
176 | act(() => {
177 | fireEvent(
178 | document.body,
179 | new Event('click', {
180 | bubbles: true,
181 | cancelable: false
182 | })
183 | )
184 | })
185 |
186 | expect(callback).toBeCalledTimes(1)
187 | expect(callback.mock.calls[0].length).toBe(1)
188 | expect(callback.mock.calls[0][0] instanceof Event).toBe(true)
189 | expect(callback.mock.calls[0][0].type).toBe('click')
190 | })
191 |
192 | it('supports array of refs, and will not call callback if target is contained by any', () => {
193 | testHookWithSibling(callback)
194 | act(() => {
195 | fireEvent(
196 | siblingRef.current,
197 | new Event('click', {
198 | bubbles: true,
199 | cancelable: false
200 | })
201 | )
202 | })
203 | act(() => {
204 | fireEvent(
205 | testElementRef.current,
206 | new Event('click', {
207 | bubbles: true,
208 | cancelable: false
209 | })
210 | )
211 | })
212 |
213 | expect(callback).toBeCalledTimes(0)
214 | })
215 | })
216 |
--------------------------------------------------------------------------------
/test/acceptance/hooks/media-controls.acceptance.test.js:
--------------------------------------------------------------------------------
1 | import globals from '../globals'
2 | import Storybook from '../storybook'
3 |
4 | const storybook = new Storybook()
5 |
6 | fixture('Media Controls Hook')
7 | .page(globals.url + '/?path=/story/mediacontrols--default')
8 | .beforeEach(async (t) => {
9 | await t.switchToIframe(storybook.iframe)
10 | })
11 | .afterEach(async (t) => {
12 | await t.switchToMainWindow()
13 | })
14 |
15 | test('The media controls demo is rendered', async (t) => {
16 | const { title } = storybook.hooks.mediaControls
17 | await t.expect(title.textContent).contains('Media Controls Demo')
18 | })
19 |
20 | test('The video should initialize in a paused state', async (t) => {
21 | const { currentTimeState, videoPausedState } = storybook.hooks.mediaControls
22 |
23 | await t
24 | .expect(videoPausedState.textContent)
25 | .contains('The video is paused: true')
26 | .expect(currentTimeState.textContent)
27 | .contains('The video currentTime: 0')
28 | })
29 |
30 | test('The play button (play/stop) should play the video', async (t) => {
31 | const { playStopButton, videoPausedState } = storybook.hooks.mediaControls
32 |
33 | await t
34 | .hover(playStopButton)
35 | .click(playStopButton)
36 | .expect(videoPausedState.textContent)
37 | .contains('The video is paused: false')
38 | })
39 |
40 | test('The stop button (play/stop) should stop the video', async (t) => {
41 | const {
42 | currentTimeState,
43 | playStopButton,
44 | videoPausedState
45 | } = storybook.hooks.mediaControls
46 |
47 | await t
48 | .hover(playStopButton)
49 | .click(playStopButton)
50 | .click(playStopButton)
51 | .expect(videoPausedState.textContent)
52 | .contains('The video is paused: true')
53 | .expect(currentTimeState.textContent)
54 | .contains('The video currentTime: 0')
55 | })
56 |
57 | test('The seek forward button should seek forward by 2 seconds', async (t) => {
58 | const {
59 | currentTimeState,
60 | playStopButton,
61 | restartButton,
62 | seekForwardButton
63 | } = storybook.hooks.mediaControls
64 |
65 | await t
66 | .hover(restartButton)
67 | .click(restartButton)
68 | .hover(playStopButton)
69 | .click(playStopButton)
70 | .hover(seekForwardButton)
71 | .click(seekForwardButton)
72 | .expect(currentTimeState.textContent)
73 | .contains('The video currentTime: 2')
74 | })
75 |
76 | test('The seek backward button should seek backward by 2 seconds', async (t) => {
77 | const {
78 | currentTimeState,
79 | playStopButton,
80 | restartButton,
81 | seekBackButton,
82 | seekForwardButton
83 | } = storybook.hooks.mediaControls
84 |
85 | await t
86 | .hover(restartButton)
87 | .click(restartButton)
88 | .hover(playStopButton)
89 | .click(playStopButton)
90 | .hover(seekForwardButton)
91 | .click(seekForwardButton)
92 | .expect(currentTimeState.textContent)
93 | .contains('The video currentTime: 2')
94 | .click(seekForwardButton)
95 | .expect(currentTimeState.textContent)
96 | .contains('The video currentTime: 4')
97 | .hover(seekBackButton)
98 | .click(seekBackButton)
99 | .expect(currentTimeState.textContent)
100 | .contains('The video currentTime: 2')
101 | })
102 |
103 | test('The restart button should play the video from the beginning', async (t) => {
104 | const {
105 | currentTimeState,
106 | restartButton,
107 | seekForwardButton,
108 | videoPausedState
109 | } = storybook.hooks.mediaControls
110 |
111 | await t
112 | .hover(seekForwardButton)
113 | .click(seekForwardButton)
114 | .hover(restartButton)
115 | .click(restartButton)
116 | .expect(currentTimeState.textContent)
117 | .contains('The video currentTime: 0')
118 | .expect(videoPausedState.textContent)
119 | .contains('The video is paused: false')
120 | })
121 |
122 | test('The audio should initialize in the correct state', async (t) => {
123 | const {
124 | audioPausedState,
125 | mutedState,
126 | volumeState
127 | } = storybook.hooks.mediaControls
128 |
129 | await t
130 | .expect(audioPausedState.textContent)
131 | .contains('The audio is paused: true')
132 | .expect(volumeState.textContent)
133 | .contains('The audio is volume: 1')
134 | .expect(mutedState.textContent)
135 | .contains('The audio is muted: true')
136 | })
137 |
138 | test('The play button (play/pause) should play the audio', async (t) => {
139 | const { playPauseButton, audioPausedState } = storybook.hooks.mediaControls
140 |
141 | await t
142 | .expect(playPauseButton.textContent)
143 | .contains('Play')
144 | .hover(playPauseButton)
145 | .click(playPauseButton)
146 | .expect(audioPausedState.textContent)
147 | .contains('The audio is paused: false')
148 | })
149 |
150 | test('The pause button (play/pause) should pause the audio', async (t) => {
151 | const {
152 | muteButton,
153 | playPauseButton,
154 | audioPausedState
155 | } = storybook.hooks.mediaControls
156 |
157 | await t
158 | .expect(muteButton.textContent)
159 | .contains('Unmute')
160 | .hover(playPauseButton)
161 | .click(playPauseButton)
162 | .click(playPauseButton)
163 | .expect(audioPausedState.textContent)
164 | .contains('The audio is paused: true')
165 | })
166 |
167 | test('The mute button should mute the audio if muted === false', async (t) => {
168 | const { muteButton, mutedState, volumeState } = storybook.hooks.mediaControls
169 |
170 | await t
171 | .expect(muteButton.textContent)
172 | .contains('Unmute')
173 | .hover(muteButton)
174 | .click(muteButton)
175 | .click(muteButton)
176 | .expect(mutedState.textContent)
177 | .contains('The audio is muted: true')
178 | .expect(volumeState.textContent)
179 | .contains('The audio is volume: 1')
180 | })
181 |
182 | test('The mute button should unmute the audio if muted === true', async (t) => {
183 | const { muteButton, mutedState, volumeState } = storybook.hooks.mediaControls
184 |
185 | await t
186 | .expect(muteButton.textContent)
187 | .contains('Unmute')
188 | .hover(muteButton)
189 | .click(muteButton)
190 | .expect(mutedState.textContent)
191 | .contains('The audio is muted: false')
192 | .expect(volumeState.textContent)
193 | .contains('The audio is volume: 1')
194 | })
195 |
196 | test('The mute button should unmute the audio if muted === true (restoring previous volume)', async (t) => {
197 | const {
198 | muteButton,
199 | mutedState,
200 | volumeDownButton,
201 | volumeState
202 | } = storybook.hooks.mediaControls
203 |
204 | await t
205 | .expect(muteButton.textContent)
206 | .contains('Unmute')
207 | .hover(muteButton)
208 | .click(muteButton)
209 | .hover(volumeDownButton)
210 | .click(volumeDownButton)
211 | .hover(muteButton)
212 | .click(muteButton)
213 | .click(muteButton)
214 | .expect(mutedState.textContent)
215 | .contains('The audio is muted: false')
216 | .expect(volumeState.textContent)
217 | .contains('The audio is volume: 0.9')
218 | })
219 |
220 | test('The volume down button should decrease the volume by 0.1', async (t) => {
221 | const {
222 | muteButton,
223 | volumeDownButton,
224 | volumeState
225 | } = storybook.hooks.mediaControls
226 |
227 | await t
228 | .expect(muteButton.textContent)
229 | .contains('Unmute')
230 | .hover(muteButton)
231 | .click(muteButton)
232 | .hover(volumeDownButton)
233 | .click(volumeDownButton)
234 | .expect(volumeState.textContent)
235 | .contains('The audio is volume: 0.9')
236 | .hover(volumeDownButton)
237 | .click(volumeDownButton)
238 | .expect(volumeState.textContent)
239 | .contains('The audio is volume: 0.8')
240 | })
241 |
--------------------------------------------------------------------------------
/test/unit/hooks/fullscreen.unit.test.js:
--------------------------------------------------------------------------------
1 | import React, { createRef } from 'react'
2 | import { act, cleanup, renderHook } from '@testing-library/react-hooks'
3 | import { fireEvent, render } from '@testing-library/react'
4 |
5 | import { useFullScreen, useFullScreenBrowser } from '../../../src'
6 | import * as constants from '../../../src/constants'
7 |
8 | afterEach(cleanup)
9 |
10 | let testElementRef
11 | beforeEach(() => {
12 | testElementRef = createRef()
13 | render(
)
14 |
15 | document.fullscreenElement = null
16 | document.mozFullScreenElement = null
17 | document.webkitFullscreenElement = null
18 | document.msFullscreenElement = null
19 | document.fullscreen = null
20 | document.mozFullScreen = null
21 | document.webkitIsFullScreen = null
22 | document.fullScreenMode = null
23 | })
24 |
25 | describe('useFullScreen', () => {
26 | describe('initial state', () => {
27 | describe('when rendered on the server', () => {
28 | beforeAll(() => {
29 | constants.IS_SERVER = true
30 | })
31 |
32 | afterAll(() => {
33 | constants.IS_SERVER = false
34 | })
35 |
36 | it('defaults to false', () => {
37 | let fullScreen
38 | document.fullscreenElement = testElementRef.current
39 | renderHook(
40 | () => ({ fullScreen } = useFullScreen({ element: testElementRef }))
41 | )
42 |
43 | expect(fullScreen).toBe(false)
44 | })
45 | })
46 |
47 | describe('with options and element ref', () => {
48 | it('uses document.fullscreenElement', () => {
49 | let fullScreen
50 | document.fullscreenElement = testElementRef.current
51 | renderHook(
52 | () => ({ fullScreen } = useFullScreen({ element: testElementRef }))
53 | )
54 |
55 | expect(fullScreen).toBe(true)
56 | })
57 |
58 | it('uses document.mozFullScreenElement', () => {
59 | let fullScreen
60 | document.mozFullScreenElement = testElementRef.current
61 | renderHook(
62 | () => ({ fullScreen } = useFullScreen({ element: testElementRef }))
63 | )
64 |
65 | expect(fullScreen).toBe(true)
66 | })
67 |
68 | it('uses document.webkitFullscreenElement', () => {
69 | let fullScreen
70 | document.webkitFullscreenElement = testElementRef.current
71 | renderHook(
72 | () => ({ fullScreen } = useFullScreen({ element: testElementRef }))
73 | )
74 |
75 | expect(fullScreen).toBe(true)
76 | })
77 |
78 | it('uses document.msFullscreenElement', () => {
79 | let fullScreen
80 | document.msFullscreenElement = testElementRef.current
81 | renderHook(
82 | () => ({ fullScreen } = useFullScreen({ element: testElementRef }))
83 | )
84 |
85 | expect(fullScreen).toBe(true)
86 | })
87 | })
88 |
89 | describe('without options', () => {
90 | it('uses document.fullscreenElement', () => {
91 | let fullScreen
92 | document.fullscreenElement = true
93 | renderHook(() => ({ fullScreen } = useFullScreen()))
94 |
95 | expect(fullScreen).toBe(true)
96 | })
97 |
98 | it('uses document.mozFullScreenElement', () => {
99 | let fullScreen
100 | document.mozFullScreenElement = true
101 | renderHook(() => ({ fullScreen } = useFullScreen()))
102 |
103 | expect(fullScreen).toBe(true)
104 | })
105 |
106 | it('uses document.webkitFullscreenElement', () => {
107 | let fullScreen
108 | document.webkitFullscreenElement = true
109 | renderHook(() => ({ fullScreen } = useFullScreen()))
110 |
111 | expect(fullScreen).toBe(true)
112 | })
113 |
114 | it('uses document.msFullscreenElement', () => {
115 | let fullScreen
116 | document.msFullscreenElement = true
117 | renderHook(() => ({ fullScreen } = useFullScreen()))
118 |
119 | expect(fullScreen).toBe(true)
120 | })
121 |
122 | it('uses document.fullscreen', () => {
123 | let fullScreen
124 | document.fullscreen = true
125 | renderHook(() => ({ fullScreen } = useFullScreen()))
126 |
127 | expect(fullScreen).toBe(true)
128 | })
129 |
130 | it('uses document.mozFullScreen', () => {
131 | let fullScreen
132 | document.mozFullScreen = true
133 | renderHook(() => ({ fullScreen } = useFullScreen()))
134 |
135 | expect(fullScreen).toBe(true)
136 | })
137 |
138 | it('uses document.webkitIsFullScreen', () => {
139 | let fullScreen
140 | document.webkitIsFullScreen = true
141 | renderHook(() => ({ fullScreen } = useFullScreen()))
142 |
143 | expect(fullScreen).toBe(true)
144 | })
145 |
146 | it('uses document.fullScreenMode', () => {
147 | let fullScreen
148 | document.fullScreenMode = true
149 | renderHook(() => ({ fullScreen } = useFullScreen()))
150 |
151 | expect(fullScreen).toBe(true)
152 | })
153 |
154 | it('defaults initial state to false', () => {
155 | let fullScreen
156 | renderHook(() => ({ fullScreen } = useFullScreen()))
157 |
158 | expect(fullScreen).toBe(false)
159 | })
160 | })
161 | })
162 |
163 | describe('change events', () => {
164 | it('updates state when "webkitfullscreenchange" fires', () => {
165 | let fullScreen
166 | document.webkitIsFullScreen = false
167 | renderHook(() => ({ fullScreen } = useFullScreen()))
168 |
169 | document.webkitIsFullScreen = true
170 |
171 | act(() => {
172 | fireEvent(
173 | document,
174 | new Event('webkitfullscreenchange', {
175 | bubbles: false,
176 | cancelable: false
177 | })
178 | )
179 | })
180 |
181 | expect(fullScreen).toBe(true)
182 | })
183 |
184 | it('updates state when "mozfullscreenchange" fires', () => {
185 | let fullScreen
186 | document.mozFullScreen = false
187 | renderHook(() => ({ fullScreen } = useFullScreen()))
188 |
189 | document.mozFullScreen = true
190 |
191 | act(() => {
192 | fireEvent(
193 | document,
194 | new Event('mozfullscreenchange', {
195 | bubbles: false,
196 | cancelable: false
197 | })
198 | )
199 | })
200 |
201 | expect(fullScreen).toBe(true)
202 | })
203 |
204 | it('updates state when "msfullscreenchange" fires', () => {
205 | let fullScreen
206 | document.fullScreenMode = false
207 | renderHook(() => ({ fullScreen } = useFullScreen()))
208 |
209 | document.fullScreenMode = true
210 |
211 | act(() => {
212 | fireEvent(
213 | document,
214 | new Event('msfullscreenchange', {
215 | bubbles: false,
216 | cancelable: false
217 | })
218 | )
219 | })
220 |
221 | expect(fullScreen).toBe(true)
222 | })
223 |
224 | it('updates state when "MSFullscreenChange" fires', () => {
225 | let fullScreen
226 | document.fullScreenMode = false
227 | renderHook(() => ({ fullScreen } = useFullScreen()))
228 |
229 | document.fullScreenMode = true
230 |
231 | act(() => {
232 | fireEvent(
233 | document,
234 | new Event('MSFullscreenChange', {
235 | bubbles: false,
236 | cancelable: false
237 | })
238 | )
239 | })
240 |
241 | expect(fullScreen).toBe(true)
242 | })
243 |
244 | it('updates state when "fullscreenchange" fires', () => {
245 | let fullScreen
246 | document.fullscreen = false
247 |
248 | renderHook(() => ({ fullScreen } = useFullScreen()))
249 |
250 | document.fullscreen = true
251 |
252 | act(() => {
253 | fireEvent(
254 | document,
255 | new Event('fullscreenchange', {
256 | bubbles: false,
257 | cancelable: false
258 | })
259 | )
260 | })
261 |
262 | expect(fullScreen).toBe(true)
263 | })
264 | })
265 |
266 | describe('open', () => {
267 | it('calls requestFullScreen', () => {
268 | let open
269 | testElementRef.current.requestFullscreen = jest.fn()
270 | renderHook(() => ({ open } = useFullScreen({ element: testElementRef })))
271 |
272 | open()
273 |
274 | expect(testElementRef.current.requestFullscreen).toHaveBeenCalled()
275 | })
276 |
277 | it('calls mozRequestFullScreen', () => {
278 | let open
279 | testElementRef.current.mozRequestFullScreen = jest.fn()
280 | renderHook(() => ({ open } = useFullScreen({ element: testElementRef })))
281 |
282 | open()
283 |
284 | expect(testElementRef.current.mozRequestFullScreen).toHaveBeenCalled()
285 | })
286 |
287 | it('calls webkitRequestFullscreen', () => {
288 | let open
289 | testElementRef.current.webkitRequestFullscreen = jest.fn()
290 | renderHook(() => ({ open } = useFullScreen({ element: testElementRef })))
291 |
292 | open()
293 |
294 | expect(testElementRef.current.webkitRequestFullscreen).toHaveBeenCalled()
295 | })
296 |
297 | it('calls msRequestFullscreen', () => {
298 | let open
299 | testElementRef.current.msRequestFullscreen = jest.fn()
300 | renderHook(() => ({ open } = useFullScreen({ element: testElementRef })))
301 |
302 | open()
303 |
304 | expect(testElementRef.current.msRequestFullscreen).toHaveBeenCalled()
305 | })
306 | })
307 |
308 | describe('close', () => {
309 | beforeEach(() => {
310 | document.exitFullscreen = null
311 | document.mozCancelFullScreen = null
312 | document.webkitExitFullscreen = null
313 | document.msExitFullscreen = null
314 | })
315 |
316 | it('calls exitFullscreen', () => {
317 | let close
318 | document.exitFullscreen = jest.fn()
319 | renderHook(() => ({ close } = useFullScreen()))
320 |
321 | close()
322 |
323 | expect(document.exitFullscreen).toHaveBeenCalled()
324 | })
325 |
326 | it('calls mozCancelFullScreen', () => {
327 | let close
328 | document.mozCancelFullScreen = jest.fn()
329 | renderHook(() => ({ close } = useFullScreen()))
330 |
331 | close()
332 |
333 | expect(document.mozCancelFullScreen).toHaveBeenCalled()
334 | })
335 |
336 | it('calls webkitExitFullscreen', () => {
337 | let close
338 | document.webkitExitFullscreen = jest.fn()
339 | renderHook(() => ({ close } = useFullScreen()))
340 |
341 | close()
342 |
343 | expect(document.webkitExitFullscreen).toHaveBeenCalled()
344 | })
345 |
346 | it('calls msExitFullscreen', () => {
347 | let close
348 | document.msExitFullscreen = jest.fn()
349 | renderHook(() => ({ close } = useFullScreen()))
350 |
351 | close()
352 |
353 | expect(document.msExitFullscreen).toHaveBeenCalled()
354 | })
355 | })
356 | })
357 |
358 | describe('useFullScreenBrowser', () => {
359 | it('sets initial state to false if it is not full screen', () => {
360 | window.screenTop = 10
361 | window.screenY = 10
362 | jest.spyOn(window.screen, 'width', 'get').mockReturnValue(1000)
363 | jest.spyOn(window.screen, 'height', 'get').mockReturnValue(1000)
364 | window.innerWidth = 500
365 | window.innerHeight = 500
366 |
367 | let fullScreen
368 | renderHook(() => ({ fullScreen } = useFullScreenBrowser()))
369 |
370 | expect(fullScreen).toBe(false)
371 | })
372 |
373 | it('sets initial state to true if window and inner sizes are the same', () => {
374 | window.screenTop = 10
375 | window.screenY = 10
376 | jest.spyOn(window.screen, 'width', 'get').mockReturnValue(500)
377 | jest.spyOn(window.screen, 'height', 'get').mockReturnValue(500)
378 | window.innerWidth = 500
379 | window.innerHeight = 500
380 |
381 | let fullScreen
382 | renderHook(() => ({ fullScreen } = useFullScreenBrowser()))
383 |
384 | expect(fullScreen).toBe(true)
385 | })
386 |
387 | it('sets initial state to true if there is no screenTop and no screenY', () => {
388 | window.screenTop = 0
389 | window.screenY = 0
390 | jest.spyOn(window.screen, 'width', 'get').mockReturnValue(1000)
391 | jest.spyOn(window.screen, 'height', 'get').mockReturnValue(1000)
392 | window.innerWidth = 500
393 | window.innerHeight = 500
394 |
395 | let fullScreen
396 | renderHook(() => ({ fullScreen } = useFullScreenBrowser()))
397 |
398 | expect(fullScreen).toBe(true)
399 | })
400 | })
401 |
--------------------------------------------------------------------------------
/test/unit/hooks/media-controls.unit.test.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { cleanup } from '@testing-library/react-hooks'
3 | import { act, fireEvent, render } from '@testing-library/react'
4 |
5 | import { useMediaControls } from '../../../src'
6 |
7 | // ref
8 | let mediaElementRef
9 |
10 | // hook state
11 | let currentTime,
12 | mute,
13 | muted,
14 | unmute,
15 | pause,
16 | paused,
17 | play,
18 | restart,
19 | seek,
20 | setVolume,
21 | stop,
22 | volume
23 |
24 | // spys
25 | let mediaElementPlaySpy, mediaElementPauseSpy
26 |
27 | // getters
28 | let pausedGetter
29 |
30 | beforeEach(() => {
31 | act(() => {
32 | testMediaControlsHook(
33 | () =>
34 | ({
35 | currentTime,
36 | mute,
37 | muted,
38 | unmute,
39 | pause,
40 | paused,
41 | play,
42 | restart,
43 | seek,
44 | setVolume,
45 | stop,
46 | volume
47 | } = useMediaControls(mediaElementRef))
48 | )
49 | })
50 |
51 | mediaElementPlaySpy = jest
52 | .spyOn(mediaElementRef.current, 'play')
53 | .mockImplementation(() => {})
54 | mediaElementPauseSpy = jest
55 | .spyOn(mediaElementRef.current, 'pause')
56 | .mockImplementation(() => {})
57 |
58 | pausedGetter = jest.spyOn(mediaElementRef.current, 'paused', 'get')
59 | })
60 |
61 | afterEach(cleanup)
62 |
63 | function TestMediaControlsHook({ callback }) {
64 | mediaElementRef = useRef(null)
65 | callback()
66 | return (
67 |
72 | )
73 | }
74 |
75 | function testMediaControlsHook(callback) {
76 | render( )
77 | }
78 |
79 | describe('useMediaControls', () => {
80 | it('sets initial state to match the media element', () => {
81 | expect(currentTime).toBe(0)
82 | expect(muted).toBe(true)
83 | expect(paused).toBe(true)
84 | expect(volume).toBe(1)
85 | })
86 |
87 | describe('plays', () => {
88 | it('when play() is called', () => {
89 | play()
90 | expect(mediaElementPlaySpy).toHaveBeenCalled()
91 | })
92 |
93 | it('when a "play" event is triggered', () => {
94 | pausedGetter.mockReturnValue(false)
95 |
96 | act(() => {
97 | fireEvent(
98 | mediaElementRef.current,
99 | new Event('play', {
100 | bubbles: false,
101 | cancelable: false
102 | })
103 | )
104 | })
105 |
106 | expect(paused).toBe(false)
107 | })
108 |
109 | it('when a "playing" event is triggered', () => {
110 | pausedGetter.mockReturnValue(false)
111 |
112 | act(() => {
113 | fireEvent(
114 | mediaElementRef.current,
115 | new Event('playing', {
116 | bubbles: false,
117 | cancelable: false
118 | })
119 | )
120 | })
121 |
122 | expect(paused).toBe(false)
123 | })
124 | })
125 |
126 | describe('pauses', () => {
127 | // play the media before pausing it
128 | beforeEach(() => {
129 | pausedGetter.mockReturnValue(false)
130 |
131 | act(() => {
132 | fireEvent(
133 | mediaElementRef.current,
134 | new Event('play', {
135 | bubbles: false,
136 | cancelable: false
137 | })
138 | )
139 | })
140 | })
141 |
142 | it('when pause() is called', () => {
143 | pause()
144 | expect(mediaElementPauseSpy).toHaveBeenCalled()
145 | })
146 |
147 | it('when a "pause" event is triggered', () => {
148 | pausedGetter.mockReturnValue(true)
149 |
150 | act(() => {
151 | fireEvent(
152 | mediaElementRef.current,
153 | new Event('pause', {
154 | bubbles: false,
155 | cancelable: false
156 | })
157 | )
158 | })
159 |
160 | expect(paused).toBe(true)
161 | })
162 |
163 | it('when a "waiting" event is triggered', () => {
164 | pausedGetter.mockReturnValue(true)
165 |
166 | act(() => {
167 | fireEvent(
168 | mediaElementRef.current,
169 | new Event('waiting', {
170 | bubbles: false,
171 | cancelable: false
172 | })
173 | )
174 | })
175 |
176 | expect(paused).toBe(true)
177 | })
178 | })
179 |
180 | describe('sets volume', () => {
181 | beforeEach(() => {
182 | act(() => {
183 | unmute()
184 | })
185 | })
186 |
187 | it('when setVolume() is called', () => {
188 | expect(volume).toBe(1)
189 | expect(mediaElementRef.current.volume).toBe(1)
190 |
191 | act(() => {
192 | setVolume(0.5)
193 | })
194 |
195 | expect(volume).toBe(0.5)
196 | expect(mediaElementRef.current.volume).toBe(0.5)
197 | })
198 |
199 | it('when a "volumechange" event is triggered', () => {
200 | expect(volume).toBe(1)
201 | expect(mediaElementRef.current.volume).toBe(1)
202 |
203 | act(() => {
204 | mediaElementRef.current.volume = 0.2 // triggers "volumechange" event
205 | })
206 |
207 | expect(volume).toBe(0.2)
208 | expect(mediaElementRef.current.volume).toBe(0.2)
209 | })
210 |
211 | it('when setVolume() attempts to set a value > 1', () => {
212 | expect(volume).toBe(1)
213 | expect(mediaElementRef.current.volume).toBe(1)
214 |
215 | act(() => {
216 | setVolume(1.1)
217 | })
218 |
219 | expect(volume).toBe(1)
220 | expect(mediaElementRef.current.volume).toBe(1)
221 | })
222 |
223 | it('when setVolume() attempts to set a value < 0', () => {
224 | expect(volume).toBe(1)
225 | expect(mediaElementRef.current.volume).toBe(1)
226 |
227 | act(() => {
228 | setVolume(-0.1)
229 | })
230 |
231 | expect(volume).toBe(0)
232 | expect(mediaElementRef.current.volume).toBe(0)
233 | })
234 | })
235 |
236 | describe('mutes', () => {
237 | beforeEach(() => {
238 | act(() => {
239 | unmute()
240 | })
241 | })
242 |
243 | it('when mute() is called', () => {
244 | expect(muted).toBe(false)
245 | expect(mediaElementRef.current.muted).toBe(false)
246 |
247 | act(() => {
248 | mute()
249 | })
250 |
251 | expect(muted).toBe(true)
252 | expect(mediaElementRef.current.muted).toBe(true)
253 | })
254 |
255 | it('when setVolume() is called with 0', () => {
256 | expect(muted).toBe(false)
257 | expect(mediaElementRef.current.muted).toBe(false)
258 |
259 | act(() => {
260 | setVolume(0)
261 | })
262 |
263 | expect(muted).toBe(true)
264 | expect(mediaElementRef.current.muted).toBe(true)
265 | })
266 |
267 | it('when unmute() is called after setVolume() is called with 0', () => {
268 | expect(muted).toBe(false)
269 | expect(mediaElementRef.current.muted).toBe(false)
270 |
271 | const originalVolume = mediaElementRef.current.volume
272 |
273 | act(() => {
274 | setVolume(0)
275 | })
276 |
277 | expect(muted).toBe(true)
278 | expect(mediaElementRef.current.muted).toBe(true)
279 |
280 | act(() => {
281 | unmute()
282 | })
283 |
284 | expect(muted).toBe(false)
285 | expect(mediaElementRef.current.muted).toBe(false)
286 | expect(volume).toBe(originalVolume)
287 | expect(mediaElementRef.current.volume).toBe(originalVolume)
288 | })
289 |
290 | it('when mute() or unmute() is called directly, volume does not change', () => {
291 | setVolume(1)
292 | expect(volume).toBe(1)
293 | expect(mediaElementRef.current.volume).toBe(1)
294 | expect(muted).toBe(false)
295 | expect(mediaElementRef.current.muted).toBe(false)
296 |
297 | act(() => {
298 | mute()
299 | })
300 |
301 | expect(volume).toBe(1)
302 | expect(mediaElementRef.current.volume).toBe(1)
303 | expect(muted).toBe(true)
304 | expect(mediaElementRef.current.muted).toBe(true)
305 |
306 | act(() => {
307 | unmute()
308 | })
309 |
310 | expect(volume).toBe(1)
311 | expect(mediaElementRef.current.volume).toBe(1)
312 | expect(muted).toBe(false)
313 | expect(mediaElementRef.current.muted).toBe(false)
314 | })
315 | })
316 |
317 | describe('unmutes', () => {
318 | beforeEach(() => {
319 | act(() => {
320 | mute()
321 | })
322 | })
323 |
324 | it('when unmute() is called', () => {
325 | expect(muted).toBe(true)
326 | expect(mediaElementRef.current.muted).toBe(true)
327 |
328 | act(() => {
329 | unmute()
330 | })
331 |
332 | expect(muted).toBe(false)
333 | expect(mediaElementRef.current.muted).toBe(false)
334 | })
335 |
336 | it('when setVolume() is called with >0', () => {
337 | expect(muted).toBe(true)
338 | expect(mediaElementRef.current.muted).toBe(true)
339 |
340 | act(() => {
341 | setVolume(1)
342 | })
343 |
344 | expect(muted).toBe(false)
345 | expect(mediaElementRef.current.muted).toBe(false)
346 | })
347 | })
348 |
349 | describe('seeks', () => {
350 | it('when seek() is called', () => {
351 | expect(currentTime).toBe(0)
352 | expect(mediaElementRef.current.currentTime).toBe(0)
353 |
354 | act(() => {
355 | seek(2)
356 | })
357 |
358 | mediaElementRef.current.addEventListener('seeked', () => {
359 | expect(currentTime).toBe(2)
360 | expect(mediaElementRef.current.currentTime).toBe(2)
361 | })
362 | })
363 |
364 | it('when a "seeked" event is triggered', () => {
365 | mediaElementRef.current.currentTime = 2
366 |
367 | act(() => {
368 | fireEvent(
369 | mediaElementRef.current,
370 | new Event('seeked', {
371 | bubbles: false,
372 | cancelable: false
373 | })
374 | )
375 | })
376 |
377 | expect(currentTime).toBe(2)
378 | expect(mediaElementRef.current.currentTime).toBe(2)
379 | })
380 |
381 | it('when a "timeupdate" event is triggered', () => {
382 | mediaElementRef.current.currentTime = 2
383 |
384 | act(() => {
385 | fireEvent(
386 | mediaElementRef.current,
387 | new Event('timeupdate', {
388 | bubbles: false,
389 | cancelable: false
390 | })
391 | )
392 | })
393 |
394 | expect(currentTime).toBe(2)
395 | expect(mediaElementRef.current.currentTime).toBe(2)
396 | })
397 | })
398 |
399 | describe('stops', () => {
400 | // play the media before stopping it
401 | beforeEach(() => {
402 | pausedGetter.mockReturnValue(false)
403 |
404 | act(() => {
405 | fireEvent(
406 | mediaElementRef.current,
407 | new Event('play', {
408 | bubbles: false,
409 | cancelable: false
410 | })
411 | )
412 | })
413 |
414 | mediaElementRef.current.currentTime = 2
415 |
416 | act(() => {
417 | fireEvent(
418 | mediaElementRef.current,
419 | new Event('seeked', {
420 | bubbles: false,
421 | cancelable: false
422 | })
423 | )
424 | })
425 | })
426 |
427 | it('when stop() is called', () => {
428 | expect(paused).toBe(false)
429 | expect(currentTime).toBeGreaterThan(0)
430 | expect(mediaElementRef.current.currentTime).toBeGreaterThan(0)
431 |
432 | act(() => {
433 | stop()
434 | })
435 |
436 | mediaElementRef.current.addEventListener('seeked', () => {
437 | expect(paused).toBe(true)
438 | expect(currentTime).toBe(0)
439 | expect(mediaElementRef.current.currentTime).toBe(0)
440 | })
441 | })
442 | })
443 |
444 | describe('restarts', () => {
445 | // end the media before restarting it
446 | beforeEach(() => {
447 | mediaElementRef.current.currentTime = 30
448 |
449 | act(() => {
450 | fireEvent(
451 | mediaElementRef.current,
452 | new Event('seeked', {
453 | bubbles: false,
454 | cancelable: false
455 | })
456 | )
457 | })
458 | })
459 |
460 | it('when restart() is called', () => {
461 | expect(paused).toBe(true)
462 | expect(currentTime).toBe(30)
463 | expect(mediaElementRef.current.currentTime).toBe(30)
464 |
465 | act(() => {
466 | restart()
467 | })
468 |
469 | mediaElementRef.current.addEventListener('seeked', () => {
470 | expect(paused).toBe(false)
471 | expect(currentTime).toBeGreaterThan(0)
472 | expect(mediaElementRef.current.currentTime).toBeGreaterThan(0)
473 | })
474 | })
475 | })
476 | })
477 |
--------------------------------------------------------------------------------