Merged PR 226: テキストボックス分割実装

## 概要
[Task2168: テキストボックス分割実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2168)

タスク 2168: テキストボックス分割実装
ライセンスキー入力のボックスを5分割に変更。
細かい画面レイアウトについては後々デザイナーさんに依頼することになるかと思いますので、レビュー対象外でお願いします。

## レビューポイント
入力時の挙動に過不足がないか。

## UIの変更
https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2168?csf=1&web=1&e=oRgAOK

## 動作確認状況
- ローカルで動作確認済み

## 補足
なし
This commit is contained in:
oura.a 2023-07-13 00:02:40 +00:00
parent e4ba5229df
commit 9528bb1ad6
4 changed files with 274 additions and 92 deletions

View File

@ -1,10 +1,8 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = {
apps: {
keyLicense: "",
isLoading: false,
},
};
@ -12,20 +10,12 @@ export const licenseCardActivateSlice = createSlice({
name: "licenseCardActivate",
initialState,
reducers: {
changeKeyLicense: (
state,
action: PayloadAction<{ keyLicense: string }>
) => {
const { keyLicense } = action.payload;
state.apps.keyLicense = keyLicense.toUpperCase();
},
cleanupApps: (state) => {
state.apps = initialState.apps;
},
},
});
export const { changeKeyLicense, cleanupApps } =
licenseCardActivateSlice.actions;
export const { cleanupApps } = licenseCardActivateSlice.actions;
export default licenseCardActivateSlice.reducer;

View File

@ -1,18 +1,4 @@
import { RootState } from "../../../app/store";
export const selectInputValidationErrors = (state: RootState) => {
const { keyLicense } = state.licenseCardActivate.apps;
const hasErrorIncorrectKeyNumber = checkErrorIncorrectKeyNumber(keyLicense);
return {
hasErrorIncorrectKeyNumber,
};
};
export const checkErrorIncorrectKeyNumber = (keyLicense: string): boolean =>
// // 20+4(20文字space4個以外の場合はエラー
keyLicense.length !== 24;
export const selectKeyLicense = (state: RootState) =>
state.licenseCardActivate.apps.keyLicense;
export const selectIsLoading = (state: RootState) =>
state.licenseCardActivate.apps.isLoading;

View File

@ -3,6 +3,5 @@ export interface LicenseCardActivateState {
}
export interface Apps {
keyLicense: string;
isLoading: boolean;
}

View File

