Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DTRA-2055 / Kate / [DTrader-V2] Trade page scrolling issue #17176

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Carousel from 'AppV2/Components/Carousel';
import BarrierDescription from './barrier-description';
import BarrierInput from './barrier-input';
import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TDurationProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -49,10 +50,21 @@ const Barrier = observer(({ is_minimized }: TDurationProps) => {
readOnly
label={<Localize i18n_default_text='Barrier' key={`barrier${is_minimized ? '-minimized' : ''}`} />}
value={v2_params_initial_values.barrier_1 || barrier_1}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
className={clsx('trade-params__option', is_minimized && 'trade-params__option--minimized')}
/>
<ActionSheet.Root isOpen={is_open} onClose={() => onClose(false)} position='left' expandable={false}>
<ActionSheet.Root
isOpen={is_open}
onClose={() => {
onClose(false);
removeFocus();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to remove focus on ActionSheet close and on every single element?
wouldn't it be sufficient to only remove focus from action sheet, or just generic document.body.blur() ?

or maybe, the option would be to specifically focus on some choosen element - e.g. do tricks like window.focus()?
(or combination of document.body.blur(); and then window.focus() - I have not tested details, just trying to figure out something which could help us avoid to remember the removeFocus in every component with focus)

}}
position='left'
expandable={false}
>
<ActionSheet.Portal shouldCloseOnDrag>
<Carousel
header={CarouselHeader}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'AppV2/Utils/trade-params-utils';
import { useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery';
import { ProposalResponse } from 'Stores/Modules/Trading/trade-store';
import { removeFocus } from 'AppV2/Utils/layout-utils';

const timeToMinutes = (time: string) => {
const [hours, minutes] = time.split(':').map(Number);
Expand Down Expand Up @@ -151,7 +152,8 @@ const DayInput = ({
textAlignment='center'
value={formatted_date}
disabled={duration_units_list.filter(item => item.value === 'd').length === 0}
onClick={() => {
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
Copy link
Contributor

@wojciech-deriv wojciech-deriv Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any chance to find solution which does not require to remember to remove focus from every single element? it feels like a recipe for sneaky bugs, I'm sure ppl will forget and it will sneak through the PRs.

The same way missing dependencies in useHook sneaks through all the time and causes sneaky bugs all the time. Everyone knows about useEffect dependencies, but ppl still forget to add them and miss them in reviews, so basically its a source of bugs and unnecessary overhead in reviews.

cc @kate-deriv @nijil-deriv

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to rework, @nijil-deriv gave me a tip to think about wrapper. The idea is to wrap all trade params with wrapper and inside that wrapper to track focus. If it'll be inside input, then remove it

Copy link
Contributor Author

@kate-deriv kate-deriv Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on comments I did another solution, could you please check?
Link
What was done: TradeParameters (in packages/trader/src/AppV2/Components/TradeParameters/trade-parameters.tsx) is a container for all trade parameters, so inside of useEffect I added an event listener for focusin. If event.target contains specific custom attribute (data-focus), then I'll remove focus.
Custom attributes (data-focus) were added for all trade params inputs. That was the only way that works for me to indicate that it's correct input. We can't just remove focus from all inputs inside trade params components as user won't be able to even type something for stake, barrier and etc
@wojciech-deriv @nijil-deriv

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If alternative solution will melt your hearts, I'll merge that branch into a current one

setOpen(true);
}}
leftIcon={<LabelPairedCalendarSmRegularIcon width={24} height={24} />}
Expand All @@ -164,7 +166,8 @@ const DayInput = ({
name='time'
value={`${(formatted_date === formatted_current_date ? end_time : temp_expiry_time) || '23:59:59'} GMT`}
disabled={formatted_date !== formatted_current_date || !is_24_hours_contract}
onClick={() => {
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setOpenTimePicker(true);
}}
leftIcon={<LabelPairedClockThreeSmRegularIcon width={24} height={24} />}
Expand All @@ -184,6 +187,7 @@ const DayInput = ({
onClose={() => {
setOpen(false);
setOpenTimePicker(false);
removeFocus();
}}
position='left'
expandable={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getDisplayedContractTypes } from 'AppV2/Utils/trade-types-utils';
import useActiveSymbols from 'AppV2/Hooks/useActiveSymbols';
import { getDatePickerStartDate, getSmallestDuration } from 'AppV2/Utils/trade-params-utils';
import { useStore } from '@deriv/stores';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TDurationProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -170,13 +171,17 @@ const Duration = observer(({ is_minimized }: TDurationProps) => {
noStatusIcon
disabled={isMarketClosed(activeSymbols, symbol)}
className={clsx('trade-params__option', is_minimized && 'trade-params__option--minimized')}
onClick={() => setOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setOpen(true);
}}
status={has_error ? 'error' : 'neutral'}
/>
<ActionSheet.Root
isOpen={is_open}
onClose={() => {
setOpen(false);
removeFocus();
}}
position='left'
expandable={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import TradeParamDefinition from 'AppV2/Components/TradeParamDefinition';
import { isSmallScreen } from 'AppV2/Utils/trade-params-utils';
import GrowthRatePicker from './growth-rate-picker';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TGrowthRateProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -43,6 +44,7 @@ const GrowthRate = observer(({ is_minimized }: TGrowthRateProps) => {
};
const onActionSheetClose = () => {
setIsOpen(false);
removeFocus();
};

const action_sheet_content = [
Expand Down Expand Up @@ -98,7 +100,10 @@ const GrowthRate = observer(({ is_minimized }: TGrowthRateProps) => {
label={
<Localize i18n_default_text='Growth rate' key={`growth-rate${is_minimized ? '-minimized' : ''}`} />
}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
readOnly
value={`${getGrowthRatePercentage(growth_rate)}%`}
variant='fill'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Skeleton } from '@deriv/components';
import { Localize } from '@deriv/translations';
import { useTraderStore } from 'Stores/useTraderStores';
import LastDigitSelector from './last-digit-selector';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TLastDigitSelectorProps = {
is_minimized?: boolean;
Expand All @@ -31,6 +32,7 @@ const LastDigitPrediction = observer(({ is_minimized }: TLastDigitSelectorProps)
const onActionSheetClose = () => {
setIsOpen(false);
setSelectedDigit(last_digit);
removeFocus();
};

if (is_minimized)
Expand All @@ -47,7 +49,10 @@ const LastDigitPrediction = observer(({ is_minimized }: TLastDigitSelectorProps)
}
value={last_digit}
className={clsx('trade-params__option', 'trade-params__option--minimized')}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
/>
<ActionSheet.Root isOpen={is_open} onClose={onActionSheetClose} position='left' expandable={false}>
<ActionSheet.Portal shouldCloseOnDrag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import TradeParamDefinition from 'AppV2/Components/TradeParamDefinition';
import { isSmallScreen } from 'AppV2/Utils/trade-params-utils';
import MultiplierWheelPicker from './multiplier-wheel-picker';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TMultiplierProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -67,14 +68,18 @@ const Multiplier = observer(({ is_minimized }: TMultiplierProps) => {
}
value={`x${multiplier}`}
className={classname}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
/>
<ActionSheet.Root
expandable={false}
isOpen={isOpen}
position='left'
onClose={() => {
setIsOpen(false);
removeFocus();
}}
>
<ActionSheet.Portal shouldCloseOnDrag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Carousel from 'AppV2/Components/Carousel';
import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import TradeParamDefinition from 'AppV2/Components/TradeParamDefinition';
import PayoutPerPointWheel from './payout-per-point-wheel';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TPayoutPerPointProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -90,12 +91,23 @@ const PayoutPerPoint = observer(({ is_minimized }: TPayoutPerPointProps) => {
key={`payout-per-point${is_minimized ? '-minimized' : ''}`}
/>
}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
readOnly
variant='fill'
value={`${v2_params_initial_values?.payout_per_point ?? payout_per_point} ${currency_display_code}`}
/>
<ActionSheet.Root isOpen={is_open} onClose={() => setIsOpen(false)} position='left' expandable={false}>
<ActionSheet.Root
isOpen={is_open}
onClose={() => {
setIsOpen(false);
removeFocus();
}}
position='left'
expandable={false}
>
<ActionSheet.Portal shouldCloseOnDrag>
<Carousel
classname={clsx(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import TradeParamDefinition from 'AppV2/Components/TradeParamDefinition';
import { addUnit, isSmallScreen } from 'AppV2/Utils/trade-params-utils';
import RiskManagementPicker from './risk-management-picker';
import RiskManagementContent from './risk-management-content';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TRiskManagementProps = {
is_minimized?: boolean;
Expand All @@ -29,7 +30,10 @@ const RiskManagement = observer(({ is_minimized }: TRiskManagementProps) => {
stop_loss,
} = useTraderStore();

const closeActionSheet = () => setIsOpen(false);
const closeActionSheet = () => {
setIsOpen(false);
removeFocus();
};
const getRiskManagementText = () => {
if (has_cancellation) return `DC: ${addUnit({ value: cancellation_duration, unit: localize('minutes') })}`;
if (has_take_profit && has_stop_loss)
Expand Down Expand Up @@ -79,7 +83,10 @@ const RiskManagement = observer(({ is_minimized }: TRiskManagementProps) => {
key={`risk-management${is_minimized ? '-minimized' : ''}`}
/>
}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
readOnly
value={getRiskManagementText()}
variant='fill'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useTraderStore } from 'Stores/useTraderStores';
import { getDisplayedContractTypes } from 'AppV2/Utils/trade-types-utils';
import StakeDetails from './stake-details';
import useContractsForCompany from 'AppV2/Hooks/useContractsForCompany';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TStakeProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -203,12 +204,23 @@ const Stake = observer(({ is_minimized }: TStakeProps) => {
readOnly
label={<Localize i18n_default_text='Stake' key={`stake${is_minimized ? '-minimized' : ''}`} />}
noStatusIcon
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
value={`${v2_params_initial_values?.stake ?? amount} ${getCurrencyDisplayCode(currency)}`}
className={clsx('trade-params__option', is_minimized && 'trade-params__option--minimized')}
status={stake_error && !is_open ? 'error' : undefined}
/>
<ActionSheet.Root isOpen={is_open} onClose={() => onClose(false)} position='left' expandable={false}>
<ActionSheet.Root
isOpen={is_open}
onClose={() => {
onClose(false);
removeFocus();
}}
position='left'
expandable={false}
>
<ActionSheet.Portal shouldCloseOnDrag>
<ActionSheet.Header title={<Localize i18n_default_text='Stake' />} />
<ActionSheet.Content className='stake-content'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Carousel from 'AppV2/Components/Carousel';
import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import { isSmallScreen } from 'AppV2/Utils/trade-params-utils';
import StrikeWheel from './strike-wheel';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TStrikeProps = {
is_minimized?: boolean;
Expand Down Expand Up @@ -97,12 +98,23 @@ const Strike = observer(({ is_minimized }: TStrikeProps) => {
<TextField
className={classname}
label={<Localize i18n_default_text='Strike price' key={`strike${is_minimized ? '-minimized' : ''}`} />}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
readOnly
variant='fill'
value={barrier_1}
/>
<ActionSheet.Root isOpen={is_open} onClose={() => setIsOpen(false)} position='left' expandable={false}>
<ActionSheet.Root
isOpen={is_open}
onClose={() => {
setIsOpen(false);
removeFocus();
}}
position='left'
expandable={false}
>
<ActionSheet.Portal shouldCloseOnDrag>
<Carousel
classname={clsx('strike__carousel', is_small_screen && 'strike__carousel--small')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Carousel from 'AppV2/Components/Carousel';
import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import TakeProfitAndStopLossInput from '../RiskManagement/take-profit-and-stop-loss-input';
import TradeParamDefinition from 'AppV2/Components/TradeParamDefinition';
import { removeFocus } from 'AppV2/Utils/layout-utils';

type TTakeProfitProps = {
is_minimized?: boolean;
Expand All @@ -19,7 +20,10 @@ const TakeProfit = observer(({ is_minimized }: TTakeProfitProps) => {

const [is_open, setIsOpen] = React.useState(false);

const onActionSheetClose = () => setIsOpen(false);
const onActionSheetClose = () => {
setIsOpen(false);
removeFocus();
};

const action_sheet_content = [
{
Expand All @@ -46,7 +50,10 @@ const TakeProfit = observer(({ is_minimized }: TTakeProfitProps) => {
label={
<Localize i18n_default_text='Take profit' key={`take-profit${is_minimized ? '-minimized' : ''}`} />
}
onClick={() => setIsOpen(true)}
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
removeFocus(e);
setIsOpen(true);
}}
readOnly
variant='fill'
value={has_take_profit && take_profit ? `${take_profit} ${getCurrencyDisplayCode(currency)}` : '-'}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TRADE_TYPES } from '@deriv/shared';
import { isTradeParamVisible, getChartHeight } from '../layout-utils';
import { isTradeParamVisible, getChartHeight, removeFocus } from '../layout-utils';

describe('isTradeParamVisible', () => {
it('should return correct value for expiration component key', () => {
Expand Down Expand Up @@ -132,3 +135,17 @@ describe('getChartHeight', () => {
).toEqual(chart_height_with_additional_info);
});
});

describe('removeFocus', () => {
it('removes focus from the element', () => {
const MockComponent = () => (
<input type='text' onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => removeFocus(e)} />
);
render(<MockComponent />);

const input = screen.getByRole('textbox');
userEvent.click(input);

expect(input).not.toHaveFocus();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, ReactNode } from 'react';
import React, { ReactElement } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CONTRACT_TYPES, TRADE_TYPES } from '@deriv/shared';
Expand Down
9 changes: 9 additions & 0 deletions packages/trader/src/AppV2/Utils/layout-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,12 @@ export const getChartHeight = ({
return height - HEIGHT.ADDITIONAL_INFO;
return height;
};

export const removeFocus = (
e?: React.MouseEvent<HTMLInputElement, MouseEvent>,
ref?: React.RefObject<HTMLInputElement>
) => {
(e?.target as HTMLElement)?.blur();
ref?.current?.blur();
(document?.activeElement as HTMLElement)?.blur();
kate-deriv marked this conversation as resolved.
Show resolved Hide resolved
};
Loading