Skip to content

Instantly share code, notes, and snippets.

@joshcox
Created June 23, 2021 10:50
Show Gist options
  • Save joshcox/84703eb96e9c5edaed80c33b5b6cf84d to your computer and use it in GitHub Desktop.
Save joshcox/84703eb96e9c5edaed80c33b5b6cf84d to your computer and use it in GitHub Desktop.
Some Scaling Mt. ActivityPlan Musings
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