@ -8,11 +8,8 @@ import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import {
activateCardLicenseAsync,
selectKeyLicense,
cleanupApps,
selectIsLoading,
changeKeyLicense,
selectInputValidationErrors,
} from "../../features/license/licenseCardActivate/index";
import progress_activit from "../../assets/images/progress_activit.svg";
@ -26,9 +23,18 @@ export const CardLicenseActivatePopup: React.FC<
const { onClose } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const cardLicenseKey = useSelector(selectKeyLicense);
const [keyNumber, setKeyNumber] = useState<string>(cardLicenseKey);
const isLoading = useSelector(selectIsLoading);
const [keyNumber1, setKeyNumber1] = useState<string>("");
const [keyNumber2, setKeyNumber2] = useState<string>("");
const [keyNumber3, setKeyNumber3] = useState<string>("");
const [keyNumber4, setKeyNumber4] = useState<string>("");
const [keyNumber5, setKeyNumber5] = useState<string>("");
const TEXTAREASIZE = 4;
const ref1 = useRef<HTMLTextAreaElement>(null);
const ref2 = useRef<HTMLTextAreaElement>(null);
const ref3 = useRef<HTMLTextAreaElement>(null);
const ref4 = useRef<HTMLTextAreaElement>(null);
const ref5 = useRef<HTMLTextAreaElement>(null);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
@ -67,17 +73,114 @@ export const CardLicenseActivatePopup: React.FC<
const [isPushActivateButton, setIsPushActivateButton] =
useState<boolean>(false);
// エラー宣言
const { hasErrorIncorrectKeyNumber } = useSelector(
selectInputValidationErrors
);
// 値を画面に分割して入れる
const inputValueOnTextarea = (input: string, startArea: number) => {
let roopCount = startArea;
while (input.length !== 0 || roopCount === startArea) {
switch (roopCount) {
case 1:
setKeyNumber1(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 2:
setKeyNumber2(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 3:
setKeyNumber3(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 4:
setKeyNumber4(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 5:
setKeyNumber5(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
default:
input = "";
}
roopCount += 1;
}
};
// テキストエリア入力時
const changeTextarea = (
e: React.ChangeEvent<HTMLTextAreaElement>,
areaNum: number
) => {
const input = e.target.value
.toUpperCase()
.replace(/[^A-Z0-9]/g, "")
.substring(0, 20);
inputValueOnTextarea(input, areaNum);
if (e.target.value.includes("\n")) {
setTimeout(() => {
onKeyDownEnter();
}, 0);
}
moveTextarea(input.length, areaNum);
};
// フォーカスを移動する
const moveFocus = (target: string) => {
// stateの反映を同期してから実施
setTimeout(() => {
const obj = document.getElementById(target);
if (obj) {
obj.focus();
}
}, 0);
};
// フォーカス移動判定
const moveTextarea = (length: number, areaNum: number) => {
if (length === TEXTAREASIZE && areaNum !== 5) {
moveFocus(`textarea${(areaNum + 1).toString()}`);
}
};
// キー入力時判定
const keyDown = (
e: React.KeyboardEvent<HTMLTextAreaElement>,
areaNum: number
) => {
if (e.key === "Enter") {
e.preventDefault();
onKeyDownEnter();
} else if (e.key === "ArrowRight") {
if (e.target.selectionStart === e.target.value.length) {
moveFocus(`textarea${(areaNum + 1).toString()}`);
}
} else if (e.key === "ArrowLeft") {
if (e.target.selectionStart === 0) {
moveFocus(`textarea${(areaNum - 1).toString()}`);
}
} else if (/^[a-zA-Z0-9]$/.test(e.key)) {
if (e.target.value.length === TEXTAREASIZE) {
e.preventDefault();
}
} else if (/^[!-/:-@[-`{-~]$/.test(e.key)) {
e.preventDefault();
}
};
// Enterキー押下時の処理
const onKeyDownEnter = () => {
const button = document.getElementById("button");
if (button) {
button.focus();
button.click();
}
};
// activateボタン押下時
const onActivateLicense = useCallback(async () => {
setIsPushActivateButton(true);
if (keyNumber.length !== 24) {
const inputBox = document.getElementById("inputBox");
const keyNumber = `${keyNumber1}${keyNumber2}${keyNumber3}${keyNumber4}${keyNumber5}`;
if (keyNumber.length !== 20) {
const inputBox = document.getElementById("textarea5");
// カーソルをテキストボックスに戻す
if (inputBox) {
inputBox.focus();
@ -86,23 +189,26 @@ export const CardLicenseActivatePopup: React.FC<
}
// activateAPIの呼び出し
const cardLicenseKeyWithoutSpaces = keyNumber.replace(/\s/g, "");
const { meta } = await dispatch(
activateCardLicenseAsync({ cardLicenseKey: cardLicenseKeyWithoutSpaces })
activateCardLicenseAsync({ cardLicenseKey: keyNumber })
);
setIsPushActivateButton(false);
// カーソルをテキストボックスに戻す
const inputBox = document.getElementById("inputBox");
const inputBox = document.getElementById("textarea1");
if (inputBox) {
inputBox.focus();
}
if (meta.requestStatus === "fulfilled") {
dispatch(cleanupApps());
setKeyNumber("");
setKeyNumber1("");
setKeyNumber2("");
setKeyNumber3("");
setKeyNumber4("");
setKeyNumber5("");
}
}, [keyNumber, dispatch]);
}, [keyNumber1, keyNumber2, keyNumber3, keyNumber4, keyNumber5, dispatch]);
// HTML
return (
@ -128,62 +234,163 @@ export const CardLicenseActivatePopup: React.FC<
</label>
</dt>
<dd className="">
<input
id="inputBox"
type="text"
size={48}
name=""
value={keyNumber}
maxLength={24} // 20+4(20文字space4個
className={styles.formInput}
<textarea
id="textarea1"
value={keyNumber1}
rows={1}
cols={TEXTAREASIZE}
ref={ref1}
style={{ resize: "none", fontSize: "16px" }}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(e) => {
let input = e.target.value.toUpperCase();
input = input.replace(/[^A-Z0-9]/g, "");
// _.chunk関数で、配列の要素を一定の要素数ごとに分ける
input = _.chunk(input, 4)
.map((a) => a.join(""))
.join(" ");
setKeyNumber(input);
if (input.includes("\n")) {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
onActivateLicense();
}
}}
onBlur={(e) => {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
let cursorPos = 0;
cursorPos = e.target.selectionStart;
changeTextarea(e, 1);
setTimeout(() => {
if (ref1.current) {
ref1.current.selectionStart = cursorPos;
ref1.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.target.value.toUpperCase();
setKeyNumber(input);
const button = document.getElementById("button");
if (button) {
button.focus();
button.click();
}
keyDown(e, 1);
}}
onFocus={(e) => {
if (ref1.current) {
ref1.current.selectionStart = e.target.value.length;
ref1.current.selectionEnd = e.target.value.length;
}
}}
/>
{isPushActivateButton && hasErrorIncorrectKeyNumber && (
<span className={styles.formError}>
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumberIncorrectError"
)
)}
</span>
)}
<textarea
id="textarea2"
value={keyNumber2}
rows={1}
cols={TEXTAREASIZE}
ref={ref2}
style={{ resize: "none", fontSize: "16px" }}
onChange={(e) => {
let cursorPos = 0;
cursorPos = e.target.selectionStart;
changeTextarea(e, 2);
setTimeout(() => {
if (ref2.current) {
ref2.current.selectionStart = cursorPos;
ref2.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 2);
}}
onFocus={(e) => {
if (ref2.current) {
ref2.current.selectionStart = e.target.value.length;
ref2.current.selectionEnd = e.target.value.length;
}
}}
/>
<textarea
id="textarea3"
value={keyNumber3}
rows={1}
cols={TEXTAREASIZE}
ref={ref3}
style={{ resize: "none", fontSize: "16px" }}
onChange={(e) => {
let cursorPos = 0;
cursorPos = e.target.selectionStart;
changeTextarea(e, 3);
setTimeout(() => {
if (ref3.current) {
ref3.current.selectionStart = cursorPos;
ref3.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 3);
}}
onFocus={(e) => {
if (ref3.current) {
ref3.current.selectionStart = e.target.value.length;
ref3.current.selectionEnd = e.target.value.length;
}
}}
/>
<textarea
id="textarea4"
value={keyNumber4}
rows={1}
cols={TEXTAREASIZE}
ref={ref4}
style={{ resize: "none", fontSize: "16px" }}
onChange={(e) => {
let cursorPos = 0;
cursorPos = e.target.selectionStart;
changeTextarea(e, 4);
setTimeout(() => {
if (ref4.current) {
ref4.current.selectionStart = cursorPos;
ref4.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 4);
}}
onFocus={(e) => {
if (ref4.current) {
ref4.current.selectionStart = e.target.value.length;
ref4.current.selectionEnd = e.target.value.length;
}
}}
/>
<textarea
id="textarea5"
value={keyNumber5}
rows={1}
cols={TEXTAREASIZE}
ref={ref5}
style={{ resize: "none", fontSize: "16px" }}
onChange={(e) => {
let cursorPos = 0;
cursorPos = e.target.selectionStart;
changeTextarea(e, 5);
setTimeout(() => {
if (ref5.current) {
ref5.current.selectionStart = cursorPos;
ref5.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 5);
}}
onFocus={(e) => {
if (ref5.current) {
ref5.current.selectionStart = e.target.value.length;
ref5.current.selectionEnd = e.target.value.length;
}
}}
/>
{isPushActivateButton &&
keyNumber1.length +
keyNumber2.length +
keyNumber3.length +
keyNumber4.length +
keyNumber5.length !==
20 && (
<span className={styles.formError}>
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumberIncorrectError"
)
)}
</span>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input