Skip to content
Snippets Groups Projects
AmountInput.tsx 4.28 KiB
// @ts-strict-ignore
import React, {
  type Ref,
  useRef,
  useState,
  useEffect,
  type FocusEventHandler,
} from 'react';

import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
import { amountToInteger, appendDecimals } from 'loot-core/src/shared/util';

import { useLocalPref } from '../../hooks/useLocalPref';
import { useMergedRefs } from '../../hooks/useMergedRefs';
import { SvgAdd, SvgSubtract } from '../../icons/v1';
import { type CSSProperties, theme } from '../../style';
import { Button } from '../common/Button';
import { InputWithContent } from '../common/InputWithContent';
import { View } from '../common/View';
import { useFormat } from '../spreadsheet/useFormat';

type AmountInputProps = {
  id?: string;
  inputRef?: Ref<HTMLInputElement>;
  value: number;
  zeroSign?: '-' | '+';
  onChangeValue?: (value: string) => void;
  onFocus?: FocusEventHandler<HTMLInputElement>;
  onBlur?: FocusEventHandler<HTMLInputElement>;
  onUpdate?: (amount: number) => void;
  style?: CSSProperties;
  textStyle?: CSSProperties;
  focused?: boolean;
  disabled?: boolean;
  autoDecimals?: boolean;
};

export function AmountInput({
  id,
  inputRef,
  value: initialValue,
  zeroSign = '-', // + or -
  onFocus,
  onBlur,
  onChangeValue,
  onUpdate,
  style,
  textStyle,
  focused,
  disabled = false,
  autoDecimals = false,
}: AmountInputProps) {
  const format = useFormat();
  const negative = (initialValue === 0 && zeroSign === '-') || initialValue < 0;

  const initialValueAbsolute = format(Math.abs(initialValue || 0), 'financial');
  const [value, setValue] = useState(initialValueAbsolute);
  useEffect(() => setValue(initialValueAbsolute), [initialValueAbsolute]);

  const buttonRef = useRef();
  const ref = useRef<HTMLInputElement>(null);
  const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
  const [hideFraction = false] = useLocalPref('hideFraction');

  useEffect(() => {
    if (focused) {
      ref.current?.focus();
    }
  }, [focused]);

  function onSwitch() {
    fireUpdate(!negative);
  }

  function getAmount(negate) {
    const valueOrInitial = Math.abs(amountToInteger(evalArithmetic(value)));
    return negate ? valueOrInitial * -1 : valueOrInitial;
  }

  function onInputTextChange(val) {
    val = autoDecimals ? appendDecimals(val, hideFraction) : val;
    setValue(val ? val : '');
    onChangeValue?.(val);
  }

  function fireUpdate(negate) {
    onUpdate?.(getAmount(negate));
  }

  function onInputAmountBlur(e) {
    if (!ref.current?.contains(e.relatedTarget)) {
      fireUpdate(negative);
    }
    onBlur?.(e);
  }

  return (
    <InputWithContent
      id={id}
      inputRef={mergedRef}
      inputMode="decimal"
      leftContent={
        <Button
          type="bare"
          disabled={disabled}
          aria-label={`Make ${negative ? 'positive' : 'negative'}`}
          style={{ padding: '0 7px' }}
          onPointerUp={onSwitch}
          onPointerDown={e => e.preventDefault()}
          ref={buttonRef}
        >
          {negative ? (
            <SvgSubtract style={{ width: 8, height: 8, color: 'inherit' }} />
          ) : (
            <SvgAdd style={{ width: 8, height: 8, color: 'inherit' }} />
          )}
        </Button>
      }
      value={value}
      disabled={disabled}
      focused={focused}
      style={{ flex: 1, alignItems: 'stretch', ...style }}
      inputStyle={{ paddingLeft: 0, ...textStyle }}
      onKeyUp={e => {
        if (e.key === 'Enter') {
          fireUpdate(negative);
        }
      }}
      onChangeValue={onInputTextChange}
      onBlur={onInputAmountBlur}
      onFocus={onFocus}
    />
  );
}

export function BetweenAmountInput({ defaultValue, onChange }) {
  const [num1, setNum1] = useState(defaultValue.num1);
  const [num2, setNum2] = useState(defaultValue.num2);

  return (
    <View style={{ flexDirection: 'row', alignItems: 'center' }}>
      <AmountInput
        value={num1}
        onUpdate={value => {
          setNum1(value);
          onChange({ num1: value, num2 });
        }}
        style={{ color: theme.formInputText }}
      />
      <View style={{ margin: '0 5px' }}>and</View>
      <AmountInput
        value={num2}
        onUpdate={value => {
          setNum2(value);
          onChange({ num1, num2: value });
        }}
        style={{ color: theme.formInputText }}
      />
    </View>
  );
}