semantic-ui-react
Version:
The official Semantic-UI-React integration.
304 lines (235 loc) • 7.93 kB
JavaScript
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import {
eventStack,
customPropTypes,
getElementType,
getUnhandledProps,
isBrowser,
META,
} from '../../lib'
/**
* Sticky content stays fixed to the browser viewport while another column of content is visible on the page.
*/
export default class Sticky extends Component {
static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,
/** A Sticky can be active. */
active: PropTypes.bool,
/** Offset in pixels from the bottom of the screen when fixing element to viewport. */
bottomOffset: PropTypes.number,
/** Primary content. */
children: PropTypes.node,
/** Additional classes. */
className: PropTypes.string,
/** Context which sticky element should stick to. */
context: PropTypes.object,
/** Offset in pixels from the top of the screen when fixing element to viewport. */
offset: PropTypes.number,
/**
* Callback when element is bound to bottom of parent container.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onBottom: PropTypes.func,
/**
* Callback when element is fixed to page.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onStick: PropTypes.func,
/**
* Callback when element is bound to top of parent container.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onTop: PropTypes.func,
/**
* Callback when element is unfixed from page.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onUnstick: PropTypes.func,
/** Whether element should be "pushed" by the viewport, attaching to the bottom of the screen when scrolling up. */
pushing: PropTypes.bool,
/** Context which sticky should attach onscroll events. */
scrollContext: PropTypes.object,
}
static defaultProps = {
active: true,
bottomOffset: 0,
offset: 0,
scrollContext: isBrowser() ? window : null,
}
static _meta = {
name: 'Sticky',
type: META.TYPES.MODULE,
}
state = {
sticky: false,
}
componentDidMount() {
if (!isBrowser()) return
const { active } = this.props
if (active) {
this.handleUpdate()
this.addListeners(this.props)
}
}
componentWillReceiveProps(nextProps) {
const { active: current } = this.props
const { active: next } = nextProps
if (current === next) return
if (next) {
this.handleUpdate()
this.addListeners(nextProps)
return
}
this.removeListeners()
this.setState({ sticky: false })
}
componentWillUnmount() {
if (!isBrowser()) return
const { active } = this.props
if (active) this.removeListeners()
}
// ----------------------------------------
// Events
// ----------------------------------------
addListeners = (props) => {
const { scrollContext } = props
eventStack.sub('resize', this.handleUpdate, { target: scrollContext })
eventStack.sub('scroll', this.handleUpdate, { target: scrollContext })
}
removeListeners = () => {
const { scrollContext } = this.props
eventStack.unsub('resize', this.handleUpdate, { target: scrollContext })
eventStack.unsub('scroll', this.handleUpdate, { target: scrollContext })
}
// ----------------------------------------
// Handlers
// ----------------------------------------
update = (e) => {
const { pushing } = this.state
this.ticking = false
this.assignRects()
if (pushing) {
if (this.didReachStartingPoint()) return this.stickToContextTop(e)
if (this.didTouchScreenBottom()) return this.stickToScreenBottom(e)
return this.stickToContextBottom(e)
}
if (this.isOversized()) {
if (this.contextRect.top > 0) return this.stickToContextTop(e)
if (this.contextRect.bottom < window.innerHeight) return this.stickToContextBottom(e)
}
if (this.didTouchScreenTop()) {
if (this.didReachContextBottom()) return this.stickToContextBottom(e)
return this.stickToScreenTop(e)
}
return this.stickToContextTop(e)
}
handleUpdate = (e) => {
if (!this.ticking) {
this.ticking = true
requestAnimationFrame(() => this.update(e))
}
}
// ----------------------------------------
// Helpers
// ----------------------------------------
assignRects = () => {
const { context } = this.props
this.triggerRect = this.triggerRef.getBoundingClientRect()
this.contextRect = (context || document.body).getBoundingClientRect()
this.stickyRect = this.stickyRef.getBoundingClientRect()
}
computeStyle() {
const { bottom, sticky, top } = this.state
if (!sticky) return {}
return {
bottom,
top,
position: 'fixed',
width: this.triggerRect.width,
}
}
// Return true when the component reached the bottom of the context
didReachContextBottom = () => {
const { offset } = this.props
return (this.stickyRect.height + offset) >= this.contextRect.bottom
}
// Return true when the component reached the starting point
didReachStartingPoint = () => this.stickyRect.top <= this.triggerRect.top
// Return true when the top of the screen overpasses the Sticky component
didTouchScreenTop = () => this.triggerRect.top < this.props.offset
// Return true when the bottom of the screen overpasses the Sticky component
didTouchScreenBottom = () => {
const { bottomOffset } = this.props
return (this.contextRect.bottom + bottomOffset) > window.innerHeight
}
// Return true if the height of the component is higher than the window
isOversized = () => this.stickyRect.height > window.innerHeight
// ----------------------------------------
// Stick helpers
// ----------------------------------------
// If true, the component will stick to the bottom of the screen instead of the top
pushing = (pushing) => {
const { pushing: possible } = this.props
if (possible) this.setState({ pushing })
}
stick = (e) => {
this.setState({ sticky: true })
_.invoke(this.props, 'onStick', e, this.props)
}
unstick = (e) => {
this.setState({ sticky: false })
_.invoke(this.props, 'onUnstick', e, this.props)
}
stickToContextBottom = (e) => {
const top = this.contextRect.bottom - this.stickyRect.height
_.invoke(this.props, 'onBottom', e, this.props)
this.stick(e)
this.setState({ top, bottom: null })
this.pushing(true)
}
stickToContextTop = (e) => {
_.invoke(this.props, 'onTop', e, this.props)
this.unstick(e)
this.pushing(false)
}
stickToScreenBottom = (e) => {
const { bottomOffset: bottom } = this.props
this.stick(e)
this.setState({ bottom, top: null })
}
stickToScreenTop = (e) => {
const { offset: top } = this.props
this.stick(e)
this.setState({ top, bottom: null })
}
// ----------------------------------------
// Refs
// ----------------------------------------
handleStickyRef = c => (this.stickyRef = c)
handleTriggerRef = c => (this.triggerRef = c)
// ----------------------------------------
// Render
// ----------------------------------------
render() {
const { children, className } = this.props
const rest = getUnhandledProps(Sticky, this.props)
const ElementType = getElementType(Sticky, this.props)
return (
<ElementType {...rest} className={className}>
<div ref={this.handleTriggerRef} />
<div ref={this.handleStickyRef} style={this.computeStyle()}>{children}</div>
</ElementType>
)
}
}