Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
552 changes: 552 additions & 0 deletions development/scripts/win-tab-switch-perf.mjs

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions packages/components/src/actions/Popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,13 @@ function RawPopover({
}: IPopoverProps) {
const { bottom } = useSafeAreaInsets();
const triggerRef = useRef<View | null>(null);
const placement = getPlacement(placementProp, triggerRef);
// Closed Windows popovers are present throughout every tab tree. Measuring
// each trigger during a focus render forces repeated synchronous layouts;
// the geometry is only needed once the popover is actually opened.
const shouldSkipClosedMeasurement = platformEnv.isDesktopWin && !isOpen;
const placement = shouldSkipClosedMeasurement
? placementProp || 'bottom-end'
: getPlacement(placementProp, triggerRef);
const transformOrigin = useMemo(() => {
switch (placement) {
case 'top':
Expand Down Expand Up @@ -398,7 +404,9 @@ function RawPopover({

const isShowNativeKeepChildrenMountedBackdrop =
platformEnv.isNative && props.keepChildrenMounted;
const maxScrollViewHeight = getMaxScrollViewHeight();
const maxScrollViewHeight = shouldSkipClosedMeasurement
? undefined
: getMaxScrollViewHeight();
const transformOriginStyle = useMemo(
() => ({ transformOrigin }),
[transformOrigin],
Expand Down
94 changes: 94 additions & 0 deletions packages/kit/src/hooks/useListenTabFocusState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @jest-environment jsdom
*/

import { act, renderHook } from '@testing-library/react-native';

import { ERootRoutes, ETabRoutes } from '@onekeyhq/shared/src/routes';

import useListenTabFocusState from './useListenTabFocusState';

import type { NavigationState } from '@react-navigation/native';

let mockRouterChangeListener:
| ((state: NavigationState | undefined) => void)
| undefined;

jest.mock('@onekeyhq/components', () => ({
useOnRouterChange: (
callback: (state: NavigationState | undefined) => void,
) => {
mockRouterChangeListener = callback;
},
}));

function buildState(
currentTab: ETabRoutes,
{ withModal = false }: { withModal?: boolean } = {},
) {
const tabRoutes = [
{ key: 'home', name: ETabRoutes.Home },
{ key: 'market', name: ETabRoutes.Market },
{ key: 'swap', name: ETabRoutes.Swap },
];
const routes: any[] = [
{
key: 'main',
name: ERootRoutes.Main,
state: {
index: tabRoutes.findIndex((route) => route.name === currentTab),
routeNames: tabRoutes.map((route) => route.name),
routes: tabRoutes,
},
},
];
if (withModal) {
routes.push({ key: 'modal', name: ERootRoutes.Modal });
}
return { index: routes.length - 1, routes } as NavigationState;
}

describe('useListenTabFocusState', () => {
beforeEach(() => {
mockRouterChangeListener = undefined;
});

it('only notifies when focus or modal visibility changes', () => {
const callback = jest.fn();
renderHook(() => useListenTabFocusState(ETabRoutes.Home, callback));

act(() => mockRouterChangeListener?.(buildState(ETabRoutes.Home)));
expect(callback).toHaveBeenLastCalledWith(true, false);

act(() => mockRouterChangeListener?.(buildState(ETabRoutes.Home)));
expect(callback).toHaveBeenCalledTimes(1);

act(() => mockRouterChangeListener?.(buildState(ETabRoutes.Market)));
expect(callback).toHaveBeenLastCalledWith(false, false);

act(() => mockRouterChangeListener?.(buildState(ETabRoutes.Market)));
expect(callback).toHaveBeenCalledTimes(2);

act(() =>
mockRouterChangeListener?.(
buildState(ETabRoutes.Market, { withModal: true }),
),
);
expect(callback).toHaveBeenLastCalledWith(false, true);

act(() => mockRouterChangeListener?.(buildState(ETabRoutes.Market)));
expect(callback).toHaveBeenLastCalledWith(false, false);
expect(callback).toHaveBeenCalledTimes(4);
});

it('preserves the extension fallback state', () => {
const callback = jest.fn();
renderHook(() => useListenTabFocusState(ETabRoutes.Home, callback));

act(() => mockRouterChangeListener?.(undefined));
act(() => mockRouterChangeListener?.(undefined));

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(true, false);
});
});
32 changes: 27 additions & 5 deletions packages/kit/src/hooks/useListenTabFocusState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@ export default function useListenTabFocusState(
callback: (isFocus: boolean, isHideByModal: boolean) => void, // do NOT useCallback to wrap the callback
) {
const tabNames = Array.isArray(tabName) ? tabName : [tabName];
const previousStateRef = useRef<
| {
isFocus: boolean;
isHideByModal: boolean;
}
| undefined
>(undefined);
useOnRouterChange((state) => {
// the state may be undefined when initializing the interface on the Ext.
if (!state) {
callback(tabName === ETabRoutes.Home, false);
const isFocus = tabName === ETabRoutes.Home;
if (
previousStateRef.current?.isFocus !== isFocus ||
previousStateRef.current?.isHideByModal !== false
) {
previousStateRef.current = { isFocus, isHideByModal: false };
callback(isFocus, false);
}
return;
}
const rootState = state?.routes.find(
Expand All @@ -29,10 +43,18 @@ export default function useListenTabFocusState(
const currentTabName = rootState?.routeNames
? (rootState?.routeNames?.[rootState?.index || 0] as ETabRoutes)
: (rootState?.routes[0].name as ETabRoutes);
callback(
tabNames.includes(currentTabName),
!!(modalRoutes || fullModalRoutes || fullScreenPushRoutes),
);
const nextState = {
isFocus: tabNames.includes(currentTabName),
isHideByModal: !!(modalRoutes || fullModalRoutes || fullScreenPushRoutes),
};
if (
previousStateRef.current?.isFocus === nextState.isFocus &&
previousStateRef.current?.isHideByModal === nextState.isHideByModal
) {
return;
}
previousStateRef.current = nextState;
callback(nextState.isFocus, nextState.isHideByModal);
});
}

Expand Down
70 changes: 70 additions & 0 deletions packages/kit/src/hooks/usePromiseResult.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,19 @@ jest.mock('@onekeyhq/kit/src/hooks/useRouteIsFocused', () => {
const ReactModule = require('react') as typeof import('react');
let currentFocus = true;
const listeners: Array<(v: boolean) => void> = [];
const refListeners: Array<(v: boolean) => void> = [];

const __setFocus = (v: boolean) => {
if (currentFocus === v) return;
currentFocus = v;
listeners.slice().forEach((l) => l(v));
refListeners.slice().forEach((l) => l(v));
};

const __resetFocus = () => {
currentFocus = true;
listeners.slice().forEach((l) => l(true));
refListeners.slice().forEach((l) => l(true));
};

const useRouteIsFocused = () => {
Expand All @@ -67,8 +70,54 @@ jest.mock('@onekeyhq/kit/src/hooks/useRouteIsFocused', () => {
return v;
};

const useRouteIsFocusedRef = ({
onChangeRef,
overrideIsFocused,
}: {
onChangeRef: React.MutableRefObject<
((isFocused: boolean) => void) | undefined
>;
overrideIsFocused?: (isFocused: boolean) => boolean;
}) => {
const overrideRef = ReactModule.useRef(overrideIsFocused);
overrideRef.current = overrideIsFocused;
const applyOverride = (isFocused: boolean) =>
overrideRef.current?.(isFocused) ?? isFocused;
const renderValue = applyOverride(currentFocus);
const focusedRef = ReactModule.useRef(renderValue);
focusedRef.current = renderValue;
const notifiedRef = ReactModule.useRef<boolean | undefined>(undefined);

ReactModule.useEffect(() => {
const listener = (isFocused: boolean) => {
const nextValue = applyOverride(isFocused);
focusedRef.current = nextValue;
if (notifiedRef.current !== nextValue) {
notifiedRef.current = nextValue;
onChangeRef.current?.(nextValue);
}
};
refListeners.push(listener);
listener(currentFocus);
return () => {
const idx = refListeners.indexOf(listener);
if (idx >= 0) refListeners.splice(idx, 1);
};
}, [onChangeRef]);

ReactModule.useEffect(() => {
if (notifiedRef.current !== renderValue) {
notifiedRef.current = renderValue;
onChangeRef.current?.(renderValue);
}
}, [onChangeRef, renderValue]);

return focusedRef;
};

return {
useRouteIsFocused,
useRouteIsFocusedRef,
__setFocus,
__resetFocus,
};
Expand Down Expand Up @@ -709,6 +758,27 @@ describe('usePromiseResult', () => {
expect(method).toHaveBeenCalledTimes(1);
});

it('does not rerender the consumer solely because route focus changes', async () => {
const method = jest.fn(() => new Promise<string>(() => {}));
const { result } = renderHook(() =>
usePromiseResultWithRenderCount(method),
);

await waitFor(() => {
expect(method).toHaveBeenCalledTimes(1);
});
const initialRenderCount = result.current.renderCount;

act(() => {
focusControl.__setFocus(false);
});
act(() => {
focusControl.__setFocus(true);
});

expect(result.current.renderCount).toBe(initialRenderCount);
});

it('does not start fetch when not focused at mount (default checkIsFocused: true)', async () => {
act(() => {
focusControl.__setFocus(false);
Expand Down
Loading
Loading