Skip to content

Commit

Permalink
[added] Affix and AutoAffix
Browse files Browse the repository at this point in the history
  • Loading branch information
taion committed Nov 2, 2015
1 parent 87fad33 commit e965152
Show file tree
Hide file tree
Showing 11 changed files with 767 additions and 8 deletions.
20 changes: 20 additions & 0 deletions examples/Affix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import AutoAffix from 'react-overlays/lib/AutoAffix';

class AffixExample extends React.Component {
render() {
return (
<div className='affix-example'>
<AutoAffix viewportOffsetTop={15} container={this}>
<div className='panel panel-default'>
<div className='panel-body'>
I am an affixed element
</div>
</div>
</AutoAffix>
</div>
);
}
}

export default AffixExample;
25 changes: 24 additions & 1 deletion examples/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@ import Editor from '@jquense/component-playground';

import PropTable from './PropTable';

import AffixSource from '../webpack/example-loader!./Affix';
import ModalExample from '../webpack/example-loader!./Modal';
import OverlaySource from '../webpack/example-loader!./Overlay';
import PortalSource from '../webpack/example-loader!./Portal';
import PositionSource from '../webpack/example-loader!./Position';
import TransitionSource from '../webpack/example-loader!./Transition';

import AffixMetadata from '../webpack/metadata-loader!react-overlays/Affix';
import AutoAffixMetadata from '../webpack/metadata-loader!react-overlays/AutoAffix';
import PortalMetadata from '../webpack/metadata-loader!react-overlays/Portal';
import PositionMetadata from '../webpack/metadata-loader!react-overlays/Position';
import OverlayMetadata from '../webpack/metadata-loader!react-overlays/Overlay';
import ModalMetadata from '../webpack/metadata-loader!react-overlays/Modal';
import TransitionMetadata from '../webpack/metadata-loader!react-overlays/Transition';

import * as ReactOverlays from 'react-overlays';
import getOffset from 'dom-helpers/query/offset';

import './styles.less';
import injectCss from './injectCss';

let scope = { React, findDOMNode, Button, injectCss, ...ReactOverlays };
let scope = {
React, findDOMNode, Button, injectCss, ...ReactOverlays, getOffset
};

const Anchor = React.createClass({
propTypes: {
Expand Down Expand Up @@ -72,6 +78,7 @@ const Example = React.createClass({
<li><a href='#modals'>Modals</a></li>
<li><a href='#position'>Position</a></li>
<li><a href='#overlay'>Overlay</a></li>
<li><a href='#affixes'>Affixes</a></li>
</ul>
</article>
<main className='col-md-10'>
Expand Down Expand Up @@ -130,6 +137,22 @@ const Example = React.createClass({
metadata={OverlayMetadata}
/>
</section>
<section>
<h2 className='page-header'>
<Anchor>Affixes</Anchor>
</h2>
<p dangerouslySetInnerHTML={{__html: AffixMetadata.Affix.descHtml }}/>
<p dangerouslySetInnerHTML={{__html: AutoAffixMetadata.AutoAffix.descHtml }}/>
<ExampleEditor codeText={AffixSource} />
<PropTable
component='Affix'
metadata={AffixMetadata}
/>
<PropTable
component='AutoAffix'
metadata={AutoAffixMetadata}
/>
</section>
</main>
</div>
);
Expand Down
7 changes: 4 additions & 3 deletions examples/PropTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ const PropTable = React.createClass({

render(){
let propsData = this.propsData;
let composes = this.props.metadata[this.props.component].composes || [];

if ( !Object.keys(propsData).length ){
return <span/>;
}

let {component, metadata} = this.props;
let composes = metadata[component].composes || [];

return (
<div>
<h3>
Props
{component} Props
{ !!composes.length && [<br/>,
<small>
{'Also accepts the same props as: '}
Expand Down
6 changes: 5 additions & 1 deletion examples/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,8 @@ h4 a:focus .anchor-icon {
button {
margin-bottom: 10px;
}
}
}

.affix-example {
height: 500px;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@
"mt-changelog": "^0.6.1",
"node-libs-browser": "^0.5.2",
"raw-loader": "^0.5.1",
"react": "0.14.0",
"react": "^0.14.0",
"react-addons-test-utils": "^0.14.0",
"react-bootstrap": "0.24.5-react-pre.0",
"react-bootstrap": "^0.27.3",
"react-component-metadata": "^1.2.2",
"react-dom": "^0.14.0",
"react-hot-loader": "^1.2.7",
Expand Down
213 changes: 213 additions & 0 deletions src/Affix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import classNames from 'classnames';
import getHeight from 'dom-helpers/query/height';
import getOffset from 'dom-helpers/query/offset';
import getOffsetParent from 'dom-helpers/query/offsetParent';
import getScrollTop from 'dom-helpers/query/scrollTop';
import requestAnimationFrame from 'dom-helpers/util/requestAnimationFrame';
import React from 'react';
import ReactDOM from 'react-dom';

import addEventListener from './utils/addEventListener';
import getDocumentHeight from './utils/getDocumentHeight';
import ownerDocument from './utils/ownerDocument';
import ownerWindow from './utils/ownerWindow';

/**
* The `<Affix/>` 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
};

this._needPositionUpdate = false;
}

componentDidMount() {
this._isMounted = true;

this._windowScrollListener = addEventListener(
ownerWindow(this), 'scroll', () => this.onWindowScroll()
);
this._documentClickListener = addEventListener(
ownerDocument(this), 'click', () => this.onDocumentClick()
);

this.onUpdate();
}

componentWillReceiveProps() {
this._needPositionUpdate = true;
}

componentDidUpdate() {
if (this._needPositionUpdate) {
this._needPositionUpdate = false;
this.onUpdate();
}
}

componentWillUnmount() {
this._isMounted = false;

if (this._windowScrollListener) {
this._windowScrollListener.remove();
}
if (this._documentClickListener) {
this._documentClickListener.remove();
}
}

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;
}

this.setState({affixed, position, top});
}

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: React.PropTypes.number,
/**
* When affixed, pixels to offset from top of viewport
*/
viewportOffsetTop: React.PropTypes.number,
/**
* Pixels to offset from bottom of screen when calculating position
*/
offsetBottom: React.PropTypes.number,
/**
* CSS class or classes to apply when at top
*/
topClassName: React.PropTypes.string,
/**
* Style to apply when at top
*/
topStyle: React.PropTypes.object,
/**
* CSS class or classes to apply when affixed
*/
affixClassName: React.PropTypes.string,
/**
* Style to apply when affixed
*/
affixStyle: React.PropTypes.object,
/**
* CSS class or classes to apply when at bottom
*/
bottomClassName: React.PropTypes.string,
/**
* Style to apply when at bottom
*/
bottomStyle: React.PropTypes.object
};

Affix.defaultProps = {
offsetTop: 0,
viewportOffsetTop: null,
offsetBottom: 0
};

export default Affix;

0 comments on commit e965152

Please sign in to comment.