From 851fa8c7f5d88185761b54a30983851d146e80c9 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Fri, 28 Apr 2023 23:13:37 +0100
Subject: [PATCH] refactor(typescript): move some common components to TS
 (#962)

---
 .../src/components/AnimatedRefresh.js         |   2 +-
 .../desktop-client/src/components/Stack.js    |   4 +-
 .../desktop-client/src/components/Text.js     |  17 --
 .../accounts/MobileAccountDetails.js          |   2 +-
 .../desktop-client/src/components/common.js   | 154 +-----------------
 .../src/components/common/Button.tsx          | 113 +++++++++++++
 .../src/components/common/Input.tsx           |  71 ++++++++
 .../src/components/common/Text.tsx            |  24 +++
 .../components/{View.js => common/View.tsx}   |  14 +-
 .../src/components/{forms.js => forms.tsx}    |  40 ++++-
 .../src/components/settings/{UI.js => UI.tsx} |  21 ++-
 .../src/components/spreadsheet/CellValue.js   |   2 +-
 upcoming-release-notes/962.md                 |   6 +
 13 files changed, 290 insertions(+), 180 deletions(-)
 delete mode 100644 packages/desktop-client/src/components/Text.js
 create mode 100644 packages/desktop-client/src/components/common/Button.tsx
 create mode 100644 packages/desktop-client/src/components/common/Input.tsx
 create mode 100644 packages/desktop-client/src/components/common/Text.tsx
 rename packages/desktop-client/src/components/{View.js => common/View.tsx} (54%)
 rename packages/desktop-client/src/components/{forms.js => forms.tsx} (74%)
 rename packages/desktop-client/src/components/settings/{UI.js => UI.tsx} (81%)
 create mode 100644 upcoming-release-notes/962.md

diff --git a/packages/desktop-client/src/components/AnimatedRefresh.js b/packages/desktop-client/src/components/AnimatedRefresh.js
index 28f48446b..b4c713032 100644
--- a/packages/desktop-client/src/components/AnimatedRefresh.js
+++ b/packages/desktop-client/src/components/AnimatedRefresh.js
@@ -4,7 +4,7 @@ import { css } from 'glamor';
 
 import Refresh from '../icons/v1/Refresh';
 
-import View from './View';
+import View from './common/View';
 
 let spin = css.keyframes({
   '0%': { transform: 'rotateZ(0deg)' },
diff --git a/packages/desktop-client/src/components/Stack.js b/packages/desktop-client/src/components/Stack.js
index d984668bc..717e7bc03 100644
--- a/packages/desktop-client/src/components/Stack.js
+++ b/packages/desktop-client/src/components/Stack.js
@@ -1,7 +1,7 @@
 import React from 'react';
 
-import Text from './Text';
-import View from './View';
+import Text from './common/Text';
+import View from './common/View';
 
 function getChildren(key, children) {
   return React.Children.toArray(children).reduce((list, child) => {
diff --git a/packages/desktop-client/src/components/Text.js b/packages/desktop-client/src/components/Text.js
deleted file mode 100644
index 7a6fae6dd..000000000
--- a/packages/desktop-client/src/components/Text.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import { css } from 'glamor';
-
-function Text(props) {
-  // Pull `numberOfLines` off since it's only used in React Native
-  const { numberOfLines, style, innerRef, ...restProps } = props;
-  return (
-    <span
-      {...restProps}
-      ref={innerRef}
-      className={`${props.className || ''} ${css(props.style)}`}
-    />
-  );
-}
-
-export default Text;
diff --git a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
index 7c83a80ca..49c99b0c7 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
+++ b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js
@@ -6,8 +6,8 @@ import CheveronLeft from '../../icons/v1/CheveronLeft';
 import SearchAlternate from '../../icons/v2/SearchAlternate';
 import { colors, styles } from '../../style';
 import { Button, InputWithContent, Label, View } from '../common';
+import Text from '../common/Text';
 import CellValue from '../spreadsheet/CellValue';
-import Text from '../Text';
 
 import { TransactionList } from './MobileTransaction';
 
diff --git a/packages/desktop-client/src/components/common.js b/packages/desktop-client/src/components/common.js
index f3ffc1c38..bcb4346ac 100644
--- a/packages/desktop-client/src/components/common.js
+++ b/packages/desktop-client/src/components/common.js
@@ -5,7 +5,6 @@ import React, {
   useState,
   useCallback,
 } from 'react';
-import mergeRefs from 'react-merge-refs';
 import ReactModal from 'react-modal';
 import { Route, NavLink, withRouter, useRouteMatch } from 'react-router-dom';
 
@@ -21,18 +20,21 @@ import hotkeys from 'hotkeys-js';
 
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
-import { useProperFocus } from '../hooks/useProperFocus';
 import Loading from '../icons/AnimatedLoading';
 import Delete from '../icons/v0/Delete';
 import ExpandArrow from '../icons/v0/ExpandArrow';
 import { styles, colors } from '../style';
 import tokens from '../tokens';
 
-import Text from './Text';
-import View from './View';
+import Button from './common/Button';
+import Input, { defaultInputStyle } from './common/Input';
+import Text from './common/Text';
+import View from './common/View';
 
-export { default as View } from './View';
-export { default as Text } from './Text';
+export { default as Button } from './common/Button';
+export { default as Input } from './common/Input';
+export { default as View } from './common/View';
+export { default as Text } from './common/Text';
 export { default as Stack } from './Stack';
 
 export function TextOneLine({ children, centered, ...props }) {
@@ -187,93 +189,6 @@ function ButtonLink_({
 
 export const ButtonLink = withRouter(ButtonLink_);
 
-export const Button = React.forwardRef(
-  (
-    {
-      children,
-      pressed,
-      primary,
-      hover,
-      bare,
-      style,
-      disabled,
-      hoveredStyle,
-      activeStyle,
-      bounce = true,
-      as = 'button',
-      ...nativeProps
-    },
-    ref,
-  ) => {
-    hoveredStyle = [
-      bare
-        ? { backgroundColor: 'rgba(100, 100, 100, .15)' }
-        : { ...styles.shadow },
-      hoveredStyle,
-    ];
-    activeStyle = [
-      bare
-        ? { backgroundColor: 'rgba(100, 100, 100, .25)' }
-        : {
-            transform: bounce && 'translateY(1px)',
-            boxShadow:
-              !bare &&
-              (primary
-                ? '0 1px 4px 0 rgba(0,0,0,0.3)'
-                : '0 1px 4px 0 rgba(0,0,0,0.2)'),
-            transition: 'none',
-          },
-      activeStyle,
-    ];
-
-    let Component = as;
-    let buttonStyle = [
-      {
-        alignItems: 'center',
-        justifyContent: 'center',
-        flexShrink: 0,
-        padding: bare ? '5px' : '5px 10px',
-        margin: 0,
-        overflow: 'hidden',
-        display: 'flex',
-        borderRadius: 4,
-        backgroundColor: bare
-          ? 'transparent'
-          : primary
-          ? disabled
-            ? colors.n7
-            : colors.p5
-          : 'white',
-        border: bare
-          ? 'none'
-          : '1px solid ' +
-            (primary ? (disabled ? colors.n7 : colors.p5) : colors.n9),
-        color: primary ? 'white' : disabled ? colors.n6 : colors.n1,
-        transition: 'box-shadow .25s',
-        ...styles.smallText,
-      },
-      { ':hover': !disabled && hoveredStyle },
-      { ':active': !disabled && activeStyle },
-      hover && hoveredStyle,
-      pressed && activeStyle,
-      style,
-    ];
-
-    return (
-      <Component
-        ref={ref}
-        {...(typeof as === 'string'
-          ? css(buttonStyle)
-          : { style: buttonStyle })}
-        disabled={disabled}
-        {...nativeProps}
-      >
-        {children}
-      </Component>
-    );
-  },
-);
-
 export const ButtonWithLoading = React.forwardRef((props, ref) => {
   let { loading, children, ...buttonProps } = props;
   return (
@@ -312,59 +227,6 @@ export const ButtonWithLoading = React.forwardRef((props, ref) => {
   );
 });
 
-const defaultInputStyle = {
-  outline: 0,
-  backgroundColor: 'white',
-  margin: 0,
-  padding: 5,
-  borderRadius: 4,
-  border: '1px solid #d0d0d0',
-};
-
-export function Input({
-  style,
-  inputRef,
-  onEnter,
-  onUpdate,
-  focused,
-  ...nativeProps
-}) {
-  let ref = useRef();
-  useProperFocus(ref, focused);
-
-  return (
-    <input
-      ref={inputRef ? mergeRefs([inputRef, ref]) : ref}
-      {...css(
-        defaultInputStyle,
-        {
-          ':focus': {
-            border: '1px solid ' + colors.b5,
-            boxShadow: '0 1px 1px ' + colors.b7,
-          },
-          '::placeholder': { color: colors.n7 },
-        },
-        styles.smallText,
-        style,
-      )}
-      {...nativeProps}
-      onKeyDown={e => {
-        if (e.key === 'Enter' && onEnter) {
-          onEnter(e);
-        }
-
-        nativeProps.onKeyDown && nativeProps.onKeyDown(e);
-      }}
-      onChange={e => {
-        if (onUpdate) {
-          onUpdate(e.target.value);
-        }
-        nativeProps.onChange && nativeProps.onChange(e);
-      }}
-    />
-  );
-}
-
 export function InputWithContent({
   leftContent,
   rightContent,
diff --git a/packages/desktop-client/src/components/common/Button.tsx b/packages/desktop-client/src/components/common/Button.tsx
new file mode 100644
index 000000000..a13fe8b23
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Button.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+
+import { css } from 'glamor';
+import type { CSSProperties } from 'glamor';
+
+import { styles, colors } from '../../style';
+
+interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {
+  pressed?: boolean;
+  primary?: boolean;
+  hover?: boolean;
+  bare?: boolean;
+  disabled?: boolean;
+  hoveredStyle?: CSSProperties;
+  activeStyle?: CSSProperties;
+  bounce?: boolean;
+  as?: 'button';
+  children: React.ReactNode;
+  onClick?: () => void;
+}
+
+const Button: React.FC<ButtonProps> = React.forwardRef<
+  HTMLButtonElement,
+  ButtonProps
+>(
+  (
+    {
+      children,
+      pressed,
+      primary,
+      hover,
+      bare,
+      style,
+      disabled,
+      hoveredStyle,
+      activeStyle,
+      bounce = true,
+      as = 'button',
+      ...nativeProps
+    },
+    ref,
+  ) => {
+    hoveredStyle = [
+      bare
+        ? { backgroundColor: 'rgba(100, 100, 100, .15)' }
+        : { ...styles.shadow },
+      hoveredStyle,
+    ];
+    activeStyle = [
+      bare
+        ? { backgroundColor: 'rgba(100, 100, 100, .25)' }
+        : {
+            transform: bounce && 'translateY(1px)',
+            boxShadow:
+              !bare &&
+              (primary
+                ? '0 1px 4px 0 rgba(0,0,0,0.3)'
+                : '0 1px 4px 0 rgba(0,0,0,0.2)'),
+            transition: 'none',
+          },
+      activeStyle,
+    ];
+
+    let Component = as;
+    let buttonStyle = [
+      {
+        alignItems: 'center',
+        justifyContent: 'center',
+        flexShrink: 0,
+        padding: bare ? '5px' : '5px 10px',
+        margin: 0,
+        overflow: 'hidden',
+        display: 'flex',
+        borderRadius: 4,
+        backgroundColor: bare
+          ? 'transparent'
+          : primary
+          ? disabled
+            ? colors.n7
+            : colors.p5
+          : 'white',
+        border: bare
+          ? 'none'
+          : '1px solid ' +
+            (primary ? (disabled ? colors.n7 : colors.p5) : colors.n9),
+        color: primary ? 'white' : disabled ? colors.n6 : colors.n1,
+        transition: 'box-shadow .25s',
+        ...styles.smallText,
+      },
+      { ':hover': !disabled && hoveredStyle },
+      { ':active': !disabled && activeStyle },
+      hover && hoveredStyle,
+      pressed && activeStyle,
+      style,
+    ];
+
+    return (
+      <Component
+        ref={ref}
+        {...(typeof as === 'string'
+          ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            (css(buttonStyle) as any)
+          : { style: buttonStyle })}
+        disabled={disabled}
+        {...nativeProps}
+      >
+        {children}
+      </Component>
+    );
+  },
+);
+
+export default Button;
diff --git a/packages/desktop-client/src/components/common/Input.tsx b/packages/desktop-client/src/components/common/Input.tsx
new file mode 100644
index 000000000..7cfef4dc5
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Input.tsx
@@ -0,0 +1,71 @@
+import React, { useRef } from 'react';
+import mergeRefs from 'react-merge-refs';
+
+import { css } from 'glamor';
+import type { CSSProperties } from 'glamor';
+
+import { useProperFocus } from '../../hooks/useProperFocus';
+import { styles, colors } from '../../style';
+
+export const defaultInputStyle = {
+  outline: 0,
+  backgroundColor: 'white',
+  margin: 0,
+  padding: 5,
+  borderRadius: 4,
+  border: '1px solid #d0d0d0',
+};
+
+interface InputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'style'> {
+  style?: CSSProperties;
+  inputRef?: React.Ref<HTMLInputElement>;
+  onEnter?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
+  onUpdate?: (newValue: string) => void;
+  focused?: boolean;
+}
+
+const Input: React.FC<InputProps> = ({
+  style,
+  inputRef,
+  onEnter,
+  onUpdate,
+  focused,
+  ...nativeProps
+}) => {
+  let ref = useRef();
+  useProperFocus(ref, focused);
+
+  return (
+    <input
+      ref={inputRef ? mergeRefs([inputRef, ref]) : ref}
+      {...css(
+        defaultInputStyle,
+        {
+          ':focus': {
+            border: '1px solid ' + colors.b5,
+            boxShadow: '0 1px 1px ' + colors.b7,
+          },
+          '::placeholder': { color: colors.n7 },
+        },
+        styles.smallText,
+        style,
+      )}
+      {...nativeProps}
+      onKeyDown={e => {
+        if (e.key === 'Enter' && onEnter) {
+          onEnter(e);
+        }
+
+        nativeProps.onKeyDown && nativeProps.onKeyDown(e);
+      }}
+      onChange={e => {
+        if (onUpdate) {
+          onUpdate(e.target.value);
+        }
+        nativeProps.onChange && nativeProps.onChange(e);
+      }}
+    />
+  );
+};
+
+export default Input;
diff --git a/packages/desktop-client/src/components/common/Text.tsx b/packages/desktop-client/src/components/common/Text.tsx
new file mode 100644
index 000000000..7ec99bbbb
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Text.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { css } from 'glamor';
+import type { CSSProperties } from 'glamor';
+
+interface TextProps extends Omit<React.HTMLProps<HTMLSpanElement>, 'style'> {
+  style?: CSSProperties;
+  innerRef?: React.Ref<HTMLSpanElement>;
+  className?: string;
+  children?: React.ReactNode;
+}
+
+export const Text: React.FC<TextProps> = props => {
+  const { style, innerRef, ...restProps } = props;
+  return (
+    <span
+      {...restProps}
+      ref={innerRef}
+      className={String(css([props.className, props.style]))}
+    />
+  );
+};
+
+export default Text;
diff --git a/packages/desktop-client/src/components/View.js b/packages/desktop-client/src/components/common/View.tsx
similarity index 54%
rename from packages/desktop-client/src/components/View.js
rename to packages/desktop-client/src/components/common/View.tsx
index 69ea70363..375907551 100644
--- a/packages/desktop-client/src/components/View.js
+++ b/packages/desktop-client/src/components/common/View.tsx
@@ -1,8 +1,16 @@
 import React from 'react';
 
 import { css } from 'glamor';
+import type { CSSProperties } from 'glamor';
 
-function View(props) {
+interface ViewProps extends Omit<React.HTMLProps<HTMLDivElement>, 'style'> {
+  className?: string;
+  style?: CSSProperties;
+  nativeStyle?: React.StyleHTMLAttributes<HTMLDivElement>;
+  innerRef?: React.Ref<HTMLDivElement>;
+}
+
+const View: React.FC<ViewProps> = props => {
   // The default styles are special-cased and pulled out into static
   // styles, and hardcode the class name here. View is used almost
   // everywhere and we can avoid any perf penalty that glamor would
@@ -14,9 +22,9 @@ function View(props) {
       {...restProps}
       ref={innerRef}
       style={nativeStyle}
-      className={`view ${props.className || ''} ${css(props.style)}`}
+      className={`view ${css([props.className, props.style])}`}
     />
   );
-}
+};
 
 export default View;
diff --git a/packages/desktop-client/src/components/forms.js b/packages/desktop-client/src/components/forms.tsx
similarity index 74%
rename from packages/desktop-client/src/components/forms.js
rename to packages/desktop-client/src/components/forms.tsx
index 5ce7b0cf6..a1ff7f059 100644
--- a/packages/desktop-client/src/components/forms.js
+++ b/packages/desktop-client/src/components/forms.tsx
@@ -1,12 +1,18 @@
 import React from 'react';
 
 import { css } from 'glamor';
+import type { CSSProperties } from 'glamor';
 
 import { colors } from '../style';
 
 import { View, Text } from './common';
 
-export function SectionLabel({ title, style }) {
+interface SectionLabelProps {
+  title?: string;
+  style?: CSSProperties;
+}
+
+export const SectionLabel: React.FC<SectionLabelProps> = ({ title, style }) => {
   return (
     <View
       style={[
@@ -23,9 +29,21 @@ export function SectionLabel({ title, style }) {
       {title}
     </View>
   );
+};
+
+interface FormLabelProps {
+  title: string;
+  id?: string;
+  htmlFor?: string;
+  style?: CSSProperties;
 }
 
-export function FormLabel({ style, title, id, htmlFor }) {
+export const FormLabel: React.FC<FormLabelProps> = ({
+  style,
+  title,
+  id,
+  htmlFor,
+}) => {
   return (
     <Text style={[{ fontSize: 13, marginBottom: 3, color: colors.n3 }, style]}>
       <label htmlFor={htmlFor} id={id}>
@@ -33,15 +51,25 @@ export function FormLabel({ style, title, id, htmlFor }) {
       </label>
     </Text>
   );
+};
+
+interface FormFieldProps {
+  style?: CSSProperties;
+  children: React.ReactNode;
 }
 
-export function FormField({ style, children }) {
+export const FormField: React.FC<FormFieldProps> = ({ style, children }) => {
   return <View style={style}>{children}</View>;
-}
+};
 
 // Custom inputs
 
-export function Checkbox(props) {
+type CheckboxProps = Omit<
+  React.HTMLProps<HTMLInputElement>,
+  'type' | 'styles'
+> & { styles?: CSSProperties };
+
+export const Checkbox: React.FC<CheckboxProps> = props => {
   return (
     <input
       type="checkbox"
@@ -95,4 +123,4 @@ export function Checkbox(props) {
       )}
     />
   );
-}
+};
diff --git a/packages/desktop-client/src/components/settings/UI.js b/packages/desktop-client/src/components/settings/UI.tsx
similarity index 81%
rename from packages/desktop-client/src/components/settings/UI.js
rename to packages/desktop-client/src/components/settings/UI.tsx
index eaa3aef48..ea4d04798 100644
--- a/packages/desktop-client/src/components/settings/UI.js
+++ b/packages/desktop-client/src/components/settings/UI.tsx
@@ -2,12 +2,23 @@ import React, { useState } from 'react';
 import { useLocation } from 'react-router';
 
 import { css, media } from 'glamor';
+import type { CSSProperties } from 'glamor';
 
 import { colors } from '../../style';
 import tokens from '../../tokens';
 import { View, Link } from '../common';
 
-export function Setting({ primaryAction, style, children }) {
+interface SettingProps {
+  primaryAction: React.ReactNode;
+  style?: CSSProperties;
+  children: React.ReactNode;
+}
+
+export const Setting: React.FC<SettingProps> = ({
+  primaryAction,
+  style,
+  children,
+}) => {
   return (
     <View
       {...css([
@@ -36,9 +47,13 @@ export function Setting({ primaryAction, style, children }) {
       {primaryAction || null}
     </View>
   );
+};
+
+interface AdvancedToggleProps {
+  children: React.ReactNode;
 }
 
-export function AdvancedToggle({ children }) {
+export const AdvancedToggle: React.FC<AdvancedToggleProps> = ({ children }) => {
   let location = useLocation();
   let [expanded, setExpanded] = useState(location.hash === '#advanced');
 
@@ -81,4 +96,4 @@ export function AdvancedToggle({ children }) {
       Show advanced settings
     </Link>
   );
-}
+};
diff --git a/packages/desktop-client/src/components/spreadsheet/CellValue.js b/packages/desktop-client/src/components/spreadsheet/CellValue.js
index 4ae08b34d..0ae919dd5 100644
--- a/packages/desktop-client/src/components/spreadsheet/CellValue.js
+++ b/packages/desktop-client/src/components/spreadsheet/CellValue.js
@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { styles } from '../../style';
-import Text from '../Text';
+import Text from '../common/Text';
 
 import format from './format';
 import SheetValue from './SheetValue';
diff --git a/upcoming-release-notes/962.md b/upcoming-release-notes/962.md
new file mode 100644
index 000000000..f881fbca7
--- /dev/null
+++ b/upcoming-release-notes/962.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+TypeScript: migrated an assortment of common components to TS
-- 
GitLab