Skip to content
This repository was archived by the owner on Dec 22, 2025. It is now read-only.

Commit 32bb93e

Browse files
committed
Fix resetting of focus on field error
1 parent dc234e5 commit 32bb93e

File tree

8 files changed

+140
-84
lines changed

8 files changed

+140
-84
lines changed

components/Home.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {Equipment, EquipmentCompositionType} from 'model/Equipment';
99
import {Campaign} from 'model/Campaign';
1010
import RecommendedCampaigns from 'components/calculationResult/RecommendedCampaigns';
1111
import CalculationInputCard from 'components/calculationInput/CalculationInputCard';
12-
import {CampaignsById, EquipmentsById} from 'components/calculationInput/PiecesCalculationCommonTypes';
12+
import {
13+
CampaignsById, EquipmentsById,
14+
} from 'components/calculationInput/PiecesCalculationCommonTypes';
1315
import useSWR from 'swr';
1416
import RecommendationsSummary from 'components/calculationResult/RecommendationsSummary';
1517
import IgnoredCampaigns from 'components/calculationResult/IgnoredCampaigns';
@@ -23,7 +25,10 @@ import {
2325
hashTierAndCategoryKey,
2426
} from 'components/calculationInput/equipments/EquipmentsInput';
2527
import {PieceState} from 'components/calculationInput/equipments/inventory/PiecesInventory';
26-
import {calculatePiecesState} from 'components/calculationInput/equipments/inventory/piecesStateCalculator';
28+
import {calculatePiecesState}
29+
from 'components/calculationInput/equipments/inventory/piecesStateCalculator';
30+
import {AddToInventoryDialogContextProvider}
31+
from './calculationInput/equipments/inventory/AddToInventoryDialog';
2732

