From cae4df8b8762d491abbcd868df9b1b44f6bb9c5c Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Thu, 14 Mar 2019 15:28:48 -0400 Subject: [PATCH] feat: Remove affix support (#287) --- src/Affix.js | 257 --------------------------------- src/AutoAffix.js | 170 ---------------------- src/index.js | 14 +- src/utils/getDocumentHeight.js | 13 -- src/utils/ownerWindow.js | 6 - test/AffixSpec.js | 249 -------------------------------- test/AutoAffixSpec.js | 107 -------------- www/src/examples/Affix.js | 15 -- www/src/pages/index.js | 32 ---- www/src/styles.less | 4 - 10 files changed, 6 insertions(+), 861 deletions(-) delete mode 100644 src/Affix.js delete mode 100644 src/AutoAffix.js delete mode 100644 src/utils/getDocumentHeight.js delete mode 100644 src/utils/ownerWindow.js delete mode 100644 test/AffixSpec.js delete mode 100644 test/AutoAffixSpec.js delete mode 100644 www/src/examples/Affix.js diff --git a/src/Affix.js b/src/Affix.js deleted file mode 100644 index cfaee3d1..00000000 --- a/src/Affix.js +++ /dev/null @@ -1,257 +0,0 @@ -import classNames from 'classnames' -import getHeight from 'dom-helpers/query/height' -import getOffset from 'dom-helpers/query/offset' -import listen from 'dom-helpers/events/listen' -import getOffsetParent from 'dom-helpers/query/offsetParent' -import getScrollTop from 'dom-helpers/query/scrollTop' -import requestAnimationFrame from 'dom-helpers/util/requestAnimationFrame' -import PropTypes from 'prop-types' -import React from 'react' -import ReactDOM from 'react-dom' - -import getDocumentHeight from './utils/getDocumentHeight' -import ownerDocument from './utils/ownerDocument' -import ownerWindow from './utils/ownerWindow' - -/** - * The `` component toggles `position: fixed;` on and off, emulating - * the effect found with `position: sticky;`. - */ -class Affix extends React.Component { - constructor(props, context) { - super(props, context) - - this.state = { - affixed: 'top', - position: null, - top: null, - } - } - - componentDidMount() { - this._isMounted = true - - this.removeScrollListener = listen(ownerWindow(this), 'scroll', () => - this.onWindowScroll() - ) - this.removeClickListener = listen(ownerDocument(this), 'click', () => - this.onDocumentClick() - ) - - this.onUpdate() - } - - componentDidUpdate(prevProps) { - if (prevProps !== this.props) { - this.onUpdate() - } - } - - componentWillUnmount() { - this._isMounted = false - - if (this.removeClickListener) this.removeClickListener() - if (this.removeScrollListener) this.removeScrollListener() - } - - onWindowScroll = () => { - this.onUpdate() - } - - onDocumentClick = () => { - requestAnimationFrame(() => this.onUpdate()) - } - - onUpdate = () => { - if (!this._isMounted) { - return - } - - const { offsetTop, viewportOffsetTop } = this.props - const scrollTop = getScrollTop(ownerWindow(this)) - const positionTopMin = scrollTop + (viewportOffsetTop || 0) - - if (positionTopMin <= offsetTop) { - this.updateState('top', null, null) - return - } - - if (positionTopMin > this.getPositionTopMax()) { - if (this.state.affixed === 'bottom') { - this.updateStateAtBottom() - } else { - // Setting position away from `fixed` can change the offset parent of - // the affix, so we can't calculate the correct position until after - // we've updated its position. - this.setState( - { - affixed: 'bottom', - position: 'absolute', - top: null, - }, - () => { - if (!this._isMounted) { - return - } - - this.updateStateAtBottom() - } - ) - } - return - } - - this.updateState('affix', 'fixed', viewportOffsetTop) - } - - getPositionTopMax = () => { - const documentHeight = getDocumentHeight(ownerDocument(this)) - const height = getHeight(ReactDOM.findDOMNode(this)) - - return documentHeight - height - this.props.offsetBottom - } - - updateState = (affixed, position, top) => { - if ( - affixed === this.state.affixed && - position === this.state.position && - top === this.state.top - ) { - return - } - - let upperName = - affixed === 'affix' - ? '' - : affixed.charAt(0).toUpperCase() + affixed.substr(1) - - if (this.props['onAffix' + upperName]) { - this.props['onAffix' + upperName]() - } - - this.setState({ affixed, position, top }, () => { - if (this.props['onAffixed' + upperName]) { - this.props['onAffixed' + upperName]() - } - }) - } - - updateStateAtBottom = () => { - const positionTopMax = this.getPositionTopMax() - const offsetParent = getOffsetParent(ReactDOM.findDOMNode(this)) - const parentTop = getOffset(offsetParent).top - - this.updateState('bottom', 'absolute', positionTopMax - parentTop) - } - - render() { - const child = React.Children.only(this.props.children) - const { className, style } = child.props - - const { affixed, position, top } = this.state - const positionStyle = { position, top } - - let affixClassName - let affixStyle - if (affixed === 'top') { - affixClassName = this.props.topClassName - affixStyle = this.props.topStyle - } else if (affixed === 'bottom') { - affixClassName = this.props.bottomClassName - affixStyle = this.props.bottomStyle - } else { - affixClassName = this.props.affixClassName - affixStyle = this.props.affixStyle - } - - return React.cloneElement(child, { - className: classNames(affixClassName, className), - style: { ...positionStyle, ...affixStyle, ...style }, - }) - } -} - -Affix.propTypes = { - /** - * Pixels to offset from top of screen when calculating position - */ - offsetTop: PropTypes.number, - - /** - * When affixed, pixels to offset from top of viewport - */ - viewportOffsetTop: PropTypes.number, - - /** - * Pixels to offset from bottom of screen when calculating position - */ - offsetBottom: PropTypes.number, - - /** - * CSS class or classes to apply when at top - */ - topClassName: PropTypes.string, - - /** - * Style to apply when at top - */ - topStyle: PropTypes.object, - - /** - * CSS class or classes to apply when affixed - */ - affixClassName: PropTypes.string, - - /** - * Style to apply when affixed - */ - affixStyle: PropTypes.object, - - /** - * CSS class or classes to apply when at bottom - */ - bottomClassName: PropTypes.string, - - /** - * Style to apply when at bottom - */ - bottomStyle: PropTypes.object, - - /** - * Callback fired right before the `affixStyle` and `affixClassName` props are rendered - */ - onAffix: PropTypes.func, - - /** - * Callback fired after the component `affixStyle` and `affixClassName` props have been rendered - */ - onAffixed: PropTypes.func, - - /** - * Callback fired right before the `topStyle` and `topClassName` props are rendered - */ - onAffixTop: PropTypes.func, - - /** - * Callback fired after the component `topStyle` and `topClassName` props have been rendered - */ - onAffixedTop: PropTypes.func, - - /** - * Callback fired right before the `bottomStyle` and `bottomClassName` props are rendered - */ - onAffixBottom: PropTypes.func, - - /** - * Callback fired after the component `bottomStyle` and `bottomClassName` props have been rendered - */ - onAffixedBottom: PropTypes.func, -} - -Affix.defaultProps = { - offsetTop: 0, - viewportOffsetTop: null, - offsetBottom: 0, -} - -export default Affix diff --git a/src/AutoAffix.js b/src/AutoAffix.js deleted file mode 100644 index c412cd69..00000000 --- a/src/AutoAffix.js +++ /dev/null @@ -1,170 +0,0 @@ -import getOffset from 'dom-helpers/query/offset' -import listen from 'dom-helpers/events/listen' -import requestAnimationFrame from 'dom-helpers/util/requestAnimationFrame' -import PropTypes from 'prop-types' -import componentOrElement from 'prop-types-extra/lib/componentOrElement' -import React from 'react' - -import Affix from './Affix' -import getContainer from './utils/getContainer' -import getDocumentHeight from './utils/getDocumentHeight' -import ownerDocument from './utils/ownerDocument' -import ownerWindow from './utils/ownerWindow' - -const displayName = 'AutoAffix' - -const propTypes = { - ...Affix.propTypes, - /** - * The logical container node or component for determining offset from bottom - * of viewport, or a function that returns it - */ - container: PropTypes.oneOfType([componentOrElement, PropTypes.func]), - /** - * Automatically set width when affixed - */ - autoWidth: PropTypes.bool, -} - -// This intentionally doesn't inherit default props from ``, so that the -// auto-calculated offsets can apply. -const defaultProps = { - viewportOffsetTop: 0, - autoWidth: true, -} - -/** - * The `` component wraps `` to automatically calculate - * offsets in many common cases. - */ -class AutoAffix extends React.Component { - constructor(props, context) { - super(props, context) - - this.state = { - offsetTop: null, - offsetBottom: null, - width: null, - } - } - - componentDidMount() { - this._isMounted = true - - this.removeScrollListener = listen(ownerWindow(this), 'scroll', () => - this.onWindowScroll() - ) - - this.removeResizeListener = listen(ownerWindow(this), 'resize', () => - this.onWindowResize() - ) - - this.removeClickListener = listen(ownerDocument(this), 'click', () => - this.onDocumentClick() - ) - - this.onUpdate() - } - - componentDidUpdate(prevProps) { - if (prevProps !== this.props) { - this.onUpdate() - } - } - - componentWillUnmount() { - this._isMounted = false - - if (this.removeScrollListener) this.removeScrollListener() - if (this.removeClickListener) this.removeClickListener() - if (this.removeResizeListener) this.removeResizeListener() - } - - onWindowScroll = () => { - this.onUpdate() - } - - onWindowResize = () => { - if (this.props.autoWidth) { - requestAnimationFrame(() => this.onUpdate()) - } - } - - onDocumentClick = () => { - requestAnimationFrame(() => this.onUpdate()) - } - - onUpdate = () => { - if (!this._isMounted) { - return - } - - const { top: offsetTop, width } = getOffset(this.positioner) - - const container = getContainer(this.props.container) - let offsetBottom - if (container) { - const documentHeight = getDocumentHeight(ownerDocument(this)) - const { top, height } = getOffset(container) - offsetBottom = documentHeight - top - height - } else { - offsetBottom = null - } - - this.updateState(offsetTop, offsetBottom, width) - } - - updateState = (offsetTop, offsetBottom, width) => { - if ( - offsetTop === this.state.offsetTop && - offsetBottom === this.state.offsetBottom && - width === this.state.width - ) { - return - } - - this.setState({ offsetTop, offsetBottom, width }) - } - - render() { - const { autoWidth, viewportOffsetTop, children, ...props } = this.props - const { offsetTop, offsetBottom, width } = this.state - - delete props.container - - const effectiveOffsetTop = Math.max(offsetTop, viewportOffsetTop || 0) - - let { affixStyle, bottomStyle } = this.props - if (autoWidth) { - affixStyle = { width, ...affixStyle } - bottomStyle = { width, ...bottomStyle } - } - - return ( -
-
{ - this.positioner = c - }} - /> - - - {children} - -
- ) - } -} - -AutoAffix.displayName = displayName -AutoAffix.propTypes = propTypes -AutoAffix.defaultProps = defaultProps - -export default AutoAffix diff --git a/src/index.js b/src/index.js index 61249a6e..aada524f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,7 @@ -import Affix from './Affix' -import AutoAffix from './AutoAffix' -import Modal from './Modal' -import Overlay from './Overlay' -import Portal from './Portal' -import RootCloseWrapper from './RootCloseWrapper' -import Dropdown from './Dropdown' +import Dropdown from './Dropdown'; +import Modal from './Modal'; +import Overlay from './Overlay'; +import Portal from './Portal'; +import RootCloseWrapper from './RootCloseWrapper'; -export { Affix, AutoAffix, Dropdown, Modal, Overlay, Portal, RootCloseWrapper } +export { Dropdown, Modal, Overlay, Portal, RootCloseWrapper }; diff --git a/src/utils/getDocumentHeight.js b/src/utils/getDocumentHeight.js deleted file mode 100644 index 850d809e..00000000 --- a/src/utils/getDocumentHeight.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Get the height of the document - * - * @returns {documentHeight: number} - */ -export default function (doc) { - return Math.max( - doc.documentElement.offsetHeight || 0, - doc.height || 0, - doc.body.scrollHeight || 0, - doc.body.offsetHeight || 0 - ); -} diff --git a/src/utils/ownerWindow.js b/src/utils/ownerWindow.js deleted file mode 100644 index 0708c357..00000000 --- a/src/utils/ownerWindow.js +++ /dev/null @@ -1,6 +0,0 @@ -import ReactDOM from 'react-dom'; -import ownerWindow from 'dom-helpers/ownerWindow'; - -export default function (componentOrElement) { - return ownerWindow(ReactDOM.findDOMNode(componentOrElement)); -} diff --git a/test/AffixSpec.js b/test/AffixSpec.js deleted file mode 100644 index d180ea0c..00000000 --- a/test/AffixSpec.js +++ /dev/null @@ -1,249 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import ReactTestUtils from 'react-dom/test-utils' - -import Affix from '../src/Affix' - -import { render, injectCss } from './helpers' - -describe('', () => { - let mountPoint - let handlers - - before(() => { - injectCss(` - html, body { margin: 0; padding: 0; } - `) - }) - - after(() => { - injectCss.reset() - }) - // This makes the window very tall; hopefully enough to exhibit the affix - // behavior. If this is insufficient, we should modify the Karma config to - // fix the browser window size. - class Container extends React.Component { - render() { - return ( -
- Placeholder - {this.props.children} -
- ) - } - } - - class Content extends React.Component { - render() { - ++Content.renderCount - return
Content
- } - } - - beforeEach(() => { - Content.renderCount = 0 - - mountPoint = document.createElement('div') - document.body.appendChild(mountPoint) - handlers = { - onAffix: sinon.spy(), - onAffixed: sinon.spy(), - onAffixTop: sinon.spy(), - onAffixedTop: sinon.spy(), - onAffixBottom: sinon.spy(), - onAffixedBottom: sinon.spy(), - } - }) - - afterEach(() => { - Object.keys(handlers).forEach(key => handlers[key].resetHistory()) - ReactDOM.unmountComponentAtNode(mountPoint) - document.body.removeChild(mountPoint) - window.scrollTo(0, 0) - }) - - it('should render the affix content', () => { - let instance = render( - - - , - mountPoint - ) - - const content = ReactTestUtils.findRenderedComponentWithType( - instance, - Content - ) - - expect(content).to.exist - }) - - describe('no viewportOffsetTop', () => { - let node - - beforeEach(() => { - const container = render( - - - - - , - mountPoint - ) - - node = ReactDOM.findDOMNode( - ReactTestUtils.findRenderedComponentWithType(container, Content) - ) - }) - - it('should render correctly at top', done => { - window.scrollTo(0, 101) - - requestAnimationFrame(() => { - window.scrollTo(0, 99) - requestAnimationFrame(() => { - expect(node.className).to.equal('affix-top') - expect(node.style.position).to.not.be.ok - expect(node.style.top).to.not.be.ok - expect(node.style.color).to.equal('red') - expect(handlers.onAffixTop).to.been.calledOnce - expect(handlers.onAffixedTop).to.been.calledOnce - done() - }) - }) - }) - - it('should affix correctly', done => { - window.scrollTo(0, 101) - requestAnimationFrame(() => { - expect(node.className).to.equal('affix') - expect(node.style.position).to.equal('fixed') - expect(node.style.top).to.not.be.ok - expect(node.style.color).to.equal('white') - - expect(handlers.onAffix).to.been.calledOnce - expect(handlers.onAffixed).to.been.calledOnce - done() - }) - }) - - it('should render correctly at bottom', done => { - window.scrollTo(0, 20000) - requestAnimationFrame(() => { - expect(node.className).to.equal('affix-bottom') - expect(node.style.position).to.equal('absolute') - expect(node.style.top).to.equal('9900px') - expect(node.style.color).to.equal('blue') - - expect(handlers.onAffixBottom).to.been.calledOnce - expect(handlers.onAffixedBottom).to.been.calledOnce - done() - }) - }) - }) - - describe('with viewportOffsetTop', () => { - let node - - beforeEach(() => { - const container = render( - - - - - , - mountPoint - ) - - node = ReactDOM.findDOMNode( - ReactTestUtils.findRenderedComponentWithType(container, Content) - ) - }) - - it('should render correctly at top', done => { - window.scrollTo(0, 49) - - requestAnimationFrame(() => { - expect(node.style.position).to.not.be.ok - expect(node.style.top).to.not.be.ok - done() - }) - }) - - it('should affix correctly', done => { - window.scrollTo(0, 51) - requestAnimationFrame(() => { - expect(node.style.position).to.equal('fixed') - expect(node.style.top).to.equal('50px') - done() - }) - }) - }) - - describe('re-rendering optimizations', () => { - beforeEach(() => { - render( - - - - - , - mountPoint - ) - }) - - it('should avoid re-rendering at top', done => { - expect(Content.renderCount).to.equal(1) - - window.scrollTo(0, 50) - requestAnimationFrame(() => { - expect(Content.renderCount).to.equal(1) - done() - }) - }) - - it('should avoid re-rendering when affixed', done => { - expect(Content.renderCount).to.equal(1) - - window.scrollTo(0, 1000) - requestAnimationFrame(() => { - expect(Content.renderCount).to.equal(2) - - window.scrollTo(0, 2000) - requestAnimationFrame(() => { - expect(Content.renderCount).to.equal(2) - done() - }) - }) - }) - - it('should avoid re-rendering at bottom', done => { - expect(Content.renderCount).to.equal(1) - - window.scrollTo(0, 15000) - requestAnimationFrame(() => { - expect(Content.renderCount).to.equal(3) - - window.scrollTo(0, 16000) - requestAnimationFrame(() => { - expect(Content.renderCount).to.equal(3) - done() - }) - }) - }) - }) -}) diff --git a/test/AutoAffixSpec.js b/test/AutoAffixSpec.js deleted file mode 100644 index 925e42b2..00000000 --- a/test/AutoAffixSpec.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import AutoAffix from '../src/AutoAffix'; - -import { render } from './helpers'; - -describe('', () => { - let mountPoint; - - // This makes the window very tall; hopefully enough to exhibit the affix - // behavior. If this is insufficient, we should modify the Karma config to - // fix the browser window size. - class Container extends React.Component { - render() { - return ( -
- {this.props.children} -
- ); - } - } - - class Content extends React.Component { - render() { - return
Content
; - } - } - - class Fixture extends React.Component { - render() { - return ( -
-
- - - - -
- ); - } - } - - beforeEach(() => { - mountPoint = document.createElement('div'); - document.body.appendChild(mountPoint); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(mountPoint); - document.body.removeChild(mountPoint); - window.scrollTo(0, 0); - }); - - describe('affix behavior', () => { - let node; - - beforeEach(() => { - const container = render(( - - - - ), mountPoint); - - node = ReactDOM.findDOMNode(ReactTestUtils.findRenderedComponentWithType( - container, Content - )); - }); - - it('should render correctly at top', (done) => { - window.scrollTo(0, 99); - - requestAnimationFrame(() => { - expect(node.style.position).to.not.be.ok; - expect(node.style.top).to.not.be.ok; - expect(node.style.width).to.not.be.ok; - done(); - }); - }); - - it('should affix correctly', (done) => { - window.scrollTo(0, 101); - requestAnimationFrame(() => { - expect(node.style.position).to.equal('fixed'); - expect(node.style.top).to.equal('0px'); - expect(node.style.width).to.equal('200px'); - done(); - }); - }); - - it('should render correctly at bottom', (done) => { - window.scrollTo(0, 9901); - - requestAnimationFrame(() => { - expect(node.style.position).to.equal('absolute'); - expect(node.style.top).to.equal('9900px'); - expect(node.style.width).to.equal('200px'); - done(); - }); - }); - }); -}); diff --git a/www/src/examples/Affix.js b/www/src/examples/Affix.js deleted file mode 100644 index a6be8a9a..00000000 --- a/www/src/examples/Affix.js +++ /dev/null @@ -1,15 +0,0 @@ -class AffixExample extends React.Component { - render() { - return ( -
- -
-
I am an affixed element
-
-
-
- ); - } -} - -render(AffixExample); diff --git a/www/src/pages/index.js b/www/src/pages/index.js index 158e8079..ec32cc4b 100644 --- a/www/src/pages/index.js +++ b/www/src/pages/index.js @@ -4,7 +4,6 @@ import { graphql } from 'gatsby'; import PropTable from '../components/PropTable'; import Playground from '../components/Playground'; -import AffixSource from '../examples/Affix'; import ModalExample from '../examples/Modal'; import OverlaySource from '../examples/Overlay'; import DropdownSource from '../examples/Dropdown'; @@ -37,8 +36,6 @@ class Example extends React.Component { render() { const { - AffixMetadata, - AutoAffixMetadata, ModalMetadata, PortalMetadata, OverlayMetadata, @@ -67,9 +64,6 @@ class Example extends React.Component {
  • Overlay
  • -
  • - Affixes -
  • RootCloseWrapper
  • @@ -115,7 +109,6 @@ class Example extends React.Component { -

    Dropdown @@ -138,25 +131,6 @@ class Example extends React.Component { metadata={DropdownToggleMetadata} />

    - -
    -

    - Affixes -

    -

    -

    - - - -

    RootCloseWrapper @@ -205,12 +179,6 @@ export default Example; export const pageQuery = graphql` query SiteQuery { - AffixMetadata: componentMetadata(displayName: { eq: "Affix" }) { - ...PropTable_metadata - } - AutoAffixMetadata: componentMetadata(displayName: { eq: "AutoAffix" }) { - ...PropTable_metadata - } ModalMetadata: componentMetadata(displayName: { eq: "Modal" }) { ...PropTable_metadata } diff --git a/www/src/styles.less b/www/src/styles.less index 4fbfece8..f51edbcd 100644 --- a/www/src/styles.less +++ b/www/src/styles.less @@ -136,7 +136,3 @@ h4 a:focus .anchor-icon { margin-bottom: 10px; } } - -.affix-example { - height: 500px; -}