Skip to content

Commit b3fd1bb

Browse files
eveningwaterliuyib
andauthored
feat(useFullscreen): support page fullscreen (#1893)
* 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 <[email protected]>
1 parent fa9afea commit b3fd1bb

File tree

5 files changed

+216
-61
lines changed

5 files changed

+216
-61
lines changed

packages/hooks/src/useFullscreen/__tests__/index.test.ts

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { renderHook, act } from '@testing-library/react';
2-
import useFullscreen, { Options } from '../index';
2+
import useFullscreen from '../index';
3+
import type { Options } from '../index';
34
import type { BasicTarget } from '../../utils/domTarget';
45

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

1415
describe('useFullscreen', () => {
15-
beforeAll(() => {
16+
beforeEach(() => {
17+
document.body.appendChild(targetEl);
1618
jest.spyOn(HTMLElement.prototype, 'requestFullscreen').mockImplementation(() => {
1719
Object.defineProperty(document, 'fullscreenElement', {
1820
value: targetEl,
@@ -38,6 +40,7 @@ describe('useFullscreen', () => {
3840
});
3941

4042
afterEach(() => {
43+
document.body.removeChild(targetEl);
4144
events.fullscreenchange.clear();
4245
});
4346

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

5760
exitFullscreen();
5861
act(() => {
59-
events['fullscreenchange'].forEach((fn: any) => fn());
62+
events.fullscreenchange.forEach((fn: any) => fn());
6063
});
6164
expect(result.current[0]).toBe(false);
6265
});
@@ -66,13 +69,13 @@ describe('useFullscreen', () => {
6669
const { toggleFullscreen } = result.current[1];
6770
toggleFullscreen();
6871
act(() => {
69-
events['fullscreenchange'].forEach((fn: any) => fn());
72+
events.fullscreenchange.forEach((fn: any) => fn());
7073
});
7174
expect(result.current[0]).toBe(true);
7275

7376
toggleFullscreen();
7477
act(() => {
75-
events['fullscreenchange'].forEach((fn: any) => fn());
78+
events.fullscreenchange.forEach((fn: any) => fn());
7679
});
7780
expect(result.current[0]).toBe(false);
7881
});
@@ -87,33 +90,83 @@ describe('useFullscreen', () => {
8790
const { toggleFullscreen } = result.current[1];
8891
toggleFullscreen();
8992
act(() => {
90-
events['fullscreenchange'].forEach((fn: any) => fn());
93+
events.fullscreenchange.forEach((fn: any) => fn());
9194
});
9295
expect(onEnter).toBeCalled();
9396

9497
toggleFullscreen();
9598
act(() => {
96-
events['fullscreenchange'].forEach((fn: any) => fn());
99+
events.fullscreenchange.forEach((fn: any) => fn());
97100
});
98101
expect(onExit).toBeCalled();
99102
});
100103

101-
it('enterFullscreen should not work when target is not element', () => {
102-
const { result } = setup(null);
103-
const { enterFullscreen } = result.current[1];
104+
it('onExit/onEnter should not be called', () => {
105+
const onExit = jest.fn();
106+
const onEnter = jest.fn();
107+
const { result } = setup(targetEl, {
108+
onExit,
109+
onEnter,
110+
});
111+
const { exitFullscreen, enterFullscreen } = result.current[1];
112+
113+
// `onExit` should not be called when not full screen
114+
exitFullscreen();
115+
act(() => events.fullscreenchange.forEach((fn: any) => fn()));
116+
expect(onExit).not.toBeCalled();
117+
118+
// Enter full screen
104119
enterFullscreen();
105-
expect(events.fullscreenchange.size).toBe(0);
120+
act(() => events.fullscreenchange.forEach((fn: any) => fn()));
121+
expect(onEnter).toBeCalled();
122+
onEnter.mockReset();
123+
124+
// `onEnter` should not be called when full screen
125+
enterFullscreen();
126+
// There is no need to write: `act(() => events.fullscreenchange.forEach((fn: any) => fn()));`,
127+
// because in a real browser, if it is already in full screen, calling `enterFullscreen` again
128+
// will not trigger the `change` event.
129+
expect(onEnter).not.toBeCalled();
106130
});
107131

108-
it('exitFullscreen should not work when not in full screen', () => {
132+
it('pageFullscreen should be work', () => {
133+
const PAGE_FULLSCREEN_CLASS_NAME = 'test-page-fullscreen';
134+
const PAGE_FULLSCREEN_Z_INDEX = 101;
109135
const onExit = jest.fn();
110-
const { result } = setup(targetEl, { onExit });
111-
const { exitFullscreen } = result.current[1];
112-
exitFullscreen();
113-
act(() => {
114-
events['fullscreenchange'].forEach((fn: any) => fn());
136+
const onEnter = jest.fn();
137+
const { result } = setup(targetEl, {
138+
onExit,
139+
onEnter,
140+
pageFullscreen: {
141+
className: PAGE_FULLSCREEN_CLASS_NAME,
142+
zIndex: PAGE_FULLSCREEN_Z_INDEX,
143+
},
115144
});
116-
expect(onExit).not.toBeCalled();
145+
const { toggleFullscreen } = result.current[1];
146+
const getStyleEl = () => targetEl.querySelector('style');
147+
148+
act(() => toggleFullscreen());
149+
expect(result.current[0]).toBe(true);
150+
expect(onEnter).toBeCalled();
151+
expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeTruthy();
152+
expect(getStyleEl()).not.toBeNull();
153+
expect(getStyleEl()?.textContent).toContain(`z-index: ${PAGE_FULLSCREEN_Z_INDEX}`);
154+
expect(getStyleEl()?.getAttribute('id')).toBe(PAGE_FULLSCREEN_CLASS_NAME);
155+
156+
act(() => toggleFullscreen());
157+
expect(result.current[0]).toBe(false);
158+
expect(onExit).toBeCalled();
159+
expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeFalsy();
160+
expect(getStyleEl()).toBeNull();
161+
expect(getStyleEl()?.textContent).toBeUndefined();
162+
expect(getStyleEl()?.getAttribute('id')).toBeUndefined();
163+
});
164+
165+
it('enterFullscreen should not work when target is not element', () => {
166+
const { result } = setup(null);
167+
const { enterFullscreen } = result.current[1];
168+
enterFullscreen();
169+
expect(events.fullscreenchange.size).toBe(0);
117170
});
118171

119172
it('should remove event listener when unmount', () => {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* title: Page full screen
3+
*
4+
* title.zh-CN: 页面全屏
5+
*/
6+
7+
import React, { useRef } from 'react';
8+
import { useFullscreen } from 'ahooks';
9+
10+
export default () => {
11+
const ref = useRef(null);
12+
const [isFullscreen, { toggleFullscreen, enterFullscreen, exitFullscreen }] = useFullscreen(ref, {
13+
pageFullscreen: true,
14+
});
15+
16+
return (
17+
<div style={{ background: 'white' }}>
18+
<div ref={ref} style={{ background: '#4B6BCD', padding: 12 }}>
19+
<div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div>
20+
<button type="button" onClick={enterFullscreen}>
21+
enterFullscreen
22+
</button>
23+
<button type="button" onClick={exitFullscreen} style={{ margin: '0 8px' }}>
24+
exitFullscreen
25+
</button>
26+
<button type="button" onClick={toggleFullscreen}>
27+
toggleFullscreen
28+
</button>
29+
</div>
30+
</div>
31+
);
32+
};

packages/hooks/src/useFullscreen/index.en-US.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@ manages DOM full screen.
1717

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

20+
### Page full screen
21+
22+
<code src="./demo/demo3.tsx" />
23+
2024
## API
2125

2226
```typescript
23-
const [
24-
isFullscreen,
25-
{
26-
enterFullscreen,
27-
exitFullscreen,
28-
toggleFullscreen,
29-
isEnabled,
30-
}] = useFullScreen(
31-
target,
32-
options?: Options
33-
);
27+
const [isFullscreen, {
28+
enterFullscreen,
29+
exitFullscreen,
30+
toggleFullscreen,
31+
isEnabled,
32+
}] = useFullScreen(
33+
target,
34+
options?: Options
35+
);
3436
```
3537

3638
### Params
@@ -42,10 +44,11 @@ const [
4244

4345
### Options
4446

45-
| Property | Description | Type | Default |
46-
| -------- | ------------------------- | ------------ | ------- |
47-
| onExit | Exit full screen trigger | `() => void` | - |
48-
| onEnter | Enter full screen trigger | `() => void` | - |
47+
| Property | Description | Type | Default |
48+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------- |
49+
| onExit | Exit full screen trigger | `() => void` | - |
50+
| onEnter | Enter full screen trigger | `() => void` | - |
51+
| 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` |
4952

5053
### Result
5154

packages/hooks/src/useFullscreen/index.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,90 @@ import useMemoizedFn from '../useMemoizedFn';
55
import useUnmount from '../useUnmount';
66
import type { BasicTarget } from '../utils/domTarget';
77
import { getTargetElement } from '../utils/domTarget';
8+
import { isBoolean } from '../utils';
9+
10+
export interface PageFullscreenOptions {
11+
className?: string;
12+
zIndex?: number;
13+
}
814

915
export interface Options {
1016
onExit?: () => void;
1117
onEnter?: () => void;
18+
pageFullscreen?: boolean | PageFullscreenOptions;
1219
}
1320

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

1726
const onExitRef = useLatest(onExit);
1827
const onEnterRef = useLatest(onEnter);
1928

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

22-
const onChange = () => {
31+
const invokeCallback = (fullscreen: boolean) => {
32+
if (fullscreen) {
33+
onEnterRef.current?.();
34+
} else {
35+
onExitRef.current?.();
36+
}
37+
};
38+
39+
// Memoized, otherwise it will be listened multiple times.
40+
const onScreenfullChange = useMemoizedFn(() => {
2341
if (screenfull.isEnabled) {
2442
const el = getTargetElement(target);
2543

2644
if (!screenfull.element) {
27-
onExitRef.current?.();
45+
invokeCallback(false);
2846
setState(false);
29-
screenfull.off('change', onChange);
47+
screenfull.off('change', onScreenfullChange);
3048
} else {
3149
const isFullscreen = screenfull.element === el;
32-
if (isFullscreen) {
33-
onEnterRef.current?.();
34-
} else {
35-
onExitRef.current?.();
36-
}
50+
51+
invokeCallback(isFullscreen);
3752
setState(isFullscreen);
3853
}
3954
}
55+
});
56+
57+
const togglePageFullscreen = (fullscreen: boolean) => {
58+
const el = getTargetElement(target);
59+
if (!el) {
60+
return;
61+
}
62+
63+
let styleElem = document.getElementById(className);
64+
65+
if (fullscreen) {
66+
el.classList.add(className);
67+
68+
if (!styleElem) {
69+
styleElem = document.createElement('style');
70+
styleElem.setAttribute('id', className);
71+
styleElem.textContent = `
72+
.${className} {
73+
position: fixed; left: 0; top: 0; right: 0; bottom: 0;
74+
width: 100% !important; height: 100% !important;
75+
z-index: ${zIndex};
76+
}`;
77+
el.appendChild(styleElem);
78+
}
79+
} else {
80+
el.classList.remove(className);
81+
82+
if (styleElem) {
83+
styleElem.remove();
84+
}
85+
}
86+
87+
// Prevent repeated calls when the state is not changed.
88+
if (state !== fullscreen) {
89+
invokeCallback(fullscreen);
90+
setState(fullscreen);
91+
}
4092
};
4193

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

100+
if (pageFullscreen) {
101+
togglePageFullscreen(true);
102+
return;
103+
}
48104
if (screenfull.isEnabled) {
49105
try {
50106
screenfull.request(el);
51-
screenfull.on('change', onChange);
107+
screenfull.on('change', onScreenfullChange);
52108
} catch (error) {
53109
console.error(error);
54110
}
@@ -57,6 +113,14 @@ const useFullscreen = (target: BasicTarget, options?: Options) => {
57113

58114
const exitFullscreen = () => {
59115
const el = getTargetElement(target);
116+
if (!el) {
117+
return;
118+
}
119+
120+
if (pageFullscreen) {
121+
togglePageFullscreen(false);
122+
return;
123+
}
60124
if (screenfull.isEnabled && screenfull.element === el) {
61125
screenfull.exit();
62126
}
@@ -71,8 +135,8 @@ const useFullscreen = (target: BasicTarget, options?: Options) => {
71135
};
72136

73137
useUnmount(() => {
74-
if (screenfull.isEnabled) {
75-
screenfull.off('change', onChange);
138+
if (screenfull.isEnabled && !pageFullscreen) {
139+
screenfull.off('change', onScreenfullChange);
76140
}
77141
});
78142

0 commit comments

Comments
 (0)