From 2eb4ecfeddd5d9392e104ed810db82d27e491d57 Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 3 Mar 2019 10:04:21 +0800 Subject: [PATCH 1/4] refactory --- components/affix/demo/target.md | 3 +- components/affix/index.1.tsx | 308 +++++++++++++++++++++++++++ components/affix/index.tsx | 355 ++++++++++++-------------------- components/affix/utils.ts | 96 +++++++++ 4 files changed, 533 insertions(+), 229 deletions(-) create mode 100644 components/affix/index.1.tsx create mode 100644 components/affix/utils.ts diff --git a/components/affix/demo/target.md b/components/affix/demo/target.md index d78ab9100e..c0a141c96e 100644 --- a/components/affix/demo/target.md +++ b/components/affix/demo/target.md @@ -3,6 +3,7 @@ order: 2 title: zh-CN: 滚动容器 en-US: Container to scroll. +only: true --- ## zh-CN @@ -21,7 +22,7 @@ class Demo extends React.Component { return (
{ this.container = node; }}>
- this.container}> + this.container} debug> diff --git a/components/affix/index.1.tsx b/components/affix/index.1.tsx new file mode 100644 index 0000000000..1508e53b61 --- /dev/null +++ b/components/affix/index.1.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as PropTypes from 'prop-types'; +import addEventListener from 'rc-util/lib/Dom/addEventListener'; +import classNames from 'classnames'; +import shallowequal from 'shallowequal'; +import omit from 'omit.js'; +import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; +import getScroll from '../_util/getScroll'; +import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; + +function getTargetRect(target: HTMLElement | Window | null): ClientRect { + return target !== window + ? (target as HTMLElement).getBoundingClientRect() + : ({ top: 0, left: 0, bottom: 0 } as ClientRect); +} + +function getOffset(element: HTMLElement, target: HTMLElement | Window | null) { + const elemRect = element.getBoundingClientRect(); + const targetRect = getTargetRect(target); + + const scrollTop = getScroll(target, true); + const scrollLeft = getScroll(target, false); + + const docElem = window.document.body; + const clientTop = docElem.clientTop || 0; + const clientLeft = docElem.clientLeft || 0; + + return { + top: elemRect.top - targetRect.top + scrollTop - clientTop, + left: elemRect.left - targetRect.left + scrollLeft - clientLeft, + width: elemRect.width, + height: elemRect.height, + }; +} + +function noop() {} + +function getDefaultTarget() { + return typeof window !== 'undefined' ? window : null; +} + +// Affix +export interface AffixProps { + /** + * 距离窗口顶部达到指定偏移量后触发 + */ + offsetTop?: number; + offset?: number; + /** 距离窗口底部达到指定偏移量后触发 */ + offsetBottom?: number; + style?: React.CSSProperties; + /** 固定状态改变时触发的回调函数 */ + onChange?: (affixed?: boolean) => void; + /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */ + target?: () => Window | HTMLElement | null; + prefixCls?: string; + className?: string; +} + +export interface AffixState { + affixStyle: React.CSSProperties | undefined; + placeholderStyle: React.CSSProperties | undefined; +} + +export default class Affix extends React.Component { + static propTypes = { + offsetTop: PropTypes.number, + offsetBottom: PropTypes.number, + target: PropTypes.func, + }; + + state: AffixState = { + affixStyle: undefined, + placeholderStyle: undefined, + }; + + private timeout: number; + private eventHandlers: Record = {}; + private fixedNode: HTMLElement; + private placeholderNode: HTMLElement; + private readonly events = [ + 'resize', + 'scroll', + 'touchstart', + 'touchmove', + 'touchend', + 'pageshow', + 'load', + ]; + + setAffixStyle(e: Event, affixStyle: React.CSSProperties | null) { + const { onChange = noop, target = getDefaultTarget } = this.props; + const originalAffixStyle = this.state.affixStyle; + const isWindow = target() === window; + if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) { + return; + } + if (shallowequal(affixStyle, originalAffixStyle)) { + return; + } + + this.setState({ affixStyle: affixStyle as React.CSSProperties }, () => { + const affixed = !!this.state.affixStyle; + if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) { + onChange(affixed); + } + }); + } + + setPlaceholderStyle(placeholderStyle: React.CSSProperties | null) { + const originalPlaceholderStyle = this.state.placeholderStyle; + if (shallowequal(placeholderStyle, originalPlaceholderStyle)) { + return; + } + this.setState({ placeholderStyle: placeholderStyle as React.CSSProperties }); + } + + syncPlaceholderStyle(e: Event) { + const { affixStyle } = this.state; + if (!affixStyle) { + return; + } + this.placeholderNode.style.cssText = ''; + this.setAffixStyle(e, { + ...affixStyle, + width: this.placeholderNode.offsetWidth, + }); + this.setPlaceholderStyle({ + width: this.placeholderNode.offsetWidth, + }); + } + + @throttleByAnimationFrameDecorator() + updatePosition(e: Event) { + const { offsetBottom, offset, target = getDefaultTarget } = this.props; + let { offsetTop } = this.props; + const targetNode = target(); + + // Backwards support + // Fix: if offsetTop === 0, it will get undefined, + // if offsetBottom is type of number, offsetMode will be { top: false, ... } + offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop; + const scrollTop = getScroll(targetNode, true); + const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; + const elemOffset = getOffset(affixNode, targetNode); + const elemSize = { + width: this.fixedNode.offsetWidth, + height: this.fixedNode.offsetHeight, + }; + + const offsetMode = { + top: false, + bottom: false, + }; + // Default to `offsetTop=0`. + if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { + offsetMode.top = true; + offsetTop = 0; + } else { + offsetMode.top = typeof offsetTop === 'number'; + offsetMode.bottom = typeof offsetBottom === 'number'; + } + + const targetRect = getTargetRect(targetNode); + const targetInnerHeight = + (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; + // ref: https://github.com/ant-design/ant-design/issues/13662 + if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) { + // Fixed Top + const width = elemOffset.width; + const top = targetRect.top + (offsetTop as number); + this.setAffixStyle(e, { + position: 'fixed', + top, + left: targetRect.left + elemOffset.left, + width, + }); + this.setPlaceholderStyle({ + width, + height: elemSize.height, + }); + } else if ( + scrollTop <= + elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && + offsetMode.bottom + ) { + // Fixed Bottom + const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom; + const width = elemOffset.width; + this.setAffixStyle(e, { + position: 'fixed', + bottom: targetBottomOffet + (offsetBottom as number), + left: targetRect.left + elemOffset.left, + width, + }); + this.setPlaceholderStyle({ + width, + height: elemOffset.height, + }); + } else { + const { affixStyle } = this.state; + if ( + e.type === 'resize' && + affixStyle && + affixStyle.position === 'fixed' && + affixNode.offsetWidth + ) { + this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); + } else { + this.setAffixStyle(e, null); + } + this.setPlaceholderStyle(null); + } + + if (e.type === 'resize') { + this.syncPlaceholderStyle(e); + } + } + + componentDidMount() { + const target = this.props.target || getDefaultTarget; + // Wait for parent component ref has its value + this.timeout = setTimeout(() => { + this.setTargetEventListeners(target); + // Mock Event object. + this.updatePosition({} as Event); + }); + } + + componentWillReceiveProps(nextProps: AffixProps) { + if (this.props.target !== nextProps.target) { + this.clearEventListeners(); + this.setTargetEventListeners(nextProps.target!); + + // Mock Event object. + this.updatePosition({} as Event); + } + if ( + this.props.offsetTop !== nextProps.offsetTop || + this.props.offsetBottom !== nextProps.offsetBottom + ) { + this.updatePosition({} as Event); + } + } + + componentWillUnmount() { + this.clearEventListeners(); + clearTimeout(this.timeout); + (this.updatePosition as any).cancel(); + } + + setTargetEventListeners(getTarget: () => HTMLElement | Window | null) { + const target = getTarget(); + if (!target) { + return; + } + this.clearEventListeners(); + + this.events.forEach(eventName => { + this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); + }); + } + + clearEventListeners() { + this.events.forEach(eventName => { + const handler = this.eventHandlers[eventName]; + if (handler && handler.remove) { + handler.remove(); + } + }); + } + + saveFixedNode = (node: HTMLDivElement) => { + this.fixedNode = node; + }; + + savePlaceholderNode = (node: HTMLDivElement) => { + this.placeholderNode = node; + }; + + renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => { + const { prefixCls } = this.props; + const className = classNames({ + [getPrefixCls('affix', prefixCls)]: this.state.affixStyle, + }); + + const props = omit(this.props, [ + 'prefixCls', + 'offsetTop', + 'offsetBottom', + 'target', + 'onChange', + ]); + const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; + return ( +
+
+ {this.props.children} +
+
+ ); + }; + + render() { + return {this.renderAffix}; + } +} diff --git a/components/affix/index.tsx b/components/affix/index.tsx index 1508e53b61..9ae75e889e 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { polyfill } from 'react-lifecycles-compat'; import * as PropTypes from 'prop-types'; import addEventListener from 'rc-util/lib/Dom/addEventListener'; import classNames from 'classnames'; @@ -9,32 +10,8 @@ import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import getScroll from '../_util/getScroll'; import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; -function getTargetRect(target: HTMLElement | Window | null): ClientRect { - return target !== window - ? (target as HTMLElement).getBoundingClientRect() - : ({ top: 0, left: 0, bottom: 0 } as ClientRect); -} - -function getOffset(element: HTMLElement, target: HTMLElement | Window | null) { - const elemRect = element.getBoundingClientRect(); - const targetRect = getTargetRect(target); - - const scrollTop = getScroll(target, true); - const scrollLeft = getScroll(target, false); - - const docElem = window.document.body; - const clientTop = docElem.clientTop || 0; - const clientLeft = docElem.clientLeft || 0; - - return { - top: elemRect.top - targetRect.top + scrollTop - clientTop, - left: elemRect.left - targetRect.left + scrollLeft - clientLeft, - width: elemRect.width, - height: elemRect.height, - }; -} - -function noop() {} +import warning from '../_util/warning'; +import { addObserveTarget, removeObserveTarget, getTargetRect, getOffset } from './utils'; function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; @@ -56,233 +33,147 @@ export interface AffixProps { target?: () => Window | HTMLElement | null; prefixCls?: string; className?: string; + + // TODO: Remove me! + debug?: boolean; +} + +enum AffixStatus { + None, + Prepare, } export interface AffixState { - affixStyle: React.CSSProperties | undefined; - placeholderStyle: React.CSSProperties | undefined; + affixStyle?: React.CSSProperties; + placeholderStyle?: React.CSSProperties; + status: AffixStatus; + lastAffix: boolean; } -export default class Affix extends React.Component { - static propTypes = { - offsetTop: PropTypes.number, - offsetBottom: PropTypes.number, - target: PropTypes.func, +class Affix extends React.Component { + static defaultProps = { + target: getDefaultTarget, }; state: AffixState = { - affixStyle: undefined, - placeholderStyle: undefined, + status: AffixStatus.None, + lastAffix: false, }; private timeout: number; - private eventHandlers: Record = {}; - private fixedNode: HTMLElement; - private placeholderNode: HTMLElement; - private readonly events = [ - 'resize', - 'scroll', - 'touchstart', - 'touchmove', - 'touchend', - 'pageshow', - 'load', - ]; - - setAffixStyle(e: Event, affixStyle: React.CSSProperties | null) { - const { onChange = noop, target = getDefaultTarget } = this.props; - const originalAffixStyle = this.state.affixStyle; - const isWindow = target() === window; - if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) { - return; - } - if (shallowequal(affixStyle, originalAffixStyle)) { - return; - } - - this.setState({ affixStyle: affixStyle as React.CSSProperties }, () => { - const affixed = !!this.state.affixStyle; - if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) { - onChange(affixed); - } - }); - } - - setPlaceholderStyle(placeholderStyle: React.CSSProperties | null) { - const originalPlaceholderStyle = this.state.placeholderStyle; - if (shallowequal(placeholderStyle, originalPlaceholderStyle)) { - return; - } - this.setState({ placeholderStyle: placeholderStyle as React.CSSProperties }); - } - - syncPlaceholderStyle(e: Event) { - const { affixStyle } = this.state; - if (!affixStyle) { - return; - } - this.placeholderNode.style.cssText = ''; - this.setAffixStyle(e, { - ...affixStyle, - width: this.placeholderNode.offsetWidth, - }); - this.setPlaceholderStyle({ - width: this.placeholderNode.offsetWidth, - }); - } - - @throttleByAnimationFrameDecorator() - updatePosition(e: Event) { - const { offsetBottom, offset, target = getDefaultTarget } = this.props; - let { offsetTop } = this.props; - const targetNode = target(); - - // Backwards support - // Fix: if offsetTop === 0, it will get undefined, - // if offsetBottom is type of number, offsetMode will be { top: false, ... } - offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop; - const scrollTop = getScroll(targetNode, true); - const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; - const elemOffset = getOffset(affixNode, targetNode); - const elemSize = { - width: this.fixedNode.offsetWidth, - height: this.fixedNode.offsetHeight, - }; - - const offsetMode = { - top: false, - bottom: false, - }; - // Default to `offsetTop=0`. - if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { - offsetMode.top = true; - offsetTop = 0; - } else { - offsetMode.top = typeof offsetTop === 'number'; - offsetMode.bottom = typeof offsetBottom === 'number'; - } - - const targetRect = getTargetRect(targetNode); - const targetInnerHeight = - (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; - // ref: https://github.com/ant-design/ant-design/issues/13662 - if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) { - // Fixed Top - const width = elemOffset.width; - const top = targetRect.top + (offsetTop as number); - this.setAffixStyle(e, { - position: 'fixed', - top, - left: targetRect.left + elemOffset.left, - width, - }); - this.setPlaceholderStyle({ - width, - height: elemSize.height, - }); - } else if ( - scrollTop <= - elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && - offsetMode.bottom - ) { - // Fixed Bottom - const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom; - const width = elemOffset.width; - this.setAffixStyle(e, { - position: 'fixed', - bottom: targetBottomOffet + (offsetBottom as number), - left: targetRect.left + elemOffset.left, - width, - }); - this.setPlaceholderStyle({ - width, - height: elemOffset.height, - }); - } else { - const { affixStyle } = this.state; - if ( - e.type === 'resize' && - affixStyle && - affixStyle.position === 'fixed' && - affixNode.offsetWidth - ) { - this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); - } else { - this.setAffixStyle(e, null); - } - this.setPlaceholderStyle(null); - } - - if (e.type === 'resize') { - this.syncPlaceholderStyle(e); - } - } + placeholderNode: HTMLDivElement; + fixedNode: HTMLDivElement; + // Event handler componentDidMount() { - const target = this.props.target || getDefaultTarget; - // Wait for parent component ref has its value - this.timeout = setTimeout(() => { - this.setTargetEventListeners(target); - // Mock Event object. - this.updatePosition({} as Event); - }); + if (!this.props.debug) return; + const { target } = this.props; + if (target) { + // Wait for parent component ref has its value + this.timeout = setTimeout(() => { + addObserveTarget(target(), this); + }); + } } - componentWillReceiveProps(nextProps: AffixProps) { - if (this.props.target !== nextProps.target) { - this.clearEventListeners(); - this.setTargetEventListeners(nextProps.target!); + componentDidUpdate(prevProps: AffixProps) { + if (!this.props.debug) return; + const { target } = this.props; + if (prevProps.target !== target) { + removeObserveTarget(this); + if (target) { + addObserveTarget(target(), this); + } + } - // Mock Event object. - this.updatePosition({} as Event); - } - if ( - this.props.offsetTop !== nextProps.offsetTop || - this.props.offsetBottom !== nextProps.offsetBottom - ) { - this.updatePosition({} as Event); - } + this.measure(); } componentWillUnmount() { - this.clearEventListeners(); clearTimeout(this.timeout); - (this.updatePosition as any).cancel(); + removeObserveTarget(this); } - setTargetEventListeners(getTarget: () => HTMLElement | Window | null) { - const target = getTarget(); - if (!target) { - return; - } - this.clearEventListeners(); - - this.events.forEach(eventName => { - this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); - }); - } - - clearEventListeners() { - this.events.forEach(eventName => { - const handler = this.eventHandlers[eventName]; - if (handler && handler.remove) { - handler.remove(); - } - }); - } - - saveFixedNode = (node: HTMLDivElement) => { - this.fixedNode = node; - }; - savePlaceholderNode = (node: HTMLDivElement) => { this.placeholderNode = node; }; + saveFixedNode = (node: HTMLDivElement) => { + this.fixedNode = node; + }; + + // =================== Measure =================== + // Handle realign logic + @throttleByAnimationFrameDecorator() + updatePosition(e: Event) { + console.log('???', e.target); + this.setState({ + status: AffixStatus.Prepare, + affixStyle: undefined, + placeholderStyle: undefined, + }); + } + + measure = () => { + const { status } = this.state; + const { target, offset, offsetBottom } = this.props; + if (status !== AffixStatus.Prepare || !this.fixedNode || !this.placeholderNode || !target) { + return; + } + + let { offsetTop } = this.props; + if (typeof offsetTop === 'undefined') { + offsetTop = offset; + warning( + typeof offset === 'undefined', + 'Affix', + '`offset` is deprecated. Please use `offsetTop` instead.', + ); + } + + const targetNode = target(); + if (!targetNode) { + return; + } + + const newState: Partial = { + status: AffixStatus.None, + }; + const targetRect = getTargetRect(targetNode); + const placeholderReact = getTargetRect(this.placeholderNode); + + console.log('>>>', targetRect.top, placeholderReact.top, offsetTop); + // TODO: inner content scroll + // const scrollTop = getScroll(targetNode, true); + if (offsetTop !== undefined && targetRect.top > placeholderReact.top - offsetTop) { + newState.affixStyle = { + position: 'fixed', + top: offsetTop, + }; + newState.placeholderStyle = { + width: placeholderReact.width, + height: placeholderReact.height, + }; + } else if (offsetBottom !== undefined && targetRect.bottom < placeholderReact.bottom + offsetBottom) { + newState.affixStyle = { + position: 'fixed', + bottom: offsetBottom, + }; + newState.placeholderStyle = { + width: placeholderReact.width, + height: placeholderReact.height, + }; + } + + this.setState(newState as AffixState); + } + + // =================== Render =================== renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => { - const { prefixCls } = this.props; + const { affixStyle, placeholderStyle, status } = this.state; + const { prefixCls, style, children } = this.props; const className = classNames({ - [getPrefixCls('affix', prefixCls)]: this.state.affixStyle, + [getPrefixCls('affix', prefixCls)]: affixStyle, }); const props = omit(this.props, [ @@ -291,12 +182,16 @@ export default class Affix extends React.Component { 'offsetBottom', 'target', 'onChange', + 'debug', ]); - const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; + const mergedPlaceholderStyle = { + ...(status === AffixStatus.None ? placeholderStyle : null), + ...style + }; return ( -
+
- {this.props.children} + {children}
); @@ -306,3 +201,7 @@ export default class Affix extends React.Component { return {this.renderAffix}; } } + +polyfill(Affix); + +export default Affix; diff --git a/components/affix/utils.ts b/components/affix/utils.ts new file mode 100644 index 0000000000..0db7e0a81d --- /dev/null +++ b/components/affix/utils.ts @@ -0,0 +1,96 @@ +import addEventListener from 'rc-util/lib/Dom/addEventListener'; +import getScroll from '../_util/getScroll'; +import Affix from './'; + +// ======================== Observer ======================== +const TRIGGER_EVENTS = [ + 'resize', + 'scroll', + 'touchstart', + 'touchmove', + 'touchend', + 'pageshow', + 'load', +]; + +interface ObserverEntity { + target: HTMLElement | Window; + affixList: Affix[]; + eventHandlers: { [eventName: string]: any }; +} + +let observerEntities: ObserverEntity[] = []; + +export function addObserveTarget(target: HTMLElement | Window | null, affix: Affix): void { + console.log('???', target); + if (!target) return; + + let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target); + + if (entity) { + entity.affixList.push(affix); + } else { + entity = { + target, + affixList: [affix], + eventHandlers: {}, + }; + observerEntities.push(entity); + + // Add listener + TRIGGER_EVENTS.forEach(eventName => { + entity!.eventHandlers[eventName] = addEventListener(target, eventName, (event: Event) => { + entity!.affixList.forEach(affix => { + affix.updatePosition(event); + }); + }); + }); + } +} + +export function removeObserveTarget(affix: Affix): void { + const observerEntity = observerEntities.find(oriObserverEntity => { + const hasAffix = oriObserverEntity.affixList.some(item => item === affix); + if (hasAffix) { + oriObserverEntity.affixList = oriObserverEntity.affixList.filter(item => item !== affix); + } + return hasAffix; + }); + + if (observerEntity && observerEntity.affixList.length === 0) { + observerEntities = observerEntities.filter(item => item !== observerEntity); + + // Remove listener + TRIGGER_EVENTS.forEach(eventName => { + const handler = observerEntity.eventHandlers[eventName]; + if (handler && handler.remove) { + handler.remove(); + } + }); + } +} + +export function getTargetRect(target: HTMLElement | Window | null): ClientRect { + return target !== window + ? (target as HTMLElement).getBoundingClientRect() + : ({ top: 0, bottom: window.innerHeight } as ClientRect); +} + +export function getOffset(element: HTMLElement, target: HTMLElement | Window | null) { + const elemRect = element.getBoundingClientRect(); + const targetRect = getTargetRect(target); + + const scrollTop = getScroll(target, true); + const scrollLeft = getScroll(target, false); + + const docElem = window.document.body; + const clientTop = docElem.clientTop || 0; + const clientLeft = docElem.clientLeft || 0; + + return { + top: elemRect.top - targetRect.top + scrollTop - clientTop, + left: elemRect.left - targetRect.left + scrollLeft - clientLeft, + width: elemRect.width, + height: elemRect.height, + }; +} From d0c2f8b05f11751d509e7b24c6bbaffcd111de62 Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 3 Mar 2019 11:40:10 +0800 Subject: [PATCH 2/4] remove debug --- components/affix/demo/target.md | 3 +- components/affix/index.1.tsx | 308 -------------------------------- components/affix/index.tsx | 56 +++--- components/affix/utils.ts | 21 --- 4 files changed, 31 insertions(+), 357 deletions(-) delete mode 100644 components/affix/index.1.tsx diff --git a/components/affix/demo/target.md b/components/affix/demo/target.md index c0a141c96e..d78ab9100e 100644 --- a/components/affix/demo/target.md +++ b/components/affix/demo/target.md @@ -3,7 +3,6 @@ order: 2 title: zh-CN: 滚动容器 en-US: Container to scroll. -only: true --- ## zh-CN @@ -22,7 +21,7 @@ class Demo extends React.Component { return (
{ this.container = node; }}>
- this.container} debug> + this.container}> diff --git a/components/affix/index.1.tsx b/components/affix/index.1.tsx deleted file mode 100644 index 1508e53b61..0000000000 --- a/components/affix/index.1.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import * as PropTypes from 'prop-types'; -import addEventListener from 'rc-util/lib/Dom/addEventListener'; -import classNames from 'classnames'; -import shallowequal from 'shallowequal'; -import omit from 'omit.js'; -import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; -import getScroll from '../_util/getScroll'; -import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; - -function getTargetRect(target: HTMLElement | Window | null): ClientRect { - return target !== window - ? (target as HTMLElement).getBoundingClientRect() - : ({ top: 0, left: 0, bottom: 0 } as ClientRect); -} - -function getOffset(element: HTMLElement, target: HTMLElement | Window | null) { - const elemRect = element.getBoundingClientRect(); - const targetRect = getTargetRect(target); - - const scrollTop = getScroll(target, true); - const scrollLeft = getScroll(target, false); - - const docElem = window.document.body; - const clientTop = docElem.clientTop || 0; - const clientLeft = docElem.clientLeft || 0; - - return { - top: elemRect.top - targetRect.top + scrollTop - clientTop, - left: elemRect.left - targetRect.left + scrollLeft - clientLeft, - width: elemRect.width, - height: elemRect.height, - }; -} - -function noop() {} - -function getDefaultTarget() { - return typeof window !== 'undefined' ? window : null; -} - -// Affix -export interface AffixProps { - /** - * 距离窗口顶部达到指定偏移量后触发 - */ - offsetTop?: number; - offset?: number; - /** 距离窗口底部达到指定偏移量后触发 */ - offsetBottom?: number; - style?: React.CSSProperties; - /** 固定状态改变时触发的回调函数 */ - onChange?: (affixed?: boolean) => void; - /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */ - target?: () => Window | HTMLElement | null; - prefixCls?: string; - className?: string; -} - -export interface AffixState { - affixStyle: React.CSSProperties | undefined; - placeholderStyle: React.CSSProperties | undefined; -} - -export default class Affix extends React.Component { - static propTypes = { - offsetTop: PropTypes.number, - offsetBottom: PropTypes.number, - target: PropTypes.func, - }; - - state: AffixState = { - affixStyle: undefined, - placeholderStyle: undefined, - }; - - private timeout: number; - private eventHandlers: Record = {}; - private fixedNode: HTMLElement; - private placeholderNode: HTMLElement; - private readonly events = [ - 'resize', - 'scroll', - 'touchstart', - 'touchmove', - 'touchend', - 'pageshow', - 'load', - ]; - - setAffixStyle(e: Event, affixStyle: React.CSSProperties | null) { - const { onChange = noop, target = getDefaultTarget } = this.props; - const originalAffixStyle = this.state.affixStyle; - const isWindow = target() === window; - if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) { - return; - } - if (shallowequal(affixStyle, originalAffixStyle)) { - return; - } - - this.setState({ affixStyle: affixStyle as React.CSSProperties }, () => { - const affixed = !!this.state.affixStyle; - if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) { - onChange(affixed); - } - }); - } - - setPlaceholderStyle(placeholderStyle: React.CSSProperties | null) { - const originalPlaceholderStyle = this.state.placeholderStyle; - if (shallowequal(placeholderStyle, originalPlaceholderStyle)) { - return; - } - this.setState({ placeholderStyle: placeholderStyle as React.CSSProperties }); - } - - syncPlaceholderStyle(e: Event) { - const { affixStyle } = this.state; - if (!affixStyle) { - return; - } - this.placeholderNode.style.cssText = ''; - this.setAffixStyle(e, { - ...affixStyle, - width: this.placeholderNode.offsetWidth, - }); - this.setPlaceholderStyle({ - width: this.placeholderNode.offsetWidth, - }); - } - - @throttleByAnimationFrameDecorator() - updatePosition(e: Event) { - const { offsetBottom, offset, target = getDefaultTarget } = this.props; - let { offsetTop } = this.props; - const targetNode = target(); - - // Backwards support - // Fix: if offsetTop === 0, it will get undefined, - // if offsetBottom is type of number, offsetMode will be { top: false, ... } - offsetTop = typeof offsetTop === 'undefined' ? offset : offsetTop; - const scrollTop = getScroll(targetNode, true); - const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; - const elemOffset = getOffset(affixNode, targetNode); - const elemSize = { - width: this.fixedNode.offsetWidth, - height: this.fixedNode.offsetHeight, - }; - - const offsetMode = { - top: false, - bottom: false, - }; - // Default to `offsetTop=0`. - if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { - offsetMode.top = true; - offsetTop = 0; - } else { - offsetMode.top = typeof offsetTop === 'number'; - offsetMode.bottom = typeof offsetBottom === 'number'; - } - - const targetRect = getTargetRect(targetNode); - const targetInnerHeight = - (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; - // ref: https://github.com/ant-design/ant-design/issues/13662 - if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) { - // Fixed Top - const width = elemOffset.width; - const top = targetRect.top + (offsetTop as number); - this.setAffixStyle(e, { - position: 'fixed', - top, - left: targetRect.left + elemOffset.left, - width, - }); - this.setPlaceholderStyle({ - width, - height: elemSize.height, - }); - } else if ( - scrollTop <= - elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && - offsetMode.bottom - ) { - // Fixed Bottom - const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom; - const width = elemOffset.width; - this.setAffixStyle(e, { - position: 'fixed', - bottom: targetBottomOffet + (offsetBottom as number), - left: targetRect.left + elemOffset.left, - width, - }); - this.setPlaceholderStyle({ - width, - height: elemOffset.height, - }); - } else { - const { affixStyle } = this.state; - if ( - e.type === 'resize' && - affixStyle && - affixStyle.position === 'fixed' && - affixNode.offsetWidth - ) { - this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); - } else { - this.setAffixStyle(e, null); - } - this.setPlaceholderStyle(null); - } - - if (e.type === 'resize') { - this.syncPlaceholderStyle(e); - } - } - - componentDidMount() { - const target = this.props.target || getDefaultTarget; - // Wait for parent component ref has its value - this.timeout = setTimeout(() => { - this.setTargetEventListeners(target); - // Mock Event object. - this.updatePosition({} as Event); - }); - } - - componentWillReceiveProps(nextProps: AffixProps) { - if (this.props.target !== nextProps.target) { - this.clearEventListeners(); - this.setTargetEventListeners(nextProps.target!); - - // Mock Event object. - this.updatePosition({} as Event); - } - if ( - this.props.offsetTop !== nextProps.offsetTop || - this.props.offsetBottom !== nextProps.offsetBottom - ) { - this.updatePosition({} as Event); - } - } - - componentWillUnmount() { - this.clearEventListeners(); - clearTimeout(this.timeout); - (this.updatePosition as any).cancel(); - } - - setTargetEventListeners(getTarget: () => HTMLElement | Window | null) { - const target = getTarget(); - if (!target) { - return; - } - this.clearEventListeners(); - - this.events.forEach(eventName => { - this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); - }); - } - - clearEventListeners() { - this.events.forEach(eventName => { - const handler = this.eventHandlers[eventName]; - if (handler && handler.remove) { - handler.remove(); - } - }); - } - - saveFixedNode = (node: HTMLDivElement) => { - this.fixedNode = node; - }; - - savePlaceholderNode = (node: HTMLDivElement) => { - this.placeholderNode = node; - }; - - renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => { - const { prefixCls } = this.props; - const className = classNames({ - [getPrefixCls('affix', prefixCls)]: this.state.affixStyle, - }); - - const props = omit(this.props, [ - 'prefixCls', - 'offsetTop', - 'offsetBottom', - 'target', - 'onChange', - ]); - const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; - return ( -
-
- {this.props.children} -
-
- ); - }; - - render() { - return {this.renderAffix}; - } -} diff --git a/components/affix/index.tsx b/components/affix/index.tsx index 9ae75e889e..7b4cbf2657 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -1,17 +1,12 @@ import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { polyfill } from 'react-lifecycles-compat'; -import * as PropTypes from 'prop-types'; -import addEventListener from 'rc-util/lib/Dom/addEventListener'; import classNames from 'classnames'; -import shallowequal from 'shallowequal'; import omit from 'omit.js'; import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; -import getScroll from '../_util/getScroll'; import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; import warning from '../_util/warning'; -import { addObserveTarget, removeObserveTarget, getTargetRect, getOffset } from './utils'; +import { addObserveTarget, removeObserveTarget, getTargetRect } from './utils'; function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; @@ -33,9 +28,6 @@ export interface AffixProps { target?: () => Window | HTMLElement | null; prefixCls?: string; className?: string; - - // TODO: Remove me! - debug?: boolean; } enum AffixStatus { @@ -56,33 +48,34 @@ class Affix extends React.Component { }; state: AffixState = { - status: AffixStatus.None, + status: AffixStatus.None, lastAffix: false, }; - private timeout: number; placeholderNode: HTMLDivElement; fixedNode: HTMLDivElement; + private timeout: number; // Event handler componentDidMount() { - if (!this.props.debug) return; const { target } = this.props; if (target) { // Wait for parent component ref has its value this.timeout = setTimeout(() => { addObserveTarget(target(), this); + // Mock Event object. + this.updatePosition({} as Event); }); } } componentDidUpdate(prevProps: AffixProps) { - if (!this.props.debug) return; const { target } = this.props; if (prevProps.target !== target) { removeObserveTarget(this); if (target) { addObserveTarget(target(), this); + // Mock Event object. } } @@ -92,6 +85,7 @@ class Affix extends React.Component { componentWillUnmount() { clearTimeout(this.timeout); removeObserveTarget(this); + (this.updatePosition as any).cancel(); } savePlaceholderNode = (node: HTMLDivElement) => { @@ -105,8 +99,9 @@ class Affix extends React.Component { // =================== Measure =================== // Handle realign logic @throttleByAnimationFrameDecorator() + // @ts-ignore TS6133 updatePosition(e: Event) { - console.log('???', e.target); + // event param is used before. Keep compatible ts define here. this.setState({ status: AffixStatus.Prepare, affixStyle: undefined, @@ -115,12 +110,12 @@ class Affix extends React.Component { } measure = () => { - const { status } = this.state; - const { target, offset, offsetBottom } = this.props; + const { status, lastAffix } = this.state; + const { target, offset, offsetBottom, onChange } = this.props; if (status !== AffixStatus.Prepare || !this.fixedNode || !this.placeholderNode || !target) { return; } - + let { offsetTop } = this.props; if (typeof offsetTop === 'undefined') { offsetTop = offset; @@ -131,6 +126,10 @@ class Affix extends React.Component { ); } + if (offsetBottom === undefined && offsetTop === undefined) { + offsetTop = 0; + } + const targetNode = target(); if (!targetNode) { return; @@ -142,22 +141,23 @@ class Affix extends React.Component { const targetRect = getTargetRect(targetNode); const placeholderReact = getTargetRect(this.placeholderNode); - console.log('>>>', targetRect.top, placeholderReact.top, offsetTop); - // TODO: inner content scroll - // const scrollTop = getScroll(targetNode, true); if (offsetTop !== undefined && targetRect.top > placeholderReact.top - offsetTop) { newState.affixStyle = { position: 'fixed', - top: offsetTop, + top: offsetTop + targetRect.top, }; newState.placeholderStyle = { width: placeholderReact.width, height: placeholderReact.height, }; - } else if (offsetBottom !== undefined && targetRect.bottom < placeholderReact.bottom + offsetBottom) { + } else if ( + offsetBottom !== undefined && + targetRect.bottom < placeholderReact.bottom + offsetBottom + ) { + const targetBottomOffset = targetNode === window ? 0 : window.innerHeight - targetRect.bottom; newState.affixStyle = { position: 'fixed', - bottom: offsetBottom, + bottom: offsetBottom + targetBottomOffset, }; newState.placeholderStyle = { width: placeholderReact.width, @@ -165,8 +165,13 @@ class Affix extends React.Component { }; } + newState.lastAffix = !!newState.affixStyle; + if (onChange && lastAffix !== newState.lastAffix) { + onChange(newState.lastAffix); + } + this.setState(newState as AffixState); - } + }; // =================== Render =================== renderAffix = ({ getPrefixCls }: ConfigConsumerProps) => { @@ -182,11 +187,10 @@ class Affix extends React.Component { 'offsetBottom', 'target', 'onChange', - 'debug', ]); const mergedPlaceholderStyle = { ...(status === AffixStatus.None ? placeholderStyle : null), - ...style + ...style, }; return (
diff --git a/components/affix/utils.ts b/components/affix/utils.ts index 0db7e0a81d..60bad06ad1 100644 --- a/components/affix/utils.ts +++ b/components/affix/utils.ts @@ -1,5 +1,4 @@ import addEventListener from 'rc-util/lib/Dom/addEventListener'; -import getScroll from '../_util/getScroll'; import Affix from './'; // ======================== Observer ======================== @@ -22,7 +21,6 @@ interface ObserverEntity { let observerEntities: ObserverEntity[] = []; export function addObserveTarget(target: HTMLElement | Window | null, affix: Affix): void { - console.log('???', target); if (!target) return; let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target); @@ -75,22 +73,3 @@ export function getTargetRect(target: HTMLElement | Window | null): ClientRect { ? (target as HTMLElement).getBoundingClientRect() : ({ top: 0, bottom: window.innerHeight } as ClientRect); } - -export function getOffset(element: HTMLElement, target: HTMLElement | Window | null) { - const elemRect = element.getBoundingClientRect(); - const targetRect = getTargetRect(target); - - const scrollTop = getScroll(target, true); - const scrollLeft = getScroll(target, false); - - const docElem = window.document.body; - const clientTop = docElem.clientTop || 0; - const clientLeft = docElem.clientLeft || 0; - - return { - top: elemRect.top - targetRect.top + scrollTop - clientTop, - left: elemRect.left - targetRect.left + scrollLeft - clientLeft, - width: elemRect.width, - height: elemRect.height, - }; -} From 439053e4f72a9c5af08e12918d12409d95bd7678 Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 3 Mar 2019 23:19:07 +0800 Subject: [PATCH 3/4] update snapshot --- components/affix/__tests__/Affix.test.js | 89 ++++++++++++------------ components/affix/index.tsx | 11 ++- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/components/affix/__tests__/Affix.test.js b/components/affix/__tests__/Affix.test.js index be147e5353..4fed4e1f18 100644 --- a/components/affix/__tests__/Affix.test.js +++ b/components/affix/__tests__/Affix.test.js @@ -17,31 +17,21 @@ class AffixMounter extends React.Component { render() { return (
{ this.container = node; }} + className="container" > -
{ + this.affix = ele; }} + {...this.props} > - this.container} - ref={ele => { - this.affix = ele; - }} - {...this.props} - > - - -
+ +
); } @@ -50,24 +40,36 @@ class AffixMounter extends React.Component { describe('Affix Render', () => { let wrapper; + const classRect = { + container: { + top: 0, + bottom: 100, + }, + }; + + const originGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + HTMLElement.prototype.getBoundingClientRect = function() { + return ( + classRect[this.className] || { + top: 0, + bottom: 0, + } + ); + }; + beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); + HTMLElement.prototype.getBoundingClientRect = originGetBoundingClientRect; }); - - const scrollTo = top => { - wrapper.instance().affix.fixedNode.parentNode.getBoundingClientRect = jest.fn(() => ({ - bottom: 100, - height: 28, - left: 0, - right: 0, - top: 50 - top, - width: 195, - })); - wrapper.instance().container.scrollTop = top; + const movePlaceholder = top => { + classRect.fixed = { + top, + bottom: top, + }; events.scroll({ type: 'scroll', }); @@ -80,14 +82,14 @@ describe('Affix Render', () => { wrapper = mount(, { attachTo: document.getElementById('mounter') }); jest.runAllTimers(); - scrollTo(0); - expect(wrapper.instance().affix.state.affixStyle).toBe(null); + movePlaceholder(0); + expect(wrapper.instance().affix.state.affixStyle).toBeFalsy(); - scrollTo(100); - expect(wrapper.instance().affix.state.affixStyle).not.toBe(null); + movePlaceholder(-100); + expect(wrapper.instance().affix.state.affixStyle).toBeTruthy(); - scrollTo(0); - expect(wrapper.instance().affix.state.affixStyle).toBe(null); + movePlaceholder(0); + expect(wrapper.instance().affix.state.affixStyle).toBeFalsy(); }); it('support offsetBottom', () => { @@ -96,16 +98,17 @@ describe('Affix Render', () => { wrapper = mount(, { attachTo: document.getElementById('mounter'), }); + jest.runAllTimers(); - scrollTo(0); - expect(wrapper.instance().affix.state.affixStyle).not.toBe(null); + movePlaceholder(300); + expect(wrapper.instance().affix.state.affixStyle).toBeTruthy(); - scrollTo(100); - expect(wrapper.instance().affix.state.affixStyle).toBe(null); + movePlaceholder(0); + expect(wrapper.instance().affix.state.affixStyle).toBeFalsy(); - scrollTo(0); - expect(wrapper.instance().affix.state.affixStyle).not.toBe(null); + movePlaceholder(300); + expect(wrapper.instance().affix.state.affixStyle).toBeTruthy(); }); it('updatePosition when offsetTop changed', () => { @@ -116,7 +119,7 @@ describe('Affix Render', () => { }); jest.runAllTimers(); - scrollTo(100); + movePlaceholder(-100); expect(wrapper.instance().affix.state.affixStyle.top).toBe(0); wrapper.setProps({ offsetTop: 10, diff --git a/components/affix/index.tsx b/components/affix/index.tsx index 7b4cbf2657..c7a8a50d22 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -60,7 +60,8 @@ class Affix extends React.Component { componentDidMount() { const { target } = this.props; if (target) { - // Wait for parent component ref has its value + // [Legacy] Wait for parent component ref has its value. + // We should use target as directly element instead of function which makes element check hard. this.timeout = setTimeout(() => { addObserveTarget(target(), this); // Mock Event object. @@ -76,9 +77,17 @@ class Affix extends React.Component { if (target) { addObserveTarget(target(), this); // Mock Event object. + this.updatePosition({} as Event); } } + if ( + prevProps.offsetTop !== this.props.offsetTop || + prevProps.offsetBottom !== this.props.offsetBottom + ) { + this.updatePosition({} as Event); + } + this.measure(); } From fd0bea73329554eac6ac93d2160d31c5d6c54c86 Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 3 Mar 2019 23:25:52 +0800 Subject: [PATCH 4/4] fix size --- components/affix/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/affix/index.tsx b/components/affix/index.tsx index c7a8a50d22..90440ffc62 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -154,6 +154,8 @@ class Affix extends React.Component { newState.affixStyle = { position: 'fixed', top: offsetTop + targetRect.top, + width: placeholderReact.width, + height: placeholderReact.height, }; newState.placeholderStyle = { width: placeholderReact.width, @@ -167,6 +169,8 @@ class Affix extends React.Component { newState.affixStyle = { position: 'fixed', bottom: offsetBottom + targetBottomOffset, + width: placeholderReact.width, + height: placeholderReact.height, }; newState.placeholderStyle = { width: placeholderReact.width,