Skip to content

Commit 275a2e7

Browse files
authored
Merge pull request #56 from kc3hack/taiseiue/six-disimal
番号入力フォームのおもてなし強化
2 parents d1fea40 + 2f47e5f commit 275a2e7

2 files changed

Lines changed: 82 additions & 51 deletions

File tree

apps/web/src/components/button/PinInputBlock.tsx

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { css } from "hono/css";
2-
import { useRef, useState } from "hono/jsx";
1+
import { css, keyframes } from "hono/css";
2+
import { useEffect, useRef, useState } from "hono/jsx";
33

44
const containerStyles = css`
55
background-color: #fff;
@@ -36,6 +36,14 @@ const titleStyles = css`
3636
margin: 0;
3737
`;
3838

39+
const shake = keyframes`
40+
0%, 100% { transform: translateX(0); }
41+
20% { transform: translateX(-8px); }
42+
40% { transform: translateX(8px); }
43+
60% { transform: translateX(-6px); }
44+
80% { transform: translateX(6px); }
45+
`;
46+
3947
const pinContainerStyles = css`
4048
display: flex;
4149
width: 100%;
@@ -47,17 +55,32 @@ const pinContainerStyles = css`
4755
}
4856
`;
4957

58+
const pinContainerShakeStyles = css`
59+
animation: ${shake} 0.35s ease-in-out;
60+
`;
61+
5062
const pinInputStyles = css`
51-
width: clamp(2.1rem, 7vw, 3rem);
52-
height: clamp(2.9rem, 9vw, 4rem);
63+
width: clamp(2.35rem, 8vw, 3.2rem);
64+
height: clamp(3rem, 9.5vw, 4rem);
5365
border: 3px solid #ccc;
5466
border-radius: 12px;
67+
box-sizing: border-box;
68+
padding: 0;
5569
text-align: center;
56-
font-size: clamp(1.4rem, 4.7vw, 2rem);
70+
line-height: 1;
71+
font-size: clamp(1.45rem, 4.9vw, 2rem);
5772
font-weight: 900;
5873
color: #333;
5974
background: #f9f9f9;
6075
transition: border-color 0.2s, box-shadow 0.2s;
76+
appearance: textfield;
77+
-moz-appearance: textfield;
78+
79+
&::-webkit-outer-spin-button,
80+
&::-webkit-inner-spin-button {
81+
-webkit-appearance: none;
82+
margin: 0;
83+
}
6184
6285
&:focus {
6386
outline: none;
@@ -67,44 +90,20 @@ const pinInputStyles = css`
6790
}
6891
6992
@media (max-width: 640px) {
70-
width: 2.28rem;
71-
height: 3.18rem;
72-
font-size: 1.45rem;
93+
width: 2.65rem;
94+
height: 3.3rem;
95+
font-size: 1.55rem;
7396
}
7497
`;
7598

76-
const submitButtonStyles = css`
77-
margin-top: 0.25rem;
78-
background: #f6ad49;
79-
color: white;
80-
border: none;
81-
width: min(100%, 15rem);
82-
min-height: 3rem;
83-
padding: 0.72rem 1rem;
84-
border-radius: 9999px;
85-
font-weight: 900;
86-
cursor: pointer;
87-
font-size: 1.02rem;
88-
transition: transform 0.1s ease, opacity 0.2s ease;
89-
90-
&:focus-visible {
91-
outline: 3px solid rgba(246, 173, 73, 0.32);
92-
outline-offset: 2px;
93-
}
94-
95-
&:active {
96-
transform: scale(0.97);
97-
}
98-
99-
&:disabled {
100-
opacity: 0.55;
101-
cursor: default;
102-
}
99+
const pinInputErrorStyles = css`
100+
border-color: #d85f39;
101+
background: #fff3ef;
103102
104-
@media (max-width: 640px) {
105-
width: 100%;
106-
min-height: 3.2rem;
107-
font-size: 1rem;
103+
&:focus {
104+
border-color: #d85f39;
105+
box-shadow: 0 0 0 4px rgba(216, 95, 57, 0.2);
106+
background: #fff;
108107
}
109108
`;
110109

@@ -137,11 +136,19 @@ type Props = {
137136
isSubmitting?: boolean;
138137
errorMessage?: string;
139138
helperMessage?: string;
139+
onAnyInput?: () => void;
140140
};
141141

