Skip to content

Commit

Permalink
feat(useFullscreen): support page fullscreen (#1893)
Browse files Browse the repository at this point in the history
* feat: 添加了默认状态值hook函数

* feat: 添加了修改hash值的hook函数

* fix: 修改了文档描述

* feat: useFullscreen添加了是否是浏览器全屏的配置

* refactor: support pass className and z-index

* docs: update demo

* docs: add detail explain for pageFullscreen

* chore: restore lock

* refactor: resolve confict

* style: rename

* chore: resolve warning in test case

* fix: onExit is invoked twice

* refactor: reuse logic

* refactor: prevent repeated calls

* refactor: update

* test: add case for pageFullscreen

* docs: fix typo

---------

Co-authored-by: liuyib <1656081615@qq.com>
  • Loading branch information
eveningwater and liuyib authored Mar 23, 2023
1 parent fa9afea commit b3fd1bb
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 61 deletions.
91 changes: 72 additions & 19 deletions packages/hooks/src/useFullscreen/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { renderHook, act } from '@testing-library/react';
import useFullscreen, { Options } from '../index';
import useFullscreen from '../index';
import type { Options } from '../index';
import type { BasicTarget } from '../../utils/domTarget';

const targetEl = document.createElement('div');
Expand All @@ -12,7 +13,8 @@ const setup = (target: BasicTarget, options?: Options) =>
renderHook(() => useFullscreen(target, options));

describe('useFullscreen', () => {
beforeAll(() => {
beforeEach(() => {
document.body.appendChild(targetEl);
jest.spyOn(HTMLElement.prototype, 'requestFullscreen').mockImplementation(() => {
Object.defineProperty(document, 'fullscreenElement', {
value: targetEl,
Expand All @@ -38,6 +40,7 @@ describe('useFullscreen', () => {
});

afterEach(() => {
document.body.removeChild(targetEl);
events.fullscreenchange.clear();
});

Expand All @@ -50,13 +53,13 @@ describe('useFullscreen', () => {
const { enterFullscreen, exitFullscreen } = result.current[1];
enterFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(result.current[0]).toBe(true);

exitFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(result.current[0]).toBe(false);
});
Expand All @@ -66,13 +69,13 @@ describe('useFullscreen', () => {
const { toggleFullscreen } = result.current[1];
toggleFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(result.current[0]).toBe(true);

toggleFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(result.current[0]).toBe(false);
});
Expand All @@ -87,33 +90,83 @@ describe('useFullscreen', () => {
const { toggleFullscreen } = result.current[1];
toggleFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(onEnter).toBeCalled();

toggleFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
events.fullscreenchange.forEach((fn: any) => fn());
});
expect(onExit).toBeCalled();
});

it('enterFullscreen should not work when target is not element', () => {
const { result } = setup(null);
const { enterFullscreen } = result.current[1];
it('onExit/onEnter should not be called', () => {
const onExit = jest.fn();
const onEnter = jest.fn();
const { result } = setup(targetEl, {
onExit,
onEnter,
});
const { exitFullscreen, enterFullscreen } = result.current[1];

// `onExit` should not be called when not full screen
exitFullscreen();
act(() => events.fullscreenchange.forEach((fn: any) => fn()));
expect(onExit).not.toBeCalled();

// Enter full screen
enterFullscreen();
expect(events.fullscreenchange.size).toBe(0);
act(() => events.fullscreenchange.forEach((fn: any) => fn()));
expect(onEnter).toBeCalled();
onEnter.mockReset();

// `onEnter` should not be called when full screen
enterFullscreen();
// There is no need to write: `act(() => events.fullscreenchange.forEach((fn: any) => fn()));`,
// because in a real browser, if it is already in full screen, calling `enterFullscreen` again
// will not trigger the `change` event.
expect(onEnter).not.toBeCalled();
});

it('exitFullscreen should not work when not in full screen', () => {
it('pageFullscreen should be work', () => {
const PAGE_FULLSCREEN_CLASS_NAME = 'test-page-fullscreen';
const PAGE_FULLSCREEN_Z_INDEX = 101;
const onExit = jest.fn();
const { result } = setup(targetEl, { onExit });
const { exitFullscreen } = result.current[1];
exitFullscreen();
act(() => {
events['fullscreenchange'].forEach((fn: any) => fn());
const onEnter = jest.fn();
const { result } = setup(targetEl, {
onExit,
onEnter,
pageFullscreen: {
className: PAGE_FULLSCREEN_CLASS_NAME,
zIndex: PAGE_FULLSCREEN_Z_INDEX,
},
});
expect(onExit).not.toBeCalled();
const { toggleFullscreen } = result.current[1];
const getStyleEl = () => targetEl.querySelector('style');

act(() => toggleFullscreen());
expect(result.current[0]).toBe(true);
expect(onEnter).toBeCalled();
expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeTruthy();
expect(getStyleEl()).not.toBeNull();
expect(getStyleEl()?.textContent).toContain(`z-index: ${PAGE_FULLSCREEN_Z_INDEX}`);
expect(getStyleEl()?.getAttribute('id')).toBe(PAGE_FULLSCREEN_CLASS_NAME);

act(() => toggleFullscreen());
expect(result.current[0]).toBe(false);
expect(onExit).toBeCalled();
expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeFalsy();
expect(getStyleEl()).toBeNull();
expect(getStyleEl()?.textContent).toBeUndefined();
expect(getStyleEl()?.getAttribute('id')).toBeUndefined();
});

it('enterFullscreen should not work when target is not element', () => {
const { result } = setup(null);
const { enterFullscreen } = result.current[1];
enterFullscreen();
expect(events.fullscreenchange.size).toBe(0);
});

it('should remove event listener when unmount', () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/hooks/src/useFullscreen/demo/demo3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* title: Page full screen
*
* title.zh-CN: 页面全屏
*/

import React, { useRef } from 'react';
import { useFullscreen } from 'ahooks';

export default () => {
const ref = useRef(null);
const [isFullscreen, { toggleFullscreen, enterFullscreen, exitFullscreen }] = useFullscreen(ref, {
pageFullscreen: true,
});

return (
<div style={{ background: 'white' }}>
<div ref={ref} style={{ background: '#4B6BCD', padding: 12 }}>
<div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div>
<button type="button" onClick={enterFullscreen}>
enterFullscreen
</button>
<button type="button" onClick={exitFullscreen} style={{ margin: '0 8px' }}>
exitFullscreen
</button>
<button type="button" onClick={toggleFullscreen}>
toggleFullscreen
</button>
</div>
</div>
);
};
33 changes: 18 additions & 15 deletions packages/hooks/src/useFullscreen/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ manages DOM full screen.

<code src="./demo/demo2.tsx" />

### Page full screen

<code src="./demo/demo3.tsx" />

## API

```typescript
const [
isFullscreen,
{
enterFullscreen,
exitFullscreen,
toggleFullscreen,
isEnabled,
}] = useFullScreen(
target,
options?: Options
);
const [isFullscreen, {
enterFullscreen,
exitFullscreen,
toggleFullscreen,
isEnabled,
}] = useFullScreen(
target,
options?: Options
);
```

### Params
Expand All @@ -42,10 +44,11 @@ const [

### Options

| Property | Description | Type | Default |
| -------- | ------------------------- | ------------ | ------- |
| onExit | Exit full screen trigger | `() => void` | - |
| onEnter | Enter full screen trigger | `() => void` | - |
| Property | Description | Type | Default |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------- |
| onExit | Exit full screen trigger | `() => void` | - |
| onEnter | Enter full screen trigger | `() => void` | - |
| pageFullscreen | Whether to enable full screen of page. If its type is object, it can set `className` and `z-index` of the full screen element | `boolean` \| `{ className?: string, zIndex: number }` | `false` |

### Result

Expand Down
88 changes: 76 additions & 12 deletions packages/hooks/src/useFullscreen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,90 @@ import useMemoizedFn from '../useMemoizedFn';
import useUnmount from '../useUnmount';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget';
import { isBoolean } from '../utils';

export interface PageFullscreenOptions {
className?: string;
zIndex?: number;
}

export interface Options {
onExit?: () => void;
onEnter?: () => void;
pageFullscreen?: boolean | PageFullscreenOptions;
}

const useFullscreen = (target: BasicTarget, options?: Options) => {
const { onExit, onEnter } = options || {};
const { onExit, onEnter, pageFullscreen = false } = options || {};
const { className = 'ahooks-page-fullscreen', zIndex = 999999 } =
isBoolean(pageFullscreen) || !pageFullscreen ? {} : pageFullscreen;

const onExitRef = useLatest(onExit);
const onEnterRef = useLatest(onEnter);

const [state, setState] = useState(false);

const onChange = () => {
const invokeCallback = (fullscreen: boolean) => {
if (fullscreen) {
onEnterRef.current?.();
} else {
onExitRef.current?.();
}
};

// Memoized, otherwise it will be listened multiple times.
const onScreenfullChange = useMemoizedFn(() => {
if (screenfull.isEnabled) {
const el = getTargetElement(target);

if (!screenfull.element) {
onExitRef.current?.();
invokeCallback(false);
setState(false);
screenfull.off('change', onChange);
screenfull.off('change', onScreenfullChange);
} else {
const isFullscreen = screenfull.element === el;
if (isFullscreen) {
onEnterRef.current?.();
} else {
onExitRef.current?.();
}

invokeCallback(isFullscreen);
setState(isFullscreen);
}
}
});

const togglePageFullscreen = (fullscreen: boolean) => {
const el = getTargetElement(target);
if (!el) {
return;
}

let styleElem = document.getElementById(className);

if (fullscreen) {
el.classList.add(className);

if (!styleElem) {
styleElem = document.createElement('style');
styleElem.setAttribute('id', className);
styleElem.textContent = `
.${className} {
position: fixed; left: 0; top: 0; right: 0; bottom: 0;
width: 100% !important; height: 100% !important;
z-index: ${zIndex};
}`;
el.appendChild(styleElem);
}
} else {
el.classList.remove(className);

if (styleElem) {
styleElem.remove();
}
}

// Prevent repeated calls when the state is not changed.
if (state !== fullscreen) {
invokeCallback(fullscreen);
setState(fullscreen);
}
};

const enterFullscreen = () => {
Expand All @@ -45,10 +97,14 @@ const useFullscreen = (target: BasicTarget, options?: Options) => {
return;
}

if (pageFullscreen) {
togglePageFullscreen(true);
return;
}
if (screenfull.isEnabled) {
try {
screenfull.request(el);
screenfull.on('change', onChange);
screenfull.on('change', onScreenfullChange);
} catch (error) {
console.error(error);
}
Expand All @@ -57,6 +113,14 @@ const useFullscreen = (target: BasicTarget, options?: Options) => {

const exitFullscreen = () => {
const el = getTargetElement(target);
if (!el) {
return;
}

if (pageFullscreen) {
togglePageFullscreen(false);
return;
}
if (screenfull.isEnabled && screenfull.element === el) {
screenfull.exit();
}
Expand All @@ -71,8 +135,8 @@ const useFullscreen = (target: BasicTarget, options?: Options) => {
};

useUnmount(() => {
if (screenfull.isEnabled) {
screenfull.off('change', onChange);
if (screenfull.isEnabled && !pageFullscreen) {
screenfull.off('change', onScreenfullChange);
}
});

Expand Down
Loading

0 comments on commit b3fd1bb

Please sign in to comment.