Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / components / Form / Field / Custom / IncrementableInput.tsx
blob061d35292c36992f95e885a43627767914e4a73c
1 import type { ChangeEvent, FC, KeyboardEvent, KeyboardEventHandler } from 'react';
2 import { useEffect, useReducer } from 'react';
4 import { Button } from '@proton/atoms';
5 import { Icon, InputFieldTwo } from '@proton/components';
6 import clsx from '@proton/utils/clsx';
8 import './IncrementableInput.scss';
10 type State = {
11     value: number;
12     disabledDecrement?: boolean;
13     disabledIncrement?: boolean;
16 export enum Actions {
17     Increment = 'INCREMENT',
18     Decrement = 'DECREMENT',
19     SetValue = 'SET_VALUE',
22 type Action = { type: Actions.Increment } | { type: Actions.Decrement } | { type: Actions.SetValue; payload: number };
24 const getInitialState = ({ value }: State): State => ({ value, disabledDecrement: false, disabledIncrement: false });
26 const reducer =
27     (min: number, max: number) =>
28     (state: State, action: Action): State => {
29         switch (action.type) {
30             case Actions.Increment: {
31                 if (state.value >= max) return { ...state, disabledIncrement: true };
33                 const newValue = state.value + 1;
34                 return {
35                     ...state,
36                     value: newValue,
37                     disabledDecrement: false,
38                     disabledIncrement: newValue >= max,
39                 };
40             }
41             case Actions.Decrement: {
42                 if (state.value <= min) return { ...state, disabledDecrement: true };
44                 const newValue = state.value - 1;
45                 return {
46                     ...state,
47                     value: newValue,
48                     disabledIncrement: false,
49                     disabledDecrement: newValue <= min,
50                 };
51             }
52             case Actions.SetValue: {
53                 const { payload } = action;
54                 return {
55                     ...state,
56                     value: payload,
57                     disabledDecrement: payload <= min,
58                     disabledIncrement: payload >= max,
59                 };
60             }
61             default:
62                 return state;
63         }
64     };
66 type Props = {
67     className?: string;
68     disabled?: boolean;
69     max?: number;
70     min?: number;
71     value: number;
72     onChange: (value: number) => void;
73     onPressEnter?: KeyboardEventHandler<HTMLInputElement>;
76 export const IncrementableInput: FC<Props> = ({
77     className,
78     disabled,
79     max = Number.MAX_SAFE_INTEGER,
80     min = Number.MIN_SAFE_INTEGER,
81     value = 0,
82     onChange,
83     onPressEnter,
84 }) => {
85     const [state, dispatch] = useReducer(reducer(min, max), getInitialState({ value }));
87     const onDecrement = () => dispatch({ type: Actions.Decrement });
88     const onIncrement = () => dispatch({ type: Actions.Increment });
90     const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
91         if (event.key === 'Enter' && onPressEnter) onPressEnter(event);
92         if (event.key === 'ArrowDown') onDecrement();
93         if (event.key === 'ArrowUp') onIncrement();
94     };
96     const onChangeValue = ({ target: { value: newValue } }: ChangeEvent<HTMLInputElement>) => {
97         const maxReads = parseInt(newValue);
98         if (!maxReads || maxReads < min || maxReads > max) return;
100         dispatch({ type: Actions.SetValue, payload: maxReads });
101     };
103     useEffect(() => onChange(state.value), [state.value]);
105     return (
106         <div className={clsx(['flex flex-nowrap justify-center items-center incrementable-input relative', className])}>
107             <Button
108                 className="bg-weak -mr-8 z-1"
109                 onClick={onDecrement}
110                 color="weak"
111                 shape="solid"
112                 size="large"
113                 pill
114                 disabled={state.disabledDecrement}
115             >
116                 <Icon name="minus" alt="Decrement value" />
117             </Button>
118             <InputFieldTwo
119                 rootClassName="flex items-center bg-weak"
120                 inputClassName="py-0 text-center"
121                 value={state.value}
122                 onChange={onChangeValue}
123                 min={min}
124                 max={max}
125                 disabled={disabled}
126                 onKeyDown={onKeyDown}
127                 unstyled
128                 dense
129             />
130             <Button
131                 className="bg-weak z-1"
132                 onClick={onIncrement}
133                 color="weak"
134                 shape="solid"
135                 size="large"
136                 pill
137                 disabled={state.disabledIncrement}
138             >
139                 <Icon name="plus" alt="Increment value" />
140             </Button>
141         </div>
142     );