Created
June 23, 2021 10:50
-
-
Save joshcox/84703eb96e9c5edaed80c33b5b6cf84d to your computer and use it in GitHub Desktop.
Some Scaling Mt. ActivityPlan Musings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { makeVar } from '@apollo/client'; | |
import { Chip, FormControlLabel, InputBase } from '@material-ui/core'; | |
import React, { useCallback, useEffect, useRef, useState } from 'react'; | |
import { snackbarState } from 'state/snackbarState'; | |
import styled from 'styled-components'; | |
import { onEnterPress } from 'utils/helpers'; | |
import { millisToSeconds, secondsToMillis } from 'utils/time'; | |
const disableOtherChips = makeVar<boolean>(false); | |
enum ChipLabels { | |
reps = 'reps', | |
holdTime = 'sec hold', | |
pointsPerRep = 'pts per rep' | |
} | |
interface Props { | |
value: number; | |
name: string; | |
handleUpdateChipValues: (value: number, name: string) => boolean; | |
editable?: boolean; | |
} | |
const ChipWrapper = styled.div` | |
display: flex; | |
align-items: center; | |
justify-content: flex-end; | |
margin-right: ${({ theme }) => theme.spacing(5)}; | |
&:last-of-type { | |
margin-right: ${({ theme }) => theme.spacing(7.5)}; | |
} | |
`; | |
const Label = styled(FormControlLabel)` | |
margin-right: 0; | |
padding: ${({ theme }) => theme.spacing(2)}; | |
height: 22px; | |
`; | |
const Input = styled(props => <InputBase {...props} />)` | |
width: 20px; | |
margin-right: ${({ theme }) => theme.spacing(0.5)}; | |
input { | |
text-align: right; | |
} | |
input::-webkit-outer-spin-button, | |
input::-webkit-inner-spin-button { | |
-webkit-appearance: none; | |
margin: 0; | |
} | |
input::selection { | |
background-color: ${({ theme, error }) => | |
error && theme.palette.error.main + '1F'}; | |
} | |
`; | |
interface MyInputProps { | |
label: string; | |
inputValue: any; | |
error: boolean; | |
onFocus: (event: React.FocusEvent<HTMLElement>) => void; | |
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |
onEnter: (event: React.KeyboardEvent<HTMLInputElement>) => void; | |
} | |
const MyInput = ({ label, inputValue, onChange, onEnter, onFocus, error }: MyInputProps): React.ElementType => React.forwardRef(({ children, className, ref }) => ( | |
<div className={className}> | |
<Input | |
aria-label={`${label} input`} | |
value={inputValue} | |
name={label} | |
type="number" | |
autoFocus | |
onFocus={onFocus} | |
onChange={onChange} | |
onKeyDown={onEnter} | |
inputProps={{ maxLength: '2' }} | |
data-testid={`${label}Input`} | |
error={error} | |
inputRef={ref} | |
/> | |
{children} | |
</div> | |
)); | |
const MyValue = (value: string | number): React.ElementType => ({ children, className }) => ( | |
<div className={className}><span>{value}</span>{children}</div> | |
); | |
export default function EditableChip({ | |
value, | |
name, | |
handleUpdateChipValues, | |
editable | |
}: Props): JSX.Element { | |
const [isEditing, setIsEditing] = useState<boolean>(false); | |
const [localValue, setLocalValue] = useState<number>(value); | |
const [error, setError] = useState<boolean>(false); | |
const valueInSeconds = millisToSeconds(localValue); | |
const label = ChipLabels[name as keyof typeof ChipLabels]; | |
const inputValue = name === 'holdTime' ? valueInSeconds : localValue; | |
const chipText = `${inputValue} ${label}`; | |
useEffect(() => { | |
setLocalValue(value); | |
}, [value]); | |
const handleSetIsEditing = useCallback((): void => { | |
if (disableOtherChips()) { | |
return; | |
} | |
if (editable) { | |
setIsEditing(true); | |
} | |
}, [editable, setIsEditing]); | |
const handleSelectInputValue = ( | |
event: React.ChangeEvent<HTMLElement> | |
): void => { | |
const element = event.currentTarget as HTMLInputElement; | |
element.select(); | |
}; | |
const setErrorState = useCallback( | |
(message: string): void => { | |
handleSetIsEditing(); | |
setError(true); | |
disableOtherChips(true); | |
snackbarState({ | |
type: 'generic', | |
severity: 'error', | |
message | |
}); | |
}, | |
[handleSetIsEditing, setError] | |
); | |
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => { | |
disableOtherChips(false); | |
setError(false); | |
const val = event.target.value; | |
switch (name) { | |
case 'reps': | |
case 'pointsPerRep': | |
setLocalValue(Number(val)); | |
break; | |
case 'holdTime': | |
const valueInMillis = secondsToMillis(Number(val)); | |
setLocalValue(valueInMillis); | |
} | |
}; | |
const handleUpdateChipOnEnter = ( | |
event: React.KeyboardEvent<HTMLInputElement> | |
): void => { | |
if (error) { | |
return; | |
} | |
onEnterPress(event, () => { | |
setIsEditing(false); | |
const holdTimeValid = handleUpdateChipValues(Number(localValue), name); | |
if (!holdTimeValid) { | |
setErrorState('Please enter a number greater than 1 for hold time'); | |
} | |
}); | |
}; | |
const handleUpdateChipOnClick = useCallback(() => { | |
if (error) { | |
return; | |
} | |
setIsEditing(false); | |
const holdTimeValid = handleUpdateChipValues(Number(localValue), name); | |
if (!holdTimeValid) { | |
setErrorState('Please enter a number greater than 1 for hold time'); | |
} | |
}, [ | |
name, | |
localValue, | |
setIsEditing, | |
handleUpdateChipValues, | |
error, | |
setErrorState | |
]); | |
useEffect(() => { | |
if (isEditing) { | |
document.addEventListener('mousedown', handleUpdateChipOnClick); | |
} | |
return () => | |
document.removeEventListener('mousedown', handleUpdateChipOnClick); | |
}, [handleUpdateChipOnClick, isEditing]); | |
useEffect(() => { | |
if (localValue === 0) { | |
setErrorState(`Please enter a number greater than 0 for ${name}`); | |
} | |
}, [localValue, setErrorState, name]); | |
return ( | |
<ChipWrapper> | |
<Chip | |
clickable={editable} | |
label={label} | |
size="small" | |
variant="outlined" | |
onClick={handleSetIsEditing} | |
data-testid={`${name}Chip`} | |
aria-label={chipText} | |
component={ | |
isEditing ? MyInput({ | |
label, | |
inputValue, | |
onFocus: handleSelectInputValue, | |
onChange: handleChange, | |
onEnter: handleUpdateChipOnEnter, | |
error | |
}) : MyValue(inputValue) | |
} | |
/> | |
</ChipWrapper> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment