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