Skip to content

Commit

Permalink
fix: make #progress-indicator run more smoothly on on_complete usage (
Browse files Browse the repository at this point in the history
#194)

* chore: move #progress-indicator to own story file

* WIP: use requestAnimationFrame to watch the onComplete animation usage

* WIP: make requestAnimationFrame work with onComplete animation usage

* fix: enhance #progress-indicator to run more smoothly on `on_complete` usage
  • Loading branch information
tujoworker authored Jun 4, 2019
1 parent 27c8a46 commit f506dd9
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ class Example extends PureComponent {
`}
</ComponentBox>
<ComponentBox
caption="ProgressIndicator with on_complete callback"
caption="ProgressIndicator with random `on_complete` callback"
noFragments={false}
>
{/* @jsx */ `
() => {
const random = (min, max) => (Math.floor( Math.random () * (max - min + 1)) + min)
const [visible, setVisibe] = useState(true)
useEffect(() => {
const timer = setInterval(() => setVisibe(!visible), 2400)
const timer = setInterval(() => setVisibe(!visible), random(2400, 4200))
return () => clearTimeout(timer)
})
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {
isTrue,
registerElement,
validateDOMAttributes,
dispatchCustomElementEvent
Expand Down Expand Up @@ -74,10 +75,9 @@ export default class ProgressIndicator extends PureComponent {

static getDerivedStateFromProps(props, state) {
if (state._listenForPropChanges) {
state.visible = Boolean(props.visible)
state.visible = isTrue(props.visible)
if (state.visible) {
state.complete = false
state.startTime = new Date().getTime()
}
if (parseFloat(props.progress) > -1) {
state.progress = props.progress
Expand All @@ -87,61 +87,37 @@ export default class ProgressIndicator extends PureComponent {
return state
}

constructor(props) {
super(props)

// this._id =
// props.id || `dnb-progress-indicator-${Math.round(Math.random() * 999)}` // cause we need an id anyway

this.state = {
_listenForPropChanges: true,
visible: Boolean(props.visible),
complete: false,
progress: props.progress
}

this.firstDelay = 300 // wait for the rest time + 200 start delay
state = {
_listenForPropChanges: true
}

componentWillUnmount() {
clearTimeout(this.completeTimeout)
clearTimeout(this.fadeOutTimeout)
}

callOnCompleteHandler() {
if (typeof this.props.on_complete === 'function') {
this.fadeOutTimeout = setTimeout(() => {
dispatchCustomElementEvent(this, 'on_complete')
}, 600) // wait for CSS fade out, defined in "progress-indicator-fade-out"
}
}

delayVisibility() {
if (this.state.complete) {
return
}

const duration = 1e3 // the duration, defined in CSS
const difference = new Date().getTime() - this.state.startTime
const ceil = Math.ceil(difference / duration) * duration
const timeToWait = ceil - difference

this.fadeOutTimeout = setTimeout(() => {
this.firstDelay = 0
callOnCompleteHandler = () => {
this.completeTimeout = setTimeout(() => {
this.setState({
complete: true
})
this.callOnCompleteHandler()
}, timeToWait + this.firstDelay)
if (typeof this.props.on_complete === 'function') {
this.fadeOutTimeout = setTimeout(() => {
dispatchCustomElementEvent(this, 'on_complete')
}, 600) // wait for CSS fade out, defined in "progress-indicator-fade-out"
}
}, 200)
}

render() {
const {
type,
size,
no_animation,
on_complete,
progress: _progress, //eslint-disable-line
visible: _visible, //eslint-disable-line
complete: _complete, //eslint-disable-line
...props
} = this.props

Expand All @@ -157,28 +133,24 @@ export default class ProgressIndicator extends PureComponent {

validateDOMAttributes(this.props, params)

const isComplete =
visible === false ||
(hasProgressIndicator && parseFloat(progress) >= 100)
if (isComplete) {
this.delayVisibility()
}

return (
<div
className={classnames(
'dnb-progress-indicator',
visible && 'dnb-progress-indicator--visible',
complete && 'dnb-progress-indicator--complete',
Boolean(no_animation) && 'dnb-progress-indicator--no-animation'
isTrue(no_animation) && 'dnb-progress-indicator--no-animation'
)}
{...params}
>
{type === 'circular' && (
<ProgressIndicatorCircular
size={size}
progress={progress}
visible={visible}
complete={complete}
onComplete={on_complete}
callOnCompleteHandler={this.callOnCompleteHandler}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,155 @@ import { validateDOMAttributes } from '../../shared/component-helper'

export const propTypes = {
size: PropTypes.string,
visible: PropTypes.bool,
complete: PropTypes.bool,
progress: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
maxOffset: PropTypes.number
maxOffset: PropTypes.number,
onComplete: PropTypes.func,
callOnCompleteHandler: PropTypes.func
}
export const defaultProps = {
size: null,
visible: true,
complete: false,
progress: null,
maxOffset: 88
maxOffset: 88,
onComplete: null,
callOnCompleteHandler: null
}

export default class ProgressIndicatorCircular extends PureComponent {
static propTypes = propTypes
static defaultProps = defaultProps
static getDerivedStateFromProps(props, state) {
state.progress = parseFloat(props.progress)
state.visible = props.visible
state.complete = props.complete
return state
}
constructor(props) {
super(props)
this.useAnimationFrame = typeof props.onComplete === 'function'
this._refDark = React.createRef()
this._refLight = React.createRef()
this.state = { animate: false }
}
componentDidMount() {
if (this.useAnimationFrame) {
this.startAnimationFirstTime()
}
}
componentWillUnmount() {
this.stopAnimation()
}
stopAnimation() {
this.setState({ animate: false })
if (this.startupTimeout) {
clearTimeout(this.startupTimeout)
}
}
startAnimationFirstTime() {
this.setState({ animate: false })
this.startupTimeout = setTimeout(() => this.startAnimation(), 300)
}
startAnimation() {
this.setState({ animate: true }, () => {
if (this._refDark.current) {
this.animate(
this._refDark.current,
true,
this.props.callOnCompleteHandler
)
}
if (this._refLight.current) {
this.animate(this._refLight.current, false)
}
})
}
animate(element, animateOnStart = true, callback = null) {
const min = 1
const max = 88
let start = 0,
ms = 0,
prog = max,
setProg = animateOnStart,
animate = true,
completeCalled = false,
stopNextRound = false

const step = timestamp => {
if (!start) {
start = timestamp
}

// milliseconds
ms = timestamp - start

if (animate) {
if (!this.state.visible && prog < 5) {
prog = min
}
if (setProg) {
element.style['stroke-dashoffset'] = prog
} else if (!animateOnStart) {
element.style['stroke-dashoffset'] = max
}
}

// if complete
if (stopNextRound) {
animate = false
if (!completeCalled) {
completeCalled = true
if (animateOnStart && typeof callback === 'function') {
callback()
}
} else if (this.state.visible && ms % 1e3 > 950) {
// this.startAnimationFirstTime() // will not start completely from scratch
stopNextRound = false
}
} else {
// make sure we stop next round
stopNextRound = !this.state.visible && prog === min
animate = true
completeCalled = false
}

// sice we have 1sec as duration, and we want always a max of 1000ms
prog = Math.round(max - (max / 1e3) * (ms % 1e3))

// calc if we want to animate
setProg = animateOnStart
? Math.ceil(ms / 1e3) % 2 === 1 || ms === 0
: Math.ceil(ms / 1e3) % 2 === 0 && ms !== 0

if (this.state.animate) {
window.requestAnimationFrame(step)
}
}
if (typeof window !== 'undefined' && window.requestAnimationFrame) {
window.requestAnimationFrame(step)
}
}
render() {
const { size, complete, maxOffset, progress } = this.props
const {
size,
maxOffset,
progress: _progress, // eslint-disable-line
visible, // eslint-disable-line
complete, // eslint-disable-line
onComplete, // eslint-disable-line
callOnCompleteHandler, // eslint-disable-line

...rest
} = this.props

const { progress } = this.state

const strokeDashoffset = maxOffset - (maxOffset / 100) * progress
const hasProgressIndicator = parseFloat(progress) > -1

const params = {}
const params = { ...rest }
if (hasProgressIndicator) {
params['title'] = `${progress}%`
params['aria-label'] = `${progress}%`
Expand Down Expand Up @@ -61,30 +190,36 @@ export default class ProgressIndicatorCircular extends PureComponent {
className={classnames(
'dnb-progress-indicator__circular__line',
'dark',
hasProgressIndicator || complete ? 'paused' : null
'dark',
hasProgressIndicator || this.useAnimationFrame
? 'paused'
: null
)}
style={hasProgressIndicator ? { strokeDashoffset } : {}}
ref={this._refDark}
/>
{!hasProgressIndicator && (
<Circle
className={classnames(
'dnb-progress-indicator__circular__line',
'light',
hasProgressIndicator || complete ? 'paused' : null
this.useAnimationFrame ? 'paused' : null
)}
ref={this._refLight}
/>
)}
</div>
)
}
}

const Circle = ({ className, ...rest }) => {
const Circle = React.forwardRef(({ className, ...rest }, ref) => {
return (
<svg
className={className}
viewBox="0 0 32 32"
shapeRendering="geometricPrecision"
ref={ref}
{...rest}
>
<circle
Expand All @@ -97,7 +232,7 @@ const Circle = ({ className, ...rest }) => {
/>
</svg>
)
}
})
Circle.propTypes = {
className: PropTypes.string.isRequired
}
Expand Down
Loading

0 comments on commit f506dd9

Please sign in to comment.