File size: 3,762 Bytes
92189dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Tooltip from '@/common/components/Tooltip';
import {ArrowPathIcon, CheckIcon, XMarkIcon} from '@heroicons/react/24/solid';
import {ChangeEvent, KeyboardEvent, useEffect, useMemo, useState} from 'react';
import {Button, Form, Input, Join} from 'react-daisyui';

type Props<T extends string | number> = Omit<
  React.InputHTMLAttributes<HTMLInputElement>,
  'size' | 'color' | 'onChange'
> & {
  label: string;
  defaultValue: T;
  initialValue: T;
  onChange: (value: string) => void;
};

function getStep(value: number) {
  const stringValue = String(value);
  const decimals = stringValue.split('.')[1];
  if (decimals != null) {
    // Not using 0.1 ** decimals.length because this will result in rounding
    // errors, e.g., 0.1 ** 2 => 0.010000000000000002.
    return 1 / 10 ** decimals.length;
  }
  return 1;
}

export default function ApprovableInput<T extends string | number>({
  label,
  defaultValue,
  initialValue,
  onChange,
  ...otherProps
}: Props<T>) {
  const [value, setValue] = useState<string>(`${initialValue}`);

  useEffect(() => {
    setValue(`${initialValue}`);
  }, [initialValue]);

  const step = useMemo(() => {
    return typeof defaultValue === 'number' && isFinite(defaultValue)
      ? getStep(defaultValue)
      : undefined;
  }, [defaultValue]);

  return (
    <div>
      <Form.Label className="flex-col items-start gap-2" title={label}>
        <Join className="w-full">
          <Input
            {...otherProps}
            className="w-full join-item"
            value={value}
            step={step}
            placeholder={`${defaultValue}`}
            onChange={(event: ChangeEvent<HTMLInputElement>) => {
              setValue(event.target.value);
            }}
            onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
              if (event.key === 'Enter') {
                event.preventDefault();
                onChange(value);
              }
            }}
          />
          <Tooltip message="Reset to default">
            <Button
              className="join-item"
              onClick={event => {
                event.preventDefault();
                setValue(`${defaultValue}`);
              }}>
              <ArrowPathIcon className="h-4 w-4" />
            </Button>
          </Tooltip>
          <Tooltip message="Revert change">
            <Button
              className="join-item"
              color="neutral"
              disabled={initialValue == value}
              onClick={event => {
                event.preventDefault();
                setValue(`${initialValue}`);
              }}>
              <XMarkIcon className="h-4 w-4" />
            </Button>
          </Tooltip>
          <Tooltip message="Apply change">
            <Button
              className="join-item"
              color="primary"
              disabled={initialValue == value}
              onClick={event => {
                event.preventDefault();
                onChange(value);
              }}>
              <CheckIcon className="h-4 w-4" />
            </Button>
          </Tooltip>
        </Join>
      </Form.Label>
    </div>
  );
}