Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(useFullscreen): support page fullscreen #1893

Merged
merged 18 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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