mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 01:58:40 -05:00
♻️ (typescript) port some common components to strict TS (#2481)
This commit is contained in:
committed by
GitHub
parent
e44fbb3847
commit
afaee6bc16
@@ -1,5 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import { type ComponentProps } from 'react';
|
||||
import { type ComponentProps, type ReactNode } from 'react';
|
||||
|
||||
import { type CSSProperties } from '../../style';
|
||||
|
||||
@@ -7,8 +6,8 @@ import { Block } from './Block';
|
||||
import { View } from './View';
|
||||
|
||||
type AlignedTextProps = ComponentProps<typeof View> & {
|
||||
left;
|
||||
right;
|
||||
left: ReactNode;
|
||||
right: ReactNode;
|
||||
style?: CSSProperties;
|
||||
leftStyle?: CSSProperties;
|
||||
rightStyle?: CSSProperties;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { forwardRef, type ElementType, type HTMLProps } from 'react';
|
||||
|
||||
import { css } from 'glamor';
|
||||
@@ -31,7 +30,9 @@ type ButtonType =
|
||||
| 'menu'
|
||||
| 'menuSelected';
|
||||
|
||||
const backgroundColor = {
|
||||
const backgroundColor: {
|
||||
[key in ButtonType | `${ButtonType}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalBackground,
|
||||
normalDisabled: theme.buttonNormalDisabledBackground,
|
||||
primary: theme.buttonPrimaryBackground,
|
||||
@@ -43,7 +44,7 @@ const backgroundColor = {
|
||||
link: theme.buttonBareBackground,
|
||||
};
|
||||
|
||||
const backgroundColorHover = {
|
||||
const backgroundColorHover: Record<ButtonType, string> = {
|
||||
normal: theme.buttonNormalBackgroundHover,
|
||||
primary: theme.buttonPrimaryBackgroundHover,
|
||||
bare: theme.buttonBareBackgroundHover,
|
||||
@@ -52,7 +53,9 @@ const backgroundColorHover = {
|
||||
link: theme.buttonBareBackground,
|
||||
};
|
||||
|
||||
const borderColor = {
|
||||
const borderColor: {
|
||||
[key in ButtonType | `${ButtonType}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalBorder,
|
||||
normalDisabled: theme.buttonNormalDisabledBorder,
|
||||
primary: theme.buttonPrimaryBorder,
|
||||
@@ -62,7 +65,9 @@ const borderColor = {
|
||||
link: theme.buttonBareBackground,
|
||||
};
|
||||
|
||||
const textColor = {
|
||||
const textColor: {
|
||||
[key in ButtonType | `${ButtonType}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalText,
|
||||
normalDisabled: theme.buttonNormalDisabledText,
|
||||
primary: theme.buttonPrimaryText,
|
||||
@@ -74,7 +79,9 @@ const textColor = {
|
||||
link: theme.pageTextLink,
|
||||
};
|
||||
|
||||
const textColorHover = {
|
||||
const textColorHover: {
|
||||
[key in ButtonType]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalTextHover,
|
||||
primary: theme.buttonPrimaryTextHover,
|
||||
bare: theme.buttonBareTextHover,
|
||||
@@ -87,7 +94,10 @@ const linkButtonHoverStyles = {
|
||||
boxShadow: 'none',
|
||||
};
|
||||
|
||||
const _getBorder = (type, typeWithDisabled) => {
|
||||
const _getBorder = (
|
||||
type: ButtonType,
|
||||
typeWithDisabled: keyof typeof borderColor,
|
||||
): string => {
|
||||
switch (type) {
|
||||
case 'bare':
|
||||
case 'link':
|
||||
@@ -98,7 +108,7 @@ const _getBorder = (type, typeWithDisabled) => {
|
||||
}
|
||||
};
|
||||
|
||||
const _getPadding = type => {
|
||||
const _getPadding = (type: ButtonType): string => {
|
||||
switch (type) {
|
||||
case 'bare':
|
||||
return '5px';
|
||||
@@ -109,7 +119,7 @@ const _getPadding = type => {
|
||||
}
|
||||
};
|
||||
|
||||
const _getActiveStyles = (type, bounce) => {
|
||||
const _getActiveStyles = (type: ButtonType, bounce: boolean): CSSProperties => {
|
||||
switch (type) {
|
||||
case 'bare':
|
||||
return { backgroundColor: theme.buttonBareBackgroundActive };
|
||||
@@ -120,7 +130,7 @@ const _getActiveStyles = (type, bounce) => {
|
||||
};
|
||||
default:
|
||||
return {
|
||||
transform: bounce && 'translateY(1px)',
|
||||
transform: bounce ? 'translateY(1px)' : undefined,
|
||||
boxShadow: `0 1px 4px 0 ${
|
||||
type === 'primary'
|
||||
? theme.buttonPrimaryShadow
|
||||
@@ -150,7 +160,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const typeWithDisabled = disabled ? type + 'Disabled' : type;
|
||||
const typeWithDisabled: ButtonType | `${ButtonType}Disabled` = disabled
|
||||
? `${type}Disabled`
|
||||
: type;
|
||||
|
||||
hoveredStyle = {
|
||||
...(type !== 'bare' && styles.shadow),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
@@ -8,14 +7,16 @@ import {
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps = {
|
||||
children?: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
|
||||
children:
|
||||
| ReactElement<{ inputRef: Ref<HTMLInputElement> }>
|
||||
| ((node: Ref<HTMLInputElement>) => ReactElement);
|
||||
};
|
||||
|
||||
export function InitialFocus({ children }: InitialFocusProps) {
|
||||
const node = useRef(null);
|
||||
const node = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (node.current && !global.IS_DESIGN_MODE) {
|
||||
if (node.current) {
|
||||
// This is needed to avoid a strange interaction with
|
||||
// `ScopeTab`, which doesn't allow it to be focused at first for
|
||||
// some reason. Need to look into it.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
useRef,
|
||||
type InputHTMLAttributes,
|
||||
type KeyboardEvent,
|
||||
type Ref,
|
||||
type InputHTMLAttributes,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import mergeRefs from 'react-merge-refs';
|
||||
|
||||
@@ -42,7 +41,7 @@ export function Input({
|
||||
focused,
|
||||
...nativeProps
|
||||
}: InputProps) {
|
||||
const ref = useRef();
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
useProperFocus(ref, focused);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { useState, type ComponentProps, type ReactNode } from 'react';
|
||||
|
||||
import { type CSSProperties, theme } from '../../style';
|
||||
@@ -23,7 +22,7 @@ export function InputWithContent({
|
||||
getStyle,
|
||||
...props
|
||||
}: InputWithContentProps) {
|
||||
const [focused, setFocused] = useState(props.focused);
|
||||
const [focused, setFocused] = useState(props.focused ?? false);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -37,7 +36,7 @@ export function InputWithContent({
|
||||
(focusStyle ?? {
|
||||
boxShadow: '0 0 0 1px ' + theme.formInputShadowSelected,
|
||||
})),
|
||||
...(getStyle && getStyle(focused)),
|
||||
...getStyle?.(focused),
|
||||
}}
|
||||
>
|
||||
{leftContent}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { type ReactNode, type ComponentProps } from 'react';
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
|
||||
import { css } from 'glamor';
|
||||
@@ -31,7 +34,7 @@ const ButtonLink = ({
|
||||
const navigate = useNavigate();
|
||||
const match = useMatch({ path: to });
|
||||
|
||||
const handleClick = e => {
|
||||
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
onClick?.(e);
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
type FunctionComponent,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
createElement,
|
||||
useEffect,
|
||||
@@ -13,6 +14,10 @@ import { Text } from './Text';
|
||||
import { Toggle } from './Toggle';
|
||||
import { View } from './View';
|
||||
|
||||
const MenuLine: unique symbol = Symbol('menu-line');
|
||||
Menu.line = MenuLine;
|
||||
Menu.label = Symbol('menu-label');
|
||||
|
||||
type KeybindingProps = {
|
||||
keyName: ReactNode;
|
||||
};
|
||||
@@ -29,7 +34,12 @@ type MenuItem = {
|
||||
type?: string | symbol;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
icon?;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
icon?: FunctionComponent<{
|
||||
width: number;
|
||||
height: number;
|
||||
style: CSSProperties;
|
||||
}>;
|
||||
iconSize?: number;
|
||||
text: string;
|
||||
key?: string;
|
||||
@@ -53,21 +63,21 @@ export function Menu<T extends MenuItem>({
|
||||
onMenuSelect,
|
||||
style,
|
||||
}: MenuProps<T>) {
|
||||
const elRef = useRef(null);
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const items = allItems.filter(x => x);
|
||||
const [hoveredIndex, setHoveredIndex] = useState(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elRef.current;
|
||||
el.focus();
|
||||
el?.focus();
|
||||
|
||||
const onKeyDown = e => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const filteredItems = items.filter(
|
||||
item => item && item !== Menu.line && item.type !== Menu.label,
|
||||
);
|
||||
const currentIndex = filteredItems.indexOf(items[hoveredIndex]);
|
||||
const currentIndex = filteredItems.indexOf(items[hoveredIndex || 0]);
|
||||
|
||||
const transformIndex = idx => items.indexOf(filteredItems[idx]);
|
||||
const transformIndex = (idx: number) => items.indexOf(filteredItems[idx]);
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
@@ -90,7 +100,7 @@ export function Menu<T extends MenuItem>({
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
const item = items[hoveredIndex];
|
||||
const item = items[hoveredIndex || 0];
|
||||
if (hoveredIndex !== null && item !== Menu.line) {
|
||||
onMenuSelect?.(item.name);
|
||||
}
|
||||
@@ -99,10 +109,10 @@ export function Menu<T extends MenuItem>({
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('keydown', onKeyDown);
|
||||
el?.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('keydown', onKeyDown);
|
||||
el?.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [hoveredIndex]);
|
||||
|
||||
@@ -203,6 +213,7 @@ export function Menu<T extends MenuItem>({
|
||||
style={{ marginLeft: 5, ...item.style }}
|
||||
onToggle={() =>
|
||||
!item.disabled &&
|
||||
onMenuSelect &&
|
||||
item.toggle !== undefined &&
|
||||
onMenuSelect(item.name)
|
||||
}
|
||||
@@ -217,7 +228,3 @@ export function Menu<T extends MenuItem>({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const MenuLine: unique symbol = Symbol('menu-line');
|
||||
Menu.line = MenuLine;
|
||||
Menu.label = Symbol('menu-label');
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// @ts-strict-ignore
|
||||
import React from 'react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { Tooltip } from '../tooltips';
|
||||
|
||||
export function MenuTooltip({ width, onClose, children }) {
|
||||
type MenuTooltipProps = {
|
||||
width: number;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function MenuTooltip({ width, onClose, children }: MenuTooltipProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
position="bottom-right"
|
||||
|
||||
@@ -103,7 +103,7 @@ export const Modal = ({
|
||||
isOpen={true}
|
||||
onRequestClose={onClose}
|
||||
shouldCloseOnOverlayClick={true}
|
||||
shouldFocusAfterRender={!global.IS_DESIGN_MODE}
|
||||
shouldFocusAfterRender
|
||||
shouldReturnFocusAfterClose={focusAfterClose}
|
||||
appElement={document.querySelector('#root') as HTMLElement}
|
||||
parentSelector={parent && (() => parent)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { type Ref } from 'react';
|
||||
|
||||
import { SvgRemove, SvgSearchAlternate } from '../../icons/v2';
|
||||
@@ -10,7 +9,7 @@ import { InputWithContent } from './InputWithContent';
|
||||
type SearchProps = {
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
value: string;
|
||||
onChange: (value: string) => unknown;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
isInModal?: boolean;
|
||||
width?: number;
|
||||
@@ -30,12 +29,12 @@ export function Search({
|
||||
style={{
|
||||
width,
|
||||
flex: '',
|
||||
borderColor: isInModal ? null : 'transparent',
|
||||
backgroundColor: isInModal ? null : theme.formInputBackground,
|
||||
borderColor: isInModal ? undefined : 'transparent',
|
||||
backgroundColor: isInModal ? undefined : theme.formInputBackground,
|
||||
}}
|
||||
focusStyle={
|
||||
isInModal
|
||||
? null
|
||||
? undefined
|
||||
: {
|
||||
boxShadow: '0 0 0 1px ' + theme.formInputShadowSelected,
|
||||
backgroundColor: theme.formInputBackgroundSelected,
|
||||
|
||||
@@ -55,7 +55,7 @@ export function AmountInput({
|
||||
useEffect(() => setValue(initialValueAbsolute), [initialValueAbsolute]);
|
||||
|
||||
const buttonRef = useRef();
|
||||
const ref = useRef<HTMLInputElement>();
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { useMemo } from 'react';
|
||||
import type { MutableRefObject, Ref, RefCallback } from 'react';
|
||||
|
||||
@@ -7,7 +6,7 @@ export function useMergedRefs<T>(
|
||||
ref2: RefCallback<T> | MutableRefObject<T>,
|
||||
): Ref<T> {
|
||||
return useMemo(() => {
|
||||
function ref(value) {
|
||||
function ref(value: T) {
|
||||
[ref1, ref2].forEach(ref => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(value);
|
||||
|
||||
@@ -77,7 +77,7 @@ export function AvoidRefocusScrollProvider({
|
||||
|
||||
export function useProperFocus(
|
||||
ref: RefObject<HTMLElement>,
|
||||
shouldFocus: boolean,
|
||||
shouldFocus = false,
|
||||
): void {
|
||||
const context = useContext(AvoidRefocusScrollContext);
|
||||
const prevShouldFocus = useRef(null);
|
||||
|
||||
6
upcoming-release-notes/2481.md
Normal file
6
upcoming-release-notes/2481.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Convert most common components to strict TypeScript
|
||||
Reference in New Issue
Block a user