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: add ScrollLocker #179

Merged
merged 8 commits into from Dec 24, 2020
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
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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把 master 的 js 体积搞大了,看看是否是必要的。

图片

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

等都迁移了就可以把上面的 switchScrollingEffect 删了

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不能删,老的 antd 版本会挂。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

antd 3 么还是


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,
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switchScrollingEffect 里的 overflow 相关代码是不是应该清理掉。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

另外,这里的滚动条是否出现是依赖 openCount 的。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switchScrollingEffect 里的 overflow 相关代码是不是应该清理掉。

switchScrollingEffect 不能动,动了算是 breaking 了,要改的东西有点多,现在这种是多了个 scrollLocker 方法供上层用

这里的滚动条是否出现是依赖 openCount 的。

不依赖 openCount ,openCount 并不是 effectCount,我现在的做法是给类似观察者,每一个 dialog 都有个属于自己的 scrollLocker,你想要锁定或者解锁都只要根据自己的业务逻辑来就行,Dialog 那边比较简单,显示就 lock,隐藏就 unLock,Drawer 有逻辑判断,并不是所有的显示都会 lock,这时候就比较好用了,还没改到 Drawer,预计问题不大,就是 Drawer 的代码有点乱,比较多的 dom 操作

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drawer 是依赖的,多层抽屉只有关掉所有才会 unLock。

https://ant.design/components/drawer-cn/#components-drawer-demo-multi-level-drawer 这个之前是好的,后来某个版本给改挂了。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嗯,现在改成不依赖了,多层抽屉就是多个 Dialog,�底层都是用的这个 portal,改挂了算是意料之中,因为 openCount 所有由 portal 创建弹层的共享,我打开一个 Modal 再打开一个 Drawer 也会出问题


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