2833
const Home: NextPage = observer((props) => {
2934
const store = useStore();
@@ -150,10 +155,10 @@ const Home: NextPage = observer((props) => {
150155
onSetSolution={onSetSolution}
151156
/>
152157

153-
{
154-
store.equipmentsRequirementStore.resultMode === ResultMode.LinearProgram ?
155-
buildLinearProgrammingSolution() : buildListStageOnlyResult()
156-
}
158+
<AddToInventoryDialogContextProvider equipById={equipmentsById} piecesState={piecesState}>
159+
{store.equipmentsRequirementStore.resultMode === ResultMode.LinearProgram ?
160+
buildLinearProgrammingSolution() : buildListStageOnlyResult()}
161+
</AddToInventoryDialogContextProvider>
157162
</>;
158163
});
159164

components/calculationInput/common/PositiveIntegerOnlyInput.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1-
import {TextField} from '@mui/material';
1+
import {TextField, TextFieldProps} from '@mui/material';
22
import {Controller, FieldValues, Path} from 'react-hook-form';
33
import React from 'react';
44
import {useTranslation} from 'next-i18next';
55
import {Control} from 'react-hook-form/dist/types/form';
66

7-
interface PositiveIntegerOnlyInputProps<T extends FieldValues> {
7+
interface PositiveIntegerOnlyInputProps<T extends FieldValues>
8+
extends Omit<TextFieldProps,
9+
| 'inputProps' | 'variant' | 'error' | 'helperText' | 'label'
10+
| 'onChange' | 'onBlur' | 'value' | 'name' | 'ref'>
11+
{
812
name: Path<T>;
913
control: Control<T>;
1014
showError: boolean;
1115
helperText: string;
1216
min?: number;
1317
inputLabel?: string;
14-
focused?: boolean;
18+
required?: boolean;
1519
}
1620

17-
const PositiveIntegerOnlyInput = function<T>({
21+
const PositiveIntegerOnlyInput = function<T extends FieldValues>({
1822
name, control, showError, helperText,
19-
min = 1, inputLabel, focused,
23+
min = 1, inputLabel, required = true,
24+
...others
2025
}: PositiveIntegerOnlyInputProps<T>) {
2126
const {t} = useTranslation('home');
2227
return <Controller
2328
name={name}
2429
control={control}
2530
rules={{
2631
required: {
27-
value: true,
32+
value: required,
2833
message: t('addPieceDialog.required'),
2934
},
3035
pattern: {
@@ -40,10 +45,11 @@ const PositiveIntegerOnlyInput = function<T>({
4045
message: t('addPieceDialog.maximumIs', {max: 999}),
4146
},
4247
}}
43-
render={({field}) => (
48+
render={({field: {ref: fieldRef, ...field}}) => (
4449
<TextField
50+
{...others}
4551
{...field}
46-
inputRef={(elm) => elm && focused && elm.focus()}
52+
inputRef={fieldRef}
4753
inputProps={{pattern: '\\d*'}}
4854
variant="outlined"
4955
error={showError}

components/calculationInput/equipments/inventory/AddToInventoryDialog.tsx

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import styles from './AddToInventoryDialog.module.scss';
22
import {
3-
Box,
4-
Button, Dialog, DialogActions, DialogContent, DialogTitle,
5-
Stack,
6-
ToggleButton,
7-
ToggleButtonGroup,
8-
useMediaQuery, useTheme,
3+
Box, Button, Dialog, DialogActions, DialogContent, DialogTitle,
4+
Stack, ToggleButton, ToggleButtonGroup, useMediaQuery, useTheme,
95
} from '@mui/material';
10-
import React, {useEffect, useMemo, useReducer, useState} from 'react';
6+
import React, {useContext, useEffect, useMemo, useState} from 'react';
117
import {useTranslation} from 'next-i18next';
128
import {useForm} from 'react-hook-form';
139
import {
@@ -21,7 +17,46 @@ import {
2117
Comparators, SortingOrder,
2218
buildArrayIndexComparator, buildComparator,
2319
} from 'common/sortUtils';
24-
import {EquipmentCategories} from 'model/Equipment';
20+
import {EquipmentCategories, EquipmentCompositionType} from 'model/Equipment';
21+
22+
const AddToInventoryDialogContext = React.createContext({
23+
open: (drops: DropPieceIdWithProbAndCount[]) => {},
24+
close: () => {},
25+
});
26+
27+
export const useAddToInventoryDialogContext = () => {
28+
const {open, close} = useContext(AddToInventoryDialogContext);
29+
return [open, close] as const;
30+
};
31+
32+
export const AddToInventoryDialogContextProvider = ({
33+
equipById,
34+
piecesState,
35+
children,
36+
}: {
37+
equipById: EquipmentsById,
38+
piecesState: Map<string, PieceState>,
39+
children?: React.ReactNode,
40+
}) => {
41+
const [drops, setDrops] = useState<DropPieceIdWithProbAndCount[]>([]);
42+
const [open, setOpen] = useState(false);
43+
const contextValue = useMemo(() => ({
44+
open: (drops: DropPieceIdWithProbAndCount[]) => {
45+
setDrops(drops);
46+
setOpen(true);
47+
},
48+
close: () => {
49+
setOpen(false);
50+
},
51+
}), [setDrops]);
52+
53+
return <AddToInventoryDialogContext.Provider value={contextValue}>
54+
<AddToInventoryDialog open={open}
55+
onUpdate={contextValue.close} onCancel={contextValue.close}
56+
equipById={equipById} piecesState={piecesState} drops={drops} />
57+
{children}
58+
</AddToInventoryDialogContext.Provider>;
59+
};
2560

2661
const AddToInventoryDialog = ({
2762
open,
@@ -42,20 +77,16 @@ const AddToInventoryDialog = ({
4277
const {t} = useTranslation('home');
4378
const theme = useTheme();
4479

45-
// hack to lazy rendering
46-
const [onceOpen, notifyOpened] = useReducer((x) => true, false);
47-
useEffect(() => {
48-
open && notifyOpened();
49-
}, [open]);
50-
5180
const [mode, setMode] = useState<'all' | 'lack' | 'required'>('lack');
5281

5382
const pieces = useMemo(() => {
5483
// 20-3: Necklace, Watch, Bag
5584
// 20-4: Watch, Charm, Badge
5685
// 13-1: Shoes, Gloves, Hat
5786
// descending tier -> descending category order?
58-
return drops.map(({id}) => (piecesState.get(id) ?? {
87+
return drops.filter(({id}) => (
88+
equipById.get(id)?.equipmentCompositionType === EquipmentCompositionType.Piece
89+
)).map(({id}) => (piecesState.get(id) ?? {
5990
pieceId: id,
6091
needCount: 0,
6192
inStockCount: 0,
@@ -72,45 +103,55 @@ const AddToInventoryDialog = ({
72103
)),
73104
));
74105
}, [drops, piecesState, mode, equipById]);
75-
const defaultValues = useMemo(() => {
76-
return pieces.reduce<InventoryForm>((acc, piece) => {
77-
acc[piece.pieceId] = '';
78-
return acc;
79-
}, {});
80-
}, [pieces]);
81106

82107
const {
83-
control,
84-
formState: {isValid: isCountValid, errors: allErrors},
85-
getValues,
86-
reset,
108+
control, formState,
109+
getValues, reset,
110+
getFieldState, setFocus,
111+
handleSubmit,
87112
} = useForm<InventoryForm>({
88113
mode: 'onChange',
89-
defaultValues,
114+
defaultValues: Object.fromEntries(drops.map(({id}) => [id, ''])),
90115
});
91116

92-
const handleCancelDialog = () => {
117+
useEffect(() => {
118+
if (!open) return;
119+
reset(Object.fromEntries(drops.map(({id}) => [id, ''])));
120+
}, [drops, open, reset]);
121+
122+
const handleCancel = () => {
93123
onCancel();
94-
reset();
95124
};
96125

97-
const handleUpdateInventory = () => {
98-
onUpdate(getValues());
99-
const inventory = Object.entries(getValues()).reduce<InventoryForm>((acc, [id, value]) => {
100-
const count = parseInt(value) ?? 0;
101-
const stock = piecesState.get(id)?.inStockCount ?? 0;
102-
acc[id] = `${count + stock}`;
103-
return acc;
104-
}, {});
126+
const handleUpdate = handleSubmit((value) => {
127+
onUpdate(value);
128+
const inventory = Object.fromEntries(Object.entries(value).map(([id, value]) => {
129+
const count = parseInt(value) || 0;
130+
const stock = piecesState.get(id)?.inStockCount || 0;
131+
return [id, `${count + stock}`];
132+
}));
105133
store.equipmentsRequirementStore.updateInventory(inventory);
106-
reset();
107-
};
134+
}, (errors) => {
135+
const field = Object.entries(errors).find(([, it]) => it && 'ref' in it)?.[0];
136+
console.log(errors);
137+
field && setFocus(field);
138+
});
108139

109140
const isFullScreen = useMediaQuery(theme.breakpoints.down('md'));
110141
const isXsOrSmallerScreen = useMediaQuery(theme.breakpoints.down('sm'));
111142
const hasManyPieces = () => pieces.length > 3;
112143

113-
return !onceOpen ? null : <Dialog open={open} keepMounted fullWidth
144+
useEffect(() => {
145+
const id = setTimeout(() => {
146+
const field = pieces.find(({pieceId}) => (
147+
getValues(pieceId) === '' || getFieldState(pieceId).invalid
148+
)) ?? pieces[0];
149+
field && setFocus(field.pieceId, {shouldSelect: true});
150+
}, 100);
151+
return () => clearTimeout(id);
152+
}, [equipById, getFieldState, getValues, open, pieces, setFocus]);
153+
154+
return <Dialog open={open} fullWidth
114155
fullScreen={hasManyPieces() && isFullScreen}
115156
maxWidth={hasManyPieces() && 'xl'}>
116157
<Stack component={DialogTitle} direction='row' alignItems='center'>
@@ -127,21 +168,20 @@ const AddToInventoryDialog = ({
127168
<DialogContent className={styles.dialogContentContainer}>
128169
<div className={styles.filler}></div>
129170
<div className={`${styles.container} ${isXsOrSmallerScreen && styles.xs}`}>
130-
{pieces.map((piece, index) => {
131-
return <ObtainedPieceBox key={piece.pieceId} allErrors={allErrors}
132-
control={control}
133-
equipmentsById={equipById}
134-
piece={piece}
135-
focused={index === 0}/>;
171+
{pieces.map((piece) => {
172+
return <ObtainedPieceBox key={piece.pieceId}
173+
allErrors={formState.errors} control={control}
174+
equipmentsById={equipById} piece={piece}
175+
required={false} />;
136176
})}
137177
</div>
138178
{pieces.length === 0 && <div>{t('filterResultEmpty')}</div>}
139179
<div className={styles.filler}></div>
140180
</DialogContent>
141181

142182
<DialogActions>
143-
<Button onClick={handleCancelDialog}>{t('cancelButton')}</Button>
144-
<Button onClick={handleUpdateInventory} disabled={!isCountValid}>{t('addButton')}</Button>
183+
<Button onClick={handleCancel}>{t('cancelButton')}</Button>
184+
<Button onClick={handleUpdate} disabled={!formState.isValid}>{t('addButton')}</Button>
145185
</DialogActions>
146186
</Dialog>;
147187
};

components/calculationInput/equipments/inventory/ObtainedPieceBox.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,23 @@ import {InventoryForm}
1212
from 'components/calculationInput/equipments/inventory/InventoryUpdateDialog';
1313
import {useTranslation} from 'next-i18next';
1414

15+
interface ObtainedPieceBoxProps
16+
extends Omit<React.ComponentProps<typeof PositiveIntegerOnlyInput>,
17+
'control' | 'name' | 'showError' | 'helperText' | 'inputLabel'>
18+
{
19+
equipmentsById: EquipmentsById,
20+
piece: PieceState,
21+
control: Control<InventoryForm>,
22+
allErrors: any,
23+
}
24+
1525
const ObtainedPieceBox = function({
1626
equipmentsById,
1727
piece,
1828
control,
1929
allErrors,
20-
focused,
21-
}: {
22-
equipmentsById: EquipmentsById,
23-
piece: PieceState,
24-
control: Control<InventoryForm>,
25-
allErrors: any,
26-
focused?: boolean,
27-
}) {
30+
...others
31+
}: ObtainedPieceBoxProps) {
2832
const {t} = useTranslation('home');
2933

3034
const pieceIcon = equipmentsById.get(piece.pieceId)?.icon;
@@ -40,8 +44,8 @@ const ObtainedPieceBox = function({
4044
</CardActionArea>
4145
</Card>
4246
<Box sx={{mr: 2}}/>
43-
<PositiveIntegerOnlyInput name={piece.pieceId}
44-
min={0} focused={focused}
47+
<PositiveIntegerOnlyInput {...others} name={piece.pieceId}
48+
min={0}
4549
control={control} showError={!!allErrors[piece.pieceId]}
4650
helperText={allErrors[piece.pieceId]?.message ?? ''}
4751
inputLabel={t('addPieceDialog.obtainedCount')} />

components/calculationResult/CampaignDropItemsList.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import styles from './CampaignDropItemsList.module.scss';
22
import {Card, CardContent, Typography} from '@mui/material';
3-
import React, {FunctionComponent, useState} from 'react';
3+
import React, {FunctionComponent} from 'react';
44
import {Campaign} from 'model/Campaign';
5-
import {DropPieceIdWithProbAndCount, EquipmentsById} from 'components/calculationInput/PiecesCalculationCommonTypes';
5+
import {
6+
DropPieceIdWithProbAndCount, EquipmentsById,
7+
} from 'components/calculationInput/PiecesCalculationCommonTypes';
68
import Grid from '@mui/material/Unstable_Grid2';
79
import BuiBanner from '../bui/BuiBanner';
810
import {useTranslation} from 'next-i18next';
911
import EquipmentCard from 'components/bui/card/EquipmentCard';
1012
import BuiButton from 'components/bui/BuiButton';
11-
import AddToInventoryDialog from '../calculationInput/equipments/inventory/AddToInventoryDialog';
13+
import {useAddToInventoryDialogContext}
14+
from '../calculationInput/equipments/inventory/AddToInventoryDialog';
1215
import {PieceState} from 'components/calculationInput/equipments/inventory/PiecesInventory';
1316

1417
type CampaignDropItemsListProps = {
@@ -35,15 +38,10 @@ const CampaignDropItemsList :
3538
}
3639
) => {
3740
const {t} = useTranslation('home');
38-
const [open, setOpen] = useState(false);
41+
42+
const [openDialog] = useAddToInventoryDialogContext();
3943
return <Card variant={containerCardVariation} className={styles.cardWrapper}
4044
elevation={containerCardVariation == 'elevation' ? 2 : undefined}>
41-
<AddToInventoryDialog open={open}
42-
onUpdate={() => setOpen(false)}
43-
onCancel={() => setOpen(false)}
44-
equipById={equipmentsById}
45-
piecesState={piecesState}
46-
drops={allDrops} />
4745
<CardContent>
4846
<Grid container>
4947
<Grid xs={12} container className={styles.campaignHeader}>
@@ -58,8 +56,8 @@ const CampaignDropItemsList :
5856

5957
<div style={{flexGrow: 1}}/>
6058

61-
<BuiButton color={'baButtonSecondary'} onClick={() => setOpen(true)}>
62-
{'結果を記入'}
59+
<BuiButton color={'baButtonSecondary'} onClick={() => openDialog(allDrops)}>
60+
{t('fillRewards')}
6361
</BuiButton>
6462
</Grid>
6563

@@ -85,4 +83,4 @@ const CampaignDropItemsList :
8583
</Card>;
8684
};
8785

88-
export default CampaignDropItemsList;
86+
export default React.memo(CampaignDropItemsList);

public/locales/en/home.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
},
7777
"stageSweepingTimes": "{{sweepingTimes}} times",
7878
"stageIsSkipped": "Skipped",
79+
"fillRewards": "Fill in the Rewards",
7980
"possibleRewards": "Possible Rewards",
8081
"otherStages": "Other stages",
8182
"otherStagesSkippedReason": "Skipped because of inefficiency",

0 commit comments

Comments
 (0)