Skip to content

Commit a470f80

Browse files
authored
feat: add ScrollLocker (#179)
* feat: add ScrollLocker * fix if set style empty * add mobile support * fix test * support container * fix lost this and container not scroll * add docs * revert switchScrollingEffect
1 parent 37aacd6 commit a470f80

File tree

6 files changed

+343
-37
lines changed

6 files changed

+343
-37
lines changed

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"semi": true,
44
"singleQuote": true,
55
"tabWidth": 2,
6-
"trailingComma": "all"
6+
"trailingComma": "all",
7+
"arrowParens": "avoid"
78
}

README.md

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin';
5858

5959
Fields in `config` and their meanings.
6060

61-
| Field | Type | Description | Default |
62-
|-------|------|-------------|---------|
63-
| autoMount | boolean | Whether to render component into container automatically | true |
64-
| autoDestroy | boolean | Whether to remove container automatically while the component is unmounted | true |
65-
| isVisible | (instance): boolean | A function to get current visibility of the component | - |
66-
| isForceRender | (instance): boolean | A function to determine whether to render popup even it's not visible | - |
67-
| getComponent | (instance, extra): ReactNode | A function to get the component which will be rendered into container | - |
68-
| getContainer | (instance): HTMLElement | A function to get the container | |
61+
| Field | Type | Description | Default |
62+
| ------------- | ---------------------------- | -------------------------------------------------------------------------- | ------- |
63+
| autoMount | boolean | Whether to render component into container automatically | true |
64+
| autoDestroy | boolean | Whether to remove container automatically while the component is unmounted | true |
65+
| isVisible | (instance): boolean | A function to get current visibility of the component | - |
66+
| isForceRender | (instance): boolean | A function to determine whether to render popup even it's not visible | - |
67+
| getComponent | (instance, extra): ReactNode | A function to get the component which will be rendered into container | - |
68+
| getContainer | (instance): HTMLElement | A function to get the container | |
6969

7070
### Portal
7171

@@ -77,11 +77,10 @@ import Portal from 'rc-util/lib/Portal';
7777

7878
Props:
7979

80-
| Prop | Type | Description | Default |
81-
|-------|------|-------------|---------|
82-
| children | ReactChildren | Content render to the container | - |
83-
| getContainer | (): HTMLElement | A function to get the container | - |
84-
80+
| Prop | Type | Description | Default |
81+
| ------------ | --------------- | ------------------------------- | ------- |
82+
| children | ReactChildren | Content render to the container | - |
83+
| getContainer | (): HTMLElement | A function to get the container | - |
8584

8685
### getScrollBarSize
8786

@@ -190,9 +189,9 @@ import canUseDom from 'rc-util/lib/Dom/canUseDom';
190189

191190
A collection of functions to operate DOM nodes' class name.
192191

193-
* `hasClass(node: HTMLElement, className: string): boolean`
194-
* `addClass(node: HTMLElement, className: string): void`
195-
* `removeClass(node: HTMLElement, className: string): void`
192+
- `hasClass(node: HTMLElement, className: string): boolean`
193+
- `addClass(node: HTMLElement, className: string): void`
194+
- `removeClass(node: HTMLElement, className: string): void`
196195

