Refactor DurationInput to be more predictable
Using two inputs instead of one with finnicky magic for a better user experiences
This commit is contained in:
parent
81abb93ed6
commit
7674e5d792
2 changed files with 134 additions and 48 deletions
|
|
@ -0,0 +1,35 @@
|
||||||
|
.Duration {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--input-bound-bg);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: $input-corner;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 calc($unit-2x - 2px);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--input-bound-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border: 2px solid $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
width: initial;
|
||||||
|
height: 100%;
|
||||||
|
padding: calc($unit-2x - 2px) 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useState, ChangeEvent, KeyboardEvent, useEffect } from 'react'
|
import React, { useState, ChangeEvent, KeyboardEvent } from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import Input from '~components/Input'
|
import Input from '~components/Input'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
|
|
@ -15,50 +14,57 @@ interface Props
|
||||||
}
|
}
|
||||||
|
|
||||||
const DurationInput = React.forwardRef<HTMLInputElement, Props>(
|
const DurationInput = React.forwardRef<HTMLInputElement, Props>(
|
||||||
function DurationInput(
|
function DurationInput({ className, value, onValueChange }, forwardedRef) {
|
||||||
{ className, placeholder, value, onValueChange },
|
// State
|
||||||
forwardedRef
|
|
||||||
) {
|
|
||||||
const [duration, setDuration] = useState('')
|
const [duration, setDuration] = useState('')
|
||||||
|
const [minutesSelected, setMinutesSelected] = useState(false)
|
||||||
|
const [secondsSelected, setSecondsSelected] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
// Refs
|
||||||
if (value > 0) setDuration(convertSecondsToString(value))
|
const minutesRef = React.createRef<HTMLInputElement>()
|
||||||
}, [value])
|
const secondsRef = React.createRef<HTMLInputElement>()
|
||||||
|
|
||||||
function convertStringToSeconds(string: string) {
|
// Event handlers: On value change
|
||||||
const parts = string.split(':')
|
function handleMinutesChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
const minutes = parseInt(parts[0])
|
const minutes = parseInt(event.currentTarget.value)
|
||||||
const seconds = parseInt(parts[1])
|
const seconds = secondsRef.current
|
||||||
|
? parseInt(secondsRef.current.value)
|
||||||
|
: 0
|
||||||
|
|
||||||
return minutes * 60 + seconds
|
handleChange(minutes, seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertSecondsToString(value: number) {
|
function handleSecondsChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
const minutes = Math.floor(value / 60)
|
const seconds = parseInt(event.currentTarget.value)
|
||||||
const seconds = value - minutes * 60
|
const minutes = minutesRef.current
|
||||||
|
? parseInt(minutesRef.current.value)
|
||||||
|
: 0
|
||||||
|
|
||||||
const paddedMinutes = padNumber(`${minutes}`, '0', 2)
|
handleChange(minutes, seconds)
|
||||||
|
|
||||||
return `${paddedMinutes}:${seconds}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function padNumber(string: string, pad: string, length: number) {
|
function handleChange(minutes: number, seconds: number) {
|
||||||
return (new Array(length + 1).join(pad) + string).slice(-length)
|
onValueChange(minutes * 60 + seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
// Event handler: Key presses
|
||||||
const value = event.currentTarget.value
|
function handleKeyUp(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
const durationInSeconds = convertStringToSeconds(value)
|
const input = event.currentTarget
|
||||||
onValueChange(durationInSeconds)
|
|
||||||
|
if (input.selectionStart === 0 && input.selectionEnd === 2) {
|
||||||
|
if (input === minutesRef.current) {
|
||||||
|
setMinutesSelected(true)
|
||||||
|
} else if (input === secondsRef.current) {
|
||||||
|
setSecondsSelected(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||||
// Allow the key to be processed normally
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current value
|
|
||||||
const input = event.currentTarget
|
const input = event.currentTarget
|
||||||
let value = event.currentTarget.value
|
let value = event.currentTarget.value
|
||||||
|
|
||||||
|
|
@ -95,16 +101,28 @@ const DurationInput = React.forwardRef<HTMLInputElement, Props>(
|
||||||
const isNumber = !isNaN(char)
|
const isNumber = !isNaN(char)
|
||||||
|
|
||||||
// Check if the character should be accepted or rejected
|
// Check if the character should be accepted or rejected
|
||||||
if (!isNumber || value.length >= 5) {
|
if (!isNumber || value.length >= 2) {
|
||||||
// Reject the character
|
// Reject the character if the user doesn't have the entire string selected
|
||||||
event.preventDefault()
|
if (!minutesSelected && input === minutesRef.current)
|
||||||
} else if (value.length === 2) {
|
event.preventDefault()
|
||||||
// Insert a colon after the second digit
|
else if (
|
||||||
input.value = value + ':'
|
!secondsSelected &&
|
||||||
|
input === secondsRef.current &&
|
||||||
|
getSeconds() > 9
|
||||||
|
)
|
||||||
|
event.preventDefault()
|
||||||
|
else {
|
||||||
|
setDuration(value)
|
||||||
|
setMinutesSelected(false)
|
||||||
|
setSecondsSelected(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setDuration(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Time manipulation
|
||||||
function incrementTime(time: string): string {
|
function incrementTime(time: string): string {
|
||||||
// Split the time into minutes and seconds
|
// Split the time into minutes and seconds
|
||||||
let [minutes, seconds] = time.split(':').map(Number)
|
let [minutes, seconds] = time.split(':').map(Number)
|
||||||
|
|
@ -144,21 +162,54 @@ const DurationInput = React.forwardRef<HTMLInputElement, Props>(
|
||||||
return `${minutes}:${seconds}`
|
return `${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Miscellaneous
|
||||||
|
|
||||||
|
function getMinutes() {
|
||||||
|
const minutes = Math.floor(value / 60)
|
||||||
|
return minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeconds() {
|
||||||
|
const seconds = value % 60
|
||||||
|
return seconds
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<div className={classNames(className, { Duration: true })}>
|
||||||
type="text"
|
<Input
|
||||||
className={classNames(
|
ref={minutesRef}
|
||||||
{
|
type="text"
|
||||||
Duration: true,
|
className={classNames(
|
||||||
AlignRight: true,
|
{
|
||||||
},
|
AlignRight: true,
|
||||||
className
|
},
|
||||||
)}
|
className
|
||||||
value={duration}
|
)}
|
||||||
onChange={handleChange}
|
value={getMinutes()}
|
||||||
onKeyDown={handleKeyDown}
|
onChange={handleMinutesChange}
|
||||||
placeholder={placeholder}
|
onKeyUp={handleKeyUp}
|
||||||
/>
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="mm"
|
||||||
|
size={3}
|
||||||
|
/>
|
||||||
|
<span>:</span>
|
||||||
|
<Input
|
||||||
|
ref={secondsRef}
|
||||||
|
type="text"
|
||||||
|
className={classNames(
|
||||||
|
{
|
||||||
|
AlignRight: true,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
value={`${getSeconds()}`.padStart(2, '0')}
|
||||||
|
onChange={handleSecondsChange}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="ss"
|
||||||
|
size={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue