Skip to content

Commit

Permalink
feat: add ScrollLocker (#179)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shaodahong committed Dec 24, 2020
1 parent 37aacd6 commit a470f80
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Expand Up @@ -3,5 +3,6 @@
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
"trailingComma": "all",
"arrowParens": "avoid"
}
75 changes: 40 additions & 35 deletions README.md
Expand Up @@ -58,14 +58,14 @@ import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin';

Fields in `config` and their meanings.

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

### Portal

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

Props:

| Prop | Type | Description | Default |
|-------|------|-------------|---------|
| children | ReactChildren | Content render to the container | - |
| getContainer | (): HTMLElement | A function to get the container | - |

| Prop | Type | Description | Default |
| ------------ | --------------- | ------------------------------- | ------- |
| children | ReactChildren | Content render to the container | - |
| getContainer | (): HTMLElement | A function to get the container | - |

### getScrollBarSize

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

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

* `hasClass(node: HTMLElement, className: string): boolean`
* `addClass(node: HTMLElement, className: string): void`
* `removeClass(node: HTMLElement, className: string): void`
- `hasClass(node: HTMLElement, className: string): boolean`
- `addClass(node: HTMLElement, className: string): void`
- `removeClass(node: HTMLElement, className: string): void`

```jsx
import cssClass from 'rc-util/lib/Dom/class;
Expand All @@ -212,14 +211,14 @@ import contains from 'rc-util/lib/Dom/contains';
A collection of functions to get or set css styles.
* `get(node: HTMLElement, name?: string): any`
* `set(node: HTMLElement, name?: string, value: any) | set(node, object)`
* `getOuterWidth(el: HTMLElement): number`
* `getOuterHeight(el: HTMLElement): number`
* `getDocSize(): { width: number, height: number }`
* `getClientSize(): { width: number, height: number }`
* `getScroll(): { scrollLeft: number, scrollTop: number }`
* `getOffset(node: HTMLElement): { left: number, top: number }`
- `get(node: HTMLElement, name?: string): any`
- `set(node: HTMLElement, name?: string, value: any) | set(node, object)`
- `getOuterWidth(el: HTMLElement): number`
- `getOuterHeight(el: HTMLElement): number`
- `getDocSize(): { width: number, height: number }`
- `getClientSize(): { width: number, height: number }`
- `getScroll(): { scrollLeft: number, scrollTop: number }`
- `getOffset(node: HTMLElement): { left: number, top: number }`
```jsx
import css from 'rc-util/lib/Dom/css';
Expand All @@ -229,11 +228,11 @@ import css from 'rc-util/lib/Dom/css';
A collection of functions to operate focus status of DOM node.
* `saveLastFocusNode(): void`
* `clearLastFocusNode(): void`
* `backLastFocusNode(): void`
* `getFocusNodeList(node: HTMLElement): HTMLElement[]` get a list of focusable nodes from the subtree of node.
* `limitTabRange(node: HTMLElement, e: Event): void`
- `saveLastFocusNode(): void`
- `clearLastFocusNode(): void`
- `backLastFocusNode(): void`
- `getFocusNodeList(node: HTMLElement): HTMLElement[]` get a list of focusable nodes from the subtree of node.
- `limitTabRange(node: HTMLElement, e: Event): void`
```jsx
import focus from 'rc-util/lib/Dom/focus';
Expand Down Expand Up @@ -271,20 +270,26 @@ Whether text and modified key is entered at the same time.
Whether character is entered.
### switchScrollingEffect
### ScrollLocker
> (close: boolean) => void
> ScrollLocker<{lock: (options: {container: HTMLElement}) => void, unLock: () => void}>
improve shake when page scroll bar hidden.
`switchScrollingEffect` change body style, and add a class `ant-scrolling-effect` when called, so if you page look abnormal, please check this;
`ScrollLocker` change body style, and add a class `ant-scrolling-effect` when called, so if you page look abnormal, please check this;
```js
import switchScrollingEffect from "./src/switchScrollingEffect";
import ScrollLocker from 'rc-util/lib/Dom/scrollLocker';
const scrollLocker = new ScrollLocker();
// lock
scrollLocker.lock()
switchScrollingEffect();
// unLock
scrollLocker.unLock()
```
## License
[MIT](/LICENSE)
[MIT](/LICENSE)
157 changes: 157 additions & 0 deletions src/Dom/scrollLocker.ts
@@ -0,0 +1,157 @@
import getScrollBarSize from '../getScrollBarSize';
import setStyle from '../setStyle';

export interface scrollLockOptions {
container: HTMLElement;
}

let passiveSupported = false;
if (typeof window !== 'undefined') {
const passiveTestOption = {
get passive() {
passiveSupported = true;
return null;
},
};

window.addEventListener('testPassive', null, passiveTestOption);
// @ts-ignore compatible passive
window.removeEventListener('testPassive', null, passiveTestOption);
}

const preventDefault = (event: React.TouchEvent | TouchEvent): boolean => {
const e = event || window.event;

// If more than one touch we don't prevent
if ((e as TouchEvent).touches.length > 1) return true;

if (e.preventDefault) e.preventDefault();

return false;
};

let uuid = 0;

interface Ilocks {
target: typeof uuid;
cacheStyle?: React.CSSProperties;
options: scrollLockOptions;
}

let locks: Ilocks[] = [];
const scrollingEffectClassName = 'ant-scrolling-effect';
const scrollingEffectClassNameReg = new RegExp(
`${scrollingEffectClassName}`,
'g',
);

export default class ScrollLocker {
lockTarget: typeof uuid;

options: scrollLockOptions;

constructor(options?: scrollLockOptions) {
// eslint-disable-next-line no-plusplus
this.lockTarget = uuid++;
this.options = options;
}

lock = () => {
// If lockTarget exist return
if (locks.some(({ target }) => target === this.lockTarget)) {
return;
}

// If same container effect, return
if (
locks.some(
({ options }) => options?.container === this.options?.container,
)
) {
locks = [...locks, { target: this.lockTarget, options: this.options }];
return;
}

let scrollBarSize = 0;

if (window.innerWidth - document.documentElement.clientWidth > 0) {
scrollBarSize = getScrollBarSize();
}

const container = this.options?.container || document.body;
const containerClassName = container.className;

// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
const cacheStyle = setStyle(
{
paddingRight: `${scrollBarSize}px`,
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden',
},
{
element: container,
},
);

// https://github.com/ant-design/ant-design/issues/19729
if (!scrollingEffectClassNameReg.test(containerClassName)) {
const addClassName = `${containerClassName} ${scrollingEffectClassName}`;
container.className = addClassName.trim();

document.addEventListener(
'touchmove',
preventDefault,
passiveSupported ? { passive: false } : undefined,
);
}

locks = [
...locks,
{ target: this.lockTarget, options: this.options, cacheStyle },
];
};

unLock = () => {
const findLock = locks.find(({ target }) => target === this.lockTarget);

locks = locks.filter(({ target }) => target !== this.lockTarget);

if (
!findLock ||
locks.some(
({ options }) => options?.container === findLock.options?.container,
)
) {
return;
}

// Remove Effect
const container = this.options?.container || document.body;
const containerClassName = container.className;

if (!scrollingEffectClassNameReg.test(containerClassName)) return;

setStyle(
// @ts-ignore position should be empty string
findLock.cacheStyle || {
paddingRight: '',
overflow: '',
overflowX: '',
overflowY: '',
},
{ element: container },
);
container.className = container.className
.replace(scrollingEffectClassNameReg, '')
.trim();

// @ts-ignore compatible passive
document.removeEventListener(
'touchmove',
preventDefault,
passiveSupported ? { passive: false } : undefined,
);
};
}
14 changes: 13 additions & 1 deletion src/PortalWrapper.tsx
Expand Up @@ -2,9 +2,10 @@
import * as React from 'react';
import raf from './raf';
import Portal, { PortalRef } from './Portal';
import canUseDom from './Dom/canUseDom';
import switchScrollingEffect from './switchScrollingEffect';
import setStyle from './setStyle';
import canUseDom from './Dom/canUseDom';
import ScrollLocker from './Dom/scrollLocker';

let openCount = 0;
const supportDom = canUseDom();
Expand Down Expand Up @@ -50,6 +51,7 @@ export interface PortalWrapperProps {
getOpenCount: () => number;
getContainer: () => HTMLElement;
switchScrollingEffect: () => void;
scrollLocker: ScrollLocker;
ref?: (c: any) => void;
}) => React.ReactNode;
}
Expand All @@ -61,6 +63,15 @@ class PortalWrapper extends React.Component<PortalWrapperProps> {

rafId?: number;

scrollLocker: ScrollLocker;

constructor(props: PortalWrapperProps) {
super(props);
this.scrollLocker = new ScrollLocker({
container: getParent(props.getContainer) as HTMLElement,
});
}

renderComponent?: (info: {
afterClose: Function;
onClose: Function;
Expand Down Expand Up @@ -199,6 +210,7 @@ class PortalWrapper extends React.Component<PortalWrapperProps> {
getOpenCount: () => openCount,
getContainer: this.getContainer,
switchScrollingEffect: this.switchScrollingEffect,
scrollLocker: this.scrollLocker,
};

if (forceRender || visible || this.componentRef.current) {
Expand Down
4 changes: 4 additions & 0 deletions src/setStyle.ts
Expand Up @@ -14,6 +14,10 @@ function setStyle(
style: React.CSSProperties,
options: SetStyleOptions = {},
): React.CSSProperties {
if (!style) {
return {};
}

const { element = document.body } = options;
const oldStyle: React.CSSProperties = {};

Expand Down

0 comments on commit a470f80

Please sign in to comment.