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
44const 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% { transfor m: translateX(0); }
41+ 20% { transfor m: translateX(-8px); }
42+ 40% { transfor m: translateX(8px); }
43+ 60% { transfor m: translateX(-6px); }
44+ 80% { transfor m: translateX(6px); }
45+ ` ;
46+
3947const pinContainerStyles = css `
4048 dis play: 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+
5062const pinInputStyles = css `
51- width : clamp (2.1 rem , 7 vw , 3 rem );
52- height : clamp (2.9 rem , 9 vw , 4rem );
63+ width : clamp (2.35 rem , 8 vw , 3.2 rem );
64+ height : clamp (3 rem , 9.5 vw , 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.28 rem ;
71- height : 3.18 rem ;
72- font-size : 1.45 rem ;
93+ width : 2.65 rem ;
94+ height : 3.3 rem ;
95+ font-size : 1.55 rem ;
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 : 640 px ) {
105- width : 100 % ;
106- min-height : 3.2 rem ;
107- font-size : 1 rem ;
103+ & : focus {
104+ border-color : # d85f39 ;
105+ box-shadow : 0 0 0 4 px 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} ;
0 commit comments