Dangerous Actions Require Safe Buttons
A user interface must not allow an unintentional triggering of a dangerous action. A dangerous action is one that cannot be undone quickly or ever e.g. deleting a GitHub repo
My app, Infogredient, has a dangerous "clear recipe" action for which I built a press 'n hold safety button (github, npm).
You can also play with the live demo.
Building a Safety Button with React Native
The markup is a TouchableWithoutFeedback
wrapper over a style-able View
which contains whatever children are passed in along with an absolute
ly positioned View
that provides the visual feedback of the button filling up.
<TouchableWithoutFeedback onPressIn={startAction} onPressOut={stopAction}>
<View style={[style]}>
{children}
<View
style={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
width: `${progress}%`,
backgroundColor: progressColor,
}}
></View>
</View>
</TouchableWithoutFeedback>
The heavy-lifting comes from the Animated
feature of react-native
. I use progress
, a plain React state hook, to drive visual changes in the component. Using progress
keeps a clean separation between Animated
constructs and react-native
components. Animations drive changes in Animated.Value
s. fillAnimationValue
goes from 0 to 100 which drives the width of the fixed view from 0% to 100%. When a finger presses in on the component, the emptyAnimation
is stopped and the fillAnimation
is started. When the fillAnimation
completes it fires the passed in action onPressAndHold
and resets the fillAnimationValue
. When a finger leaves the component, the fillAnimation
is stopped and the emptyAnimation
is started.
const [progress, updateProgress] = useState(0)
const [fillAnimationValue] = useState(new Animated.Value(0)) //
fillAnimationValue.addListener(({ value }) => updateProgress(value))
const fillAnimation = Animated.timing(fillAnimationValue, {
toValue: 100,
duration: activationThreshold,
})
const emptyAnimation = Animated.timing(fillAnimationValue, {
toValue: 0,
duration: deactivationTime,
})
function startAction() {
emptyAnimation.stop()
fillAnimation.start(({ finished }) => {
if (finished) {
onPressAndHold()
console.log('button action fired')
fillAnimationValue.setValue(0)
} else {
emptyAnimation.start()
}
})
}
function stopAction() {
fillAnimation.stop()
emptyAnimation.start()
}
Feel free to leave a comment/question/pull request on the repo