diff --git a/frontend/src/Author/Details/AuthorDetails.css b/frontend/src/Author/Details/AuthorDetails.css index fb7a51529..7c5942678 100644 --- a/frontend/src/Author/Details/AuthorDetails.css +++ b/frontend/src/Author/Details/AuthorDetails.css @@ -78,7 +78,6 @@ .toggleMonitoredContainer { align-self: center; - margin-right: 10px; } .monitorToggleButton { diff --git a/frontend/src/Author/Details/AuthorDetails.js b/frontend/src/Author/Details/AuthorDetails.js index adc096f73..17f4d746b 100644 --- a/frontend/src/Author/Details/AuthorDetails.js +++ b/frontend/src/Author/Details/AuthorDetails.js @@ -223,7 +223,7 @@ class AuthorDetails extends Component { overviewHeight } = this.state; - const marqueeWidth = (titleWidth - 170); + const marqueeWidth = (titleWidth - 165); const continuing = status === 'continuing'; diff --git a/frontend/src/Book/Details/BookDetails.css b/frontend/src/Book/Details/BookDetails.css index 8ea082e55..23740ac30 100644 --- a/frontend/src/Book/Details/BookDetails.css +++ b/frontend/src/Book/Details/BookDetails.css @@ -65,7 +65,6 @@ .toggleMonitoredContainer { align-self: center; - margin-right: 10px; } .monitorToggleButton { diff --git a/frontend/src/Book/Details/BookDetails.js b/frontend/src/Book/Details/BookDetails.js index 9abd9b610..81e4181d5 100644 --- a/frontend/src/Book/Details/BookDetails.js +++ b/frontend/src/Book/Details/BookDetails.js @@ -158,7 +158,7 @@ class BookDetails extends Component { overviewHeight } = this.state; - const marqueeWidth = (titleWidth - 170); + const marqueeWidth = (titleWidth - 165); return ( diff --git a/frontend/src/Components/Marquee.css b/frontend/src/Components/Marquee.css new file mode 100644 index 000000000..b7f9276fb --- /dev/null +++ b/frontend/src/Components/Marquee.css @@ -0,0 +1,17 @@ +.container { + position: relative; + overflow: hidden; + padding-left: 10px; + white-space: nowrap; + /* stylelint-disable-next-line property-no-vendor-prefix */ + -webkit-mask-image: linear-gradient(to right, transparent, $white 10px, $white 90%, transparent), linear-gradient(to left, transparent, $white 10px, $white 90%, transparent); + mask-image: linear-gradient(to right, transparent, $white 10px, $white 90%, transparent), linear-gradient(to left, transparent, $white 10px, $white 90%, transparent); +} + +.inner { + transition: transform var(--duration) ease-in-out; +} + +.toLeft { + transform: translateX(var(--distance)); +} diff --git a/frontend/src/Components/Marquee.js b/frontend/src/Components/Marquee.js index 900ccafd6..1c0d55a98 100644 --- a/frontend/src/Components/Marquee.js +++ b/frontend/src/Components/Marquee.js @@ -1,181 +1,144 @@ +import classNames from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Measure from './Measure'; +import styles from './Marquee.css'; -const FPS = 20; -const STEP = 1; -const TIMEOUT = 1 / FPS * 1000; +const SPEED = 50; // pixels per second class Marquee extends Component { - static propTypes = { - text: PropTypes.string, - title: PropTypes.string, - hoverToStop: PropTypes.bool, - loop: PropTypes.bool, - className: PropTypes.string - }; - - static defaultProps = { - text: '', - title: '', - hoverToStop: true, - loop: false - }; - - state = { - animatedWidth: 0, - overflowWidth: 0, - direction: 0 - }; - - componentDidMount() { - this.measureText(); - - if (this.props.hoverToStop) { - this.startAnimation(); - } + // + // Lifecycle + + constructor(props) { + super(props); + + this.state = { + containerWidth: 0, + overflowWidth: 0, + animationState: null, + key: 0 + }; } - componentWillReceiveProps(nextProps) { - if (this.props.text.length !== nextProps.text.length) { - clearTimeout(this.marqueeTimer); - this.setState({ animatedWidth: 0, direction: 0 }); + componentDidUpdate(prevProps) { + if (this.props.text !== prevProps.text) { + // reset the component, set a new key to force re-render so new text isn't in old position + this.setState({ + overflowWidth: 0, + animationState: null, + key: this.state.key + 1 + }); + return; } - } - componentDidUpdate() { - this.measureText(); + const containerWidth = this.state.containerWidth; + const node = this.text; + + if (containerWidth && node) { + const textWidth = node.offsetWidth; + // eslint-disable-next-line no-bitwise + const overflowWidth = (textWidth - containerWidth + 10) | 0; // 10 margin, round towards 0 + + if (overflowWidth !== this.state.overflowWidth) { + const triggerUpdate = overflowWidth > 0 && this.state.overflowWidth === 0; - if (this.props.hoverToStop) { - this.startAnimation(); + this.setState({ overflowWidth }, () => { + if (triggerUpdate) { + this.onHandleMouseEnter(); + } + }); + } } } - componentWillUnmount() { - clearTimeout(this.marqueeTimer); - } + // + // Listeners onHandleMouseEnter = () => { - if (this.props.hoverToStop) { - clearTimeout(this.marqueeTimer); - } else if (this.state.overflowWidth > 0) { - this.startAnimation(); - } - } + const { + animationState, + overflowWidth + } = this.state; - onHandleMouseLeave = () => { - if (this.props.hoverToStop && this.state.overflowWidth > 0) { - this.startAnimation(); - } else { - clearTimeout(this.marqueeTimer); - this.setState({ animatedWidth: 0 }); + if (animationState === null && overflowWidth > 0) { + this.setState({ animationState: 'toLeft' }); } } - startAnimation = () => { - clearTimeout(this.marqueeTimer); - const isLeading = this.state.animatedWidth === 0; - const timeout = isLeading ? 0 : TIMEOUT; - - const animate = () => { - const { overflowWidth } = this.state; - let animatedWidth = this.state.animatedWidth; - let direction = this.state.direction; - - if (direction === 0) { - animatedWidth = this.state.animatedWidth + STEP; - } else { - animatedWidth = this.state.animatedWidth - STEP; - } - - const isRoundOver = animatedWidth < 0; - const endOfText = animatedWidth > overflowWidth; + onTransitionEnd = (payload) => { + const { + animationState + } = this.state; - if (endOfText) { - direction = direction === 1; - } - - if (isRoundOver) { - if (this.props.loop) { - direction = direction === 0; - } else { - return; - } - } + if (animationState === 'toLeft') { + this.setState({ animationState: 'toRight' }); + } - this.setState({ animatedWidth, direction }); - this.marqueeTimer = setTimeout(animate, TIMEOUT); - }; + if (animationState === 'toRight') { + this.setState({ animationState: null }); + } + } - this.marqueeTimer = setTimeout(animate, timeout); + onContainerMeasure = ({ width }) => { + this.setState({ containerWidth: width }); } - measureText = () => { - const container = this.container; - const node = this.text; + // + // Render - if (container && node) { - const containerWidth = container.offsetWidth; - const textWidth = node.offsetWidth; - const overflowWidth = textWidth - containerWidth; + render() { + const { + text + } = this.props; - if (overflowWidth !== this.state.overflowWidth) { - this.setState({ overflowWidth }); - } - } - } + const { + key, + overflowWidth, + animationState + } = this.state; + + const moveDist = -overflowWidth - 10; + const duration = -moveDist / SPEED; - render() { const style = { - position: 'relative', - right: this.state.animatedWidth, - whiteSpace: 'nowrap' + '--duration': `${duration}s`, + '--distance': `${moveDist}px` }; - if (this.state.overflowWidth < 0) { - return ( + return ( +
{ - this.container = el; - }} - className={`ui-marquee ${this.props.className}`} - style={{ overflow: 'hidden' }} + className={classNames( + styles.inner, + animationState === 'toLeft' && styles.toLeft + )} + style={style} + onTransitionEnd={this.onTransitionEnd} > { this.text = el; }} - style={style} - title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text} + title={text} > - {this.props.text} + {text}
- ); - } - - return ( -
{ - this.container = el; - }} - className={`ui-marquee ${this.props.className}`.trim()} - style={{ overflow: 'hidden' }} - onMouseEnter={this.onHandleMouseEnter} - onMouseLeave={this.onHandleMouseLeave} - > - { - this.text = el; - }} - style={style} - title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text} - > - {this.props.text} - -
+
); } } +Marquee.propTypes = { + text: PropTypes.string.isRequired +}; + export default Marquee;