♻️ (typescript) migrating hooks to TypeScript (#1479)

This commit is contained in:
Matiss Janis Aboltins
2023-08-12 10:02:02 +01:00
committed by GitHub
parent a0ecd65e70
commit b325bd9b18
5 changed files with 117 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react';
export default function usePrevious(value) {
const ref = useRef();
export default function usePrevious<T = unknown>(value: T): T | undefined {
const ref = useRef<T | undefined>();
useEffect(() => {
ref.current = value;

View File

@@ -4,13 +4,16 @@ import React, {
useLayoutEffect,
useContext,
useMemo,
type RefObject,
type ReactElement,
type MutableRefObject,
} from 'react';
function getFocusedKey(el) {
let node = el;
function getFocusedKey(el: HTMLElement): string | null {
let node: HTMLElement | ParentNode = el;
// Search up to 10 parent nodes
for (let i = 0; i < 10 && node; i++) {
let key = node.dataset && node.dataset.focusKey;
let key = 'dataset' in node ? node.dataset?.focusKey : undefined;
if (key) {
return key;
}
@@ -20,7 +23,10 @@ function getFocusedKey(el) {
return null;
}
function focusElement(el, refocusContext) {
function focusElement(
el: HTMLElement,
refocusContext: AvoidRefocusScrollContextValue,
): void {
if (refocusContext) {
let key = getFocusedKey(el);
el.focus({ preventScroll: key && key === refocusContext.keyRef.current });
@@ -29,17 +35,29 @@ function focusElement(el, refocusContext) {
el.focus();
}
if (el.tagName === 'INPUT') {
if (el instanceof HTMLInputElement) {
el.setSelectionRange(0, 10000);
}
}
let AvoidRefocusScrollContext = createContext(null);
type AvoidRefocusScrollContextValue = {
keyRef: MutableRefObject<string>;
onKeyChange: (key: string) => void;
};
export function AvoidRefocusScrollProvider({ children }) {
let keyRef = useRef(null);
let AvoidRefocusScrollContext =
createContext<AvoidRefocusScrollContextValue>(null);
let value = useMemo(
type AvoidRefocusScrollProviderProps = {
children: ReactElement;
};
export function AvoidRefocusScrollProvider({
children,
}: AvoidRefocusScrollProviderProps) {
let keyRef = useRef<string>(null);
let value = useMemo<AvoidRefocusScrollContextValue>(
() => ({
keyRef,
onKeyChange: key => {
@@ -56,7 +74,10 @@ export function AvoidRefocusScrollProvider({ children }) {
);
}
export function useProperFocus(ref, shouldFocus) {
export function useProperFocus(
ref: RefObject<HTMLElement>,
shouldFocus: boolean,
): void {
let context = useContext(AvoidRefocusScrollContext);
let prevShouldFocus = useRef(null);
@@ -68,7 +89,7 @@ export function useProperFocus(ref, shouldFocus) {
let selector = 'input,button,div[tabindex]';
let focusEl = view.matches(selector)
? view
: view.querySelector(selector);
: view.querySelector<HTMLElement>(selector);
if (shouldFocus && focusEl) {
focusElement(focusEl, context);

View File

@@ -5,14 +5,20 @@ import React, {
useCallback,
useEffect,
useRef,
type Dispatch,
type ReactElement,
} from 'react';
import { useSelector } from 'react-redux';
import { listen } from 'loot-core/src/platform/client/fetch';
import * as undo from 'loot-core/src/platform/client/undo';
import { type UndoState } from 'loot-core/src/server/undo';
import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
function iterateRange(range, func) {
type Range<T> = { start: T; end: T | null };
type Item = { id: string };
function iterateRange(range: Range<number>, func: (i: number) => void): void {
let from = Math.min(range.start, range.end);
let to = Math.max(range.start, range.end);
@@ -21,9 +27,35 @@ function iterateRange(range, func) {
}
}
export default function useSelected(name, items, initialSelectedIds) {
type State = {
selectedRange: Range<string> | null;
selectedItems: Set<string>;
};
type WithOptionalMouseEvent = {
event?: MouseEvent;
};
type SelectAction = {
type: 'select';
id: string;
} & WithOptionalMouseEvent;
type SelectNoneAction = {
type: 'select-none';
} & WithOptionalMouseEvent;
type SelectAllAction = {
type: 'select-all';
ids?: string[];
} & WithOptionalMouseEvent;
type Actions = SelectAction | SelectNoneAction | SelectAllAction;
export default function useSelected<T extends Item>(
name: string,
items: T[],
initialSelectedIds: string[],
) {
let [state, dispatch] = useReducer(
(state, action) => {
(state: State, action: Actions) => {
switch (action.type) {
case 'select': {
let { selectedRange } = state;
@@ -34,8 +66,8 @@ export default function useSelected(name, items, initialSelectedIds) {
let idx = items.findIndex(p => p.id === id);
let startIdx = items.findIndex(p => p.id === selectedRange.start);
let endIdx = items.findIndex(p => p.id === selectedRange.end);
let range;
let deleteUntil;
let range: Range<number>;
let deleteUntil: Range<number>;
if (endIdx === -1) {
range = { start: startIdx, end: idx };
@@ -93,7 +125,7 @@ export default function useSelected(name, items, initialSelectedIds) {
}
case 'select-none':
return { ...state, selectedItems: new Set() };
return { ...state, selectedItems: new Set<string>() };
case 'select-all':
return {
@@ -106,12 +138,12 @@ export default function useSelected(name, items, initialSelectedIds) {
};
default:
throw new Error('Unexpected action: ' + action.type);
throw new Error('Unexpected action: ' + JSON.stringify(action));
}
},
null,
() => ({
selectedItems: new Set(initialSelectedIds || []),
selectedItems: new Set<string>(initialSelectedIds || []),
selectedRange:
initialSelectedIds && initialSelectedIds.length === 1
? { start: initialSelectedIds[0], end: null }
@@ -165,7 +197,7 @@ export default function useSelected(name, items, initialSelectedIds) {
let lastUndoState = useSelector(state => state.app.lastUndoState);
useEffect(() => {
function onUndo({ messages, undoTag }) {
function onUndo({ messages, undoTag }: UndoState) {
let tagged = undo.getTaggedState(undoTag);
let deletedIds = new Set(
@@ -174,11 +206,7 @@ export default function useSelected(name, items, initialSelectedIds) {
.map(msg => msg.row),
);
if (
tagged &&
tagged.selectedItems &&
tagged.selectedItems.name === name
) {
if (tagged?.selectedItems?.name === name) {
dispatch({
type: 'select-all',
// Coerce the Set into an array
@@ -198,13 +226,12 @@ export default function useSelected(name, items, initialSelectedIds) {
return {
items: state.selectedItems,
setItems: state.setSelectedItems,
dispatch,
};
}
let SelectedDispatch = createContext(null);
let SelectedItems = createContext(null);
let SelectedDispatch = createContext<(action: Actions) => void>(null);
let SelectedItems = createContext<Set<unknown>>(null);
export function useSelectedDispatch() {
return useContext(SelectedDispatch);
@@ -214,7 +241,17 @@ export function useSelectedItems() {
return useContext(SelectedItems);
}
export function SelectedProvider({ instance, fetchAllIds, children }) {
type SelectedProviderProps<T extends Item> = {
instance: ReturnType<typeof useSelected<T>>;
fetchAllIds?: () => Promise<string[]>;
children: ReactElement;
};
export function SelectedProvider<T extends Item>({
instance,
fetchAllIds,
children,
}: SelectedProviderProps<T>) {
let latestItems = useRef(null);
useEffect(() => {
@@ -222,7 +259,7 @@ export function SelectedProvider({ instance, fetchAllIds, children }) {
}, [instance.items]);
let dispatch = useCallback(
async action => {
async (action: Actions) => {
if (!action.event && isNonProductionEnvironment()) {
throw new Error('SelectedDispatch actions must have an event');
}
@@ -257,24 +294,33 @@ export function SelectedProvider({ instance, fetchAllIds, children }) {
);
}
type SelectedProviderWithItemsProps<T extends Item> = {
name: string;
items: T[];
initialSelectedIds: string[];
fetchAllIds: () => Promise<string[]>;
registerDispatch?: (dispatch: Dispatch<Actions>) => void;
children: ReactElement;
};
// This can be helpful in class components if you cannot use the
// custom hook
export function SelectedProviderWithItems({
export function SelectedProviderWithItems<T extends Item>({
name,
items,
initialSelectedIds,
fetchAllIds,
registerDispatch,
children,
}) {
let selected = useSelected(name, items, initialSelectedIds);
}: SelectedProviderWithItemsProps<T>) {
let selected = useSelected<T>(name, items, initialSelectedIds);
useEffect(() => {
registerDispatch?.(selected.dispatch);
}, [registerDispatch]);
return (
<SelectedProvider
<SelectedProvider<T>
instance={selected}
fetchAllIds={fetchAllIds}
children={children}

View File

@@ -2,16 +2,19 @@ export type UndoState = {
id?: string;
url: unknown;
openModal: unknown;
selectedItems: unknown;
selectedItems: {
name: string;
items: Set<string>;
} | null;
};
export function setUndoState(
name: keyof Omit<UndoState, 'id'>,
value: unknown,
export function setUndoState<K extends keyof Omit<UndoState, 'id'>>(
name: K,
value: UndoState[K],
): void;
export type SetUndoState = typeof setUndoState;
export function getUndoState(name: keyof UndoState): unknown;
export function getUndoState<K extends keyof UndoState>(name: K): UndoState[K];
export type GetUndoState = typeof getUndoState;
export function getTaggedState(id: string): UndoState | undefined;

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Migrate hooks from native JS to TypeScript