142-
export const PinInputBlock = ({ onSubmit, isSubmitting = false, errorMessage = "", helperMessage = "" }: Props) => {
142+
export const PinInputBlock = ({
143+
onSubmit,
144+
isSubmitting = false,
145+
errorMessage = "",
146+
helperMessage = "",
147+
onAnyInput,
148+
}: Props) => {
143149
const [digits, setDigits] = useState<string[]>(Array.from({ length: PIN_INPUT_KEYS.length }, () => ""));
144150
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
151+
const lastAutoSubmittedPinRef = useRef<string | null>(null);
145152

146153
const pin = digits.join("");
147154
const canSubmit = pin.length === PIN_INPUT_KEYS.length && !isSubmitting;
@@ -168,6 +175,7 @@ export const PinInputBlock = ({ onSubmit, isSubmitting = false, errorMessage = "
168175
if (!(target instanceof HTMLInputElement)) {
169176
return;
170177
}
178+
onAnyInput?.();
171179

172180
const raw = target.value.replace(/\D/g, "");
173181
if (!raw) {
@@ -206,6 +214,7 @@ export const PinInputBlock = ({ onSubmit, isSubmitting = false, errorMessage = "
206214
if (!onlyDigits) {
207215
return;
208216
}
217+
onAnyInput?.();
209218

210219
const next = Array.from({ length: PIN_INPUT_KEYS.length }, (_, index) => onlyDigits[index] ?? "");
211220
setDigits(next);
@@ -219,19 +228,35 @@ export const PinInputBlock = ({ onSubmit, isSubmitting = false, errorMessage = "
219228
submitIfPossible();
220229
};
221230

231+
useEffect(() => {
232+
if (pin.length !== PIN_INPUT_KEYS.length) {
233+
lastAutoSubmittedPinRef.current = null;
234+
return;
235+
}
236+
237+
if (isSubmitting || lastAutoSubmittedPinRef.current === pin) {
238+
return;
239+
}
240+
241+
lastAutoSubmittedPinRef.current = pin;
242+
void Promise.resolve(onSubmit(pin));
243+
}, [pin, isSubmitting, onSubmit]);
244+
222245
return (
223246
<form action="" class={containerStyles} onSubmit={handleSubmit}>
224247
<p class={titleStyles}>6桁の番号を入力</p>
225248

226-
<div class={pinContainerStyles}>
249+
<div class={`${pinContainerStyles} ${errorMessage ? pinContainerShakeStyles : ""}`}>
227250
{PIN_INPUT_KEYS.map((key, index) => (
228251
<input
229252
key={key}
230253
name={key}
231-
type="text"
254+
type="number"
232255
inputMode="numeric"
256+
pattern="[0-9]*"
257+
autoComplete="one-time-code"
233258
maxLength={1}
234-
class={pinInputStyles}
259+
class={`${pinInputStyles} ${errorMessage ? pinInputErrorStyles : ""}`}
235260
value={digits[index]}
236261
onInput={(e) => handleInput(index, e)}
237262
onKeyDown={(e) => handleKeyDown(index, e)}
@@ -246,12 +271,11 @@ export const PinInputBlock = ({ onSubmit, isSubmitting = false, errorMessage = "
246271
))}
247272
</div>
248273

249-
<button type="submit" class={submitButtonStyles} disabled={!canSubmit}>
250-
{isSubmitting ? "確認中..." : "受信する"}
251-
</button>
252-
253-
{helperMessage && <p class={helperTextStyles}>{helperMessage}</p>}
254-
{errorMessage && <p class={errorStyles}>{errorMessage}</p>}
274+
{errorMessage ? (
275+
<p class={errorStyles}>{errorMessage}</p>
276+
) : (
277+
helperMessage && <p class={helperTextStyles}>{helperMessage}</p>
278+
)}
255279
</form>
256280
);
257281
};

apps/web/src/pages/r/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ export const ReceivePage = () => {
329329
const [isMoving, setIsMoving] = useState(false);
330330
const [isResolvingPin, setIsResolvingPin] = useState(false);
331331
const [pinError, setPinError] = useState("");
332+
const pinGuideMessage = "送信側で発行された6桁の番号を\n入力してください";
332333

333334
const findRoomIdByShortCode = async (shortcode: string) => {
334335
const validate = await apiClient.rooms[":shortcode"].$get({
@@ -362,7 +363,7 @@ export const ReceivePage = () => {
362363
window.location.href = `${window.location.origin}/r/${encodeURIComponent(roomId)}?source=code`;
363364
} catch (error) {
364365
console.error(error);
365-
setPinError("6桁の番号が見つかりませんでした。番号を再確認してください。");
366+
setPinError(pinGuideMessage);
366367
} finally {
367368
setIsResolvingPin(false);
368369
}
@@ -381,9 +382,15 @@ export const ReceivePage = () => {
381382

382383
<PinInputBlock
383384
onSubmit={handleSubmitPin}
385+
onAnyInput={() => {
386+
if (!pinError) {
387+
return;
388+
}
389+
setPinError("");
390+
}}
384391
isSubmitting={isResolvingPin}
385392
errorMessage={pinError}
386-
helperMessage={"送信側で発行された6桁の番号を\n入力してください"}
393+
helperMessage={pinGuideMessage}
387394
/>
388395

389396
<SendBackButton onClick={handleSendClick} />

0 commit comments

Comments
 (0)