197196
```jsx
198197
import cssClass from 'rc-util/lib/Dom/class;
@@ -212,14 +211,14 @@ import contains from 'rc-util/lib/Dom/contains';
212211
213212
A collection of functions to get or set css styles.
214213
215-
* `get(node: HTMLElement, name?: string): any`
216-
* `set(node: HTMLElement, name?: string, value: any) | set(node, object)`
217-
* `getOuterWidth(el: HTMLElement): number`
218-
* `getOuterHeight(el: HTMLElement): number`
219-
* `getDocSize(): { width: number, height: number }`
220-
* `getClientSize(): { width: number, height: number }`
221-
* `getScroll(): { scrollLeft: number, scrollTop: number }`
222-
* `getOffset(node: HTMLElement): { left: number, top: number }`
214+
- `get(node: HTMLElement, name?: string): any`
215+
- `set(node: HTMLElement, name?: string, value: any) | set(node, object)`
216+
- `getOuterWidth(el: HTMLElement): number`
217+
- `getOuterHeight(el: HTMLElement): number`
218+
- `getDocSize(): { width: number, height: number }`
219+
- `getClientSize(): { width: number, height: number }`
220+
- `getScroll(): { scrollLeft: number, scrollTop: number }`
221+
- `getOffset(node: HTMLElement): { left: number, top: number }`
223222
224223
```jsx
225224
import css from 'rc-util/lib/Dom/css';
@@ -229,11 +228,11 @@ import css from 'rc-util/lib/Dom/css';
229228
230229
A collection of functions to operate focus status of DOM node.
231230
232-
* `saveLastFocusNode(): void`
233-
* `clearLastFocusNode(): void`
234-
* `backLastFocusNode(): void`
235-
* `getFocusNodeList(node: HTMLElement): HTMLElement[]` get a list of focusable nodes from the subtree of node.
236-
* `limitTabRange(node: HTMLElement, e: Event): void`
231+
- `saveLastFocusNode(): void`
232+
- `clearLastFocusNode(): void`
233+
- `backLastFocusNode(): void`
234+
- `getFocusNodeList(node: HTMLElement): HTMLElement[]` get a list of focusable nodes from the subtree of node.
235+
- `limitTabRange(node: HTMLElement, e: Event): void`
237236
238237
```jsx
239238
import focus from 'rc-util/lib/Dom/focus';
@@ -271,20 +270,26 @@ Whether text and modified key is entered at the same time.
271270
272271
Whether character is entered.
273272
274-
### switchScrollingEffect
273+
### ScrollLocker
275274
276-
> (close: boolean) => void
275+
> ScrollLocker<{lock: (options: {container: HTMLElement}) => void, unLock: () => void}>
277276
278277
improve shake when page scroll bar hidden.
279278
280-
`switchScrollingEffect` change body style, and add a class `ant-scrolling-effect` when called, so if you page look abnormal, please check this;
279+
`ScrollLocker` change body style, and add a class `ant-scrolling-effect` when called, so if you page look abnormal, please check this;
281280
282281
```js
283-
import switchScrollingEffect from "./src/switchScrollingEffect";
282+
import ScrollLocker from 'rc-util/lib/Dom/scrollLocker';
283+
284+
const scrollLocker = new ScrollLocker();
285+
286+
// lock
287+
scrollLocker.lock()
284288
285-
switchScrollingEffect();
289+
// unLock
290+
scrollLocker.unLock()
286291
```
287292
288293
## License
289294
290-
[MIT](/LICENSE)
295+
[MIT](/LICENSE)

src/Dom/scrollLocker.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import getScrollBarSize from '../getScrollBarSize';
2+
import setStyle from '../setStyle';
3+
4+
export interface scrollLockOptions {
5+
container: HTMLElement;
6+
}
7+
8+
let passiveSupported = false;
9+
if (typeof window !== 'undefined') {
10+
const passiveTestOption = {
11+
get passive() {
12+
passiveSupported = true;
13+
return null;
14+
},
15+
};
16+
17+
window.addEventListener('testPassive', null, passiveTestOption);
18+
// @ts-ignore compatible passive
19+
window.removeEventListener('testPassive', null, passiveTestOption);
20+
}
21+
22+
const preventDefault = (event: React.TouchEvent | TouchEvent): boolean => {
23+
const e = event || window.event;
24+
25+
// If more than one touch we don't prevent
26+
if ((e as TouchEvent).touches.length > 1) return true;
27+
28+
if (e.preventDefault) e.preventDefault();
29+
30+
return false;
31+
};
32+
33+
let uuid = 0;
34+
35+
interface Ilocks {
36+
target: typeof uuid;
37+
cacheStyle?: React.CSSProperties;
38+
options: scrollLockOptions;
39+
}
40+
41+
let locks: Ilocks[] = [];
42+
const scrollingEffectClassName = 'ant-scrolling-effect';
43+
const scrollingEffectClassNameReg = new RegExp(
44+
`${scrollingEffectClassName}`,
45+
'g',
46+
);
47+
48+
export default class ScrollLocker {
49+
lockTarget: typeof uuid;
50+
51+
options: scrollLockOptions;
52+
53+
constructor(options?: scrollLockOptions) {
54+
// eslint-disable-next-line no-plusplus
55+
this.lockTarget = uuid++;
56+
this.options = options;
57+
}
58+
59+
lock = () => {
60+
// If lockTarget exist return
61+
if (locks.some(({ target }) => target === this.lockTarget)) {
62+
return;
63+
}
64+
65+
// If same container effect, return
66+
if (
67+
locks.some(
68+
({ options }) => options?.container === this.options?.container,
69+
)
70+
) {
71+
locks = [...locks, { target: this.lockTarget, options: this.options }];
72+
return;
73+
}
74+
75+
let scrollBarSize = 0;
76+
77+
if (window.innerWidth - document.documentElement.clientWidth > 0) {
78+
scrollBarSize = getScrollBarSize();
79+
}
80+
81+
const container = this.options?.container || document.body;
82+
const containerClassName = container.className;
83+
84+
// https://github.com/ant-design/ant-design/issues/19340
85+
// https://github.com/ant-design/ant-design/issues/19332
86+
const cacheStyle = setStyle(
87+
{
88+
paddingRight: `${scrollBarSize}px`,
89+
overflow: 'hidden',
90+
overflowX: 'hidden',
91+
overflowY: 'hidden',
92+
},
93+
{
94+
element: container,
95+
},
96+
);
97+
98+
// https://github.com/ant-design/ant-design/issues/19729
99+
if (!scrollingEffectClassNameReg.test(containerClassName)) {
100+
const addClassName = `${containerClassName} ${scrollingEffectClassName}`;
101+
container.className = addClassName.trim();
102+
103+
document.addEventListener(
104+
'touchmove',
105+
preventDefault,
106+
passiveSupported ? { passive: false } : undefined,
107+
);
108+
}
109+
110+
locks = [
111+
...locks,
112+
{ target: this.lockTarget, options: this.options, cacheStyle },
113+
];
114+
};
115+
116+
unLock = () => {
117+
const findLock = locks.find(({ target }) => target === this.lockTarget);
118+
119+
locks = locks.filter(({ target }) => target !== this.lockTarget);
120+
121+
if (
122+
!findLock ||
123+
locks.some(
124+
({ options }) => options?.container === findLock.options?.container,
125+
)
126+
) {
127+
return;
128+
}
129+
130+
// Remove Effect
131+
const container = this.options?.container || document.body;
132+
const containerClassName = container.className;
133+
134+
if (!scrollingEffectClassNameReg.test(containerClassName)) return;
135+
136+
setStyle(
137+
// @ts-ignore position should be empty string
138+
findLock.cacheStyle || {
139+
paddingRight: '',
140+
overflow: '',
141+
overflowX: '',
142+
overflowY: '',
143+
},
144+
{ element: container },
145+
);
146+
container.className = container.className
147+
.replace(scrollingEffectClassNameReg, '')
148+
.trim();
149+
150+
// @ts-ignore compatible passive
151+
document.removeEventListener(
152+
'touchmove',
153+
preventDefault,
154+
passiveSupported ? { passive: false } : undefined,
155+
);
156+
};
157+
}

src/PortalWrapper.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import * as React from 'react';
33
import raf from './raf';
44
import Portal, { PortalRef } from './Portal';
5+
import canUseDom from './Dom/canUseDom';
56
import switchScrollingEffect from './switchScrollingEffect';
67
import setStyle from './setStyle';
7-
import canUseDom from './Dom/canUseDom';
8+
import ScrollLocker from './Dom/scrollLocker';
89

910
let openCount = 0;
1011
const supportDom = canUseDom();
@@ -50,6 +51,7 @@ export interface PortalWrapperProps {
5051
getOpenCount: () => number;
5152
getContainer: () => HTMLElement;
5253
switchScrollingEffect: () => void;
54+
scrollLocker: ScrollLocker;
5355
ref?: (c: any) => void;
5456
}) => React.ReactNode;
5557
}
@@ -61,6 +63,15 @@ class PortalWrapper extends React.Component<PortalWrapperProps> {
6163

6264
rafId?: number;
6365

66+
scrollLocker: ScrollLocker;
67+
68+
constructor(props: PortalWrapperProps) {
69+
super(props);
70+
this.scrollLocker = new ScrollLocker({
71+
container: getParent(props.getContainer) as HTMLElement,
72+
});
73+
}
74+
6475
renderComponent?: (info: {
6576
afterClose: Function;
6677
onClose: Function;
@@ -199,6 +210,7 @@ class PortalWrapper extends React.Component<PortalWrapperProps> {
199210
getOpenCount: () => openCount,
200211
getContainer: this.getContainer,
201212
switchScrollingEffect: this.switchScrollingEffect,
213+
scrollLocker: this.scrollLocker,
202214
};
203215

204216
if (forceRender || visible || this.componentRef.current) {

src/setStyle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ function setStyle(
1414
style: React.CSSProperties,
1515
options: SetStyleOptions = {},
1616
): React.CSSProperties {
17+
if (!style) {
18+
return {};
19+
}
20+
1721
const { element = document.body } = options;
1822
const oldStyle: React.CSSProperties = {};
1923

0 commit comments

Comments
 (0)