From 20f77cef0b1b622c48ce70fce624ce225fee7f61 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 5 Sep 2024 13:45:04 +0530 Subject: [PATCH 1/5] ui:Added dropdown for mnemonic phrase input --- src/components/MnemonicPhraseInput.module.css | 4 + src/components/MnemonicPhraseInput.tsx | 90 +++++++++++++++++-- 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/components/MnemonicPhraseInput.module.css diff --git a/src/components/MnemonicPhraseInput.module.css b/src/components/MnemonicPhraseInput.module.css new file mode 100644 index 00000000..8e6b9a47 --- /dev/null +++ b/src/components/MnemonicPhraseInput.module.css @@ -0,0 +1,4 @@ +.dropdownMenu { + min-width: 100% !important; + max-height: 12.7rem; +} diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index c3ae4d73..9e971cc9 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -1,5 +1,9 @@ import { useEffect, useRef, useState } from 'react' import { Bip39MnemonicWordInput } from './MnemonicWordInput' +import { MNEMONIC_WORDS } from '../constants/bip39words' +import * as rb from 'react-bootstrap' +import { forwardRef } from 'react' +import style from './MnemonicPhraseInput.module.css' interface MnemonicPhraseInputProps { columns?: number @@ -9,6 +13,26 @@ interface MnemonicPhraseInputProps { onChange: (value: MnemonicPhrase) => void } +interface MnemonicDropdownProps { + show: boolean + words: string[] + onSelect: (word: string) => void +} + +const MnemonicDropdown = forwardRef(({ show, words, onSelect }, ref) => ( + + + {words.map((word) => ( +
+ onSelect(word)} className="p-2"> + {word} + +
+ ))} +
+
+)) + export default function MnemonicPhraseInput({ columns = 3, mnemonicPhrase, @@ -18,6 +42,9 @@ export default function MnemonicPhraseInput({ }: MnemonicPhraseInputProps) { const [activeIndex, setActiveIndex] = useState(0) const inputRefs = useRef([]) + const [showDropdown, setShowDropdown] = useState(null) + const [filteredWords, setFilteredWords] = useState(undefined) + const dropdownRef = useRef(null) useEffect(() => { if (activeIndex < mnemonicPhrase.length && isValid && isValid(activeIndex)) { @@ -30,6 +57,43 @@ export default function MnemonicPhraseInput({ } }, [mnemonicPhrase, activeIndex, isValid]) + const handleInputChange = (value: string, index: number) => { + const newPhrase = mnemonicPhrase.map((old, i) => (i === index ? value : old)) + onChange(newPhrase) + handleDropdownValue(value, index) + } + + const handleDropdownValue = (value: string, index: number) => { + const matched = value ? MNEMONIC_WORDS.filter((word) => word.startsWith(value)) : [] + if (matched.length > 0) { + setShowDropdown(index) + setFilteredWords(matched) + } else { + setShowDropdown(null) + setFilteredWords(undefined) + } + } + + const handleSelectWord = (word: string, index: number) => { + const newPhrase = mnemonicPhrase.map((old, i) => (i === index ? word : old)) + onChange(newPhrase) + setShowDropdown(null) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + if (filteredWords && filteredWords.length > 0 && dropdownRef.current) { + const firstItem = dropdownRef.current.querySelector('.dropdown-item') + if (firstItem) { + ;(firstItem as HTMLElement).focus() + } + } + } else if (e.key === 'Enter') { + setShowDropdown(null) + } + } + return (
{mnemonicPhrase.map((_, outerIndex) => { @@ -38,7 +102,13 @@ export default function MnemonicPhraseInput({ const wordGroup = mnemonicPhrase.slice(outerIndex, Math.min(outerIndex + columns, mnemonicPhrase.length)) return ( -
+
{ + handleKeyDown(e) + }} + > {wordGroup.map((givenWord, innerIndex) => { const wordIndex = outerIndex + innerIndex const isCurrentActive = wordIndex === activeIndex @@ -48,15 +118,23 @@ export default function MnemonicPhraseInput({ forwardRef={(el: HTMLInputElement) => (inputRefs.current[wordIndex] = el)} index={wordIndex} value={givenWord} - setValue={(value, i) => { - const newPhrase = mnemonicPhrase.map((old, index) => (index === i ? value : old)) - onChange(newPhrase) - }} + setValue={(value) => handleInputChange(value, wordIndex)} isValid={isValid ? isValid(wordIndex) : undefined} disabled={isDisabled ? isDisabled(wordIndex) : undefined} - onFocus={() => setActiveIndex(wordIndex)} + onFocus={() => { + setActiveIndex(wordIndex) + handleDropdownValue(givenWord, wordIndex) + }} autoFocus={isCurrentActive} /> + {filteredWords && ( + handleSelectWord(word, wordIndex)} + /> + )}
) })} From 994f6ed3c8375e517cbf3f8ecaaaca60fde5f6ae Mon Sep 17 00:00:00 2001 From: amitx13 Date: Fri, 6 Sep 2024 03:13:39 +0530 Subject: [PATCH 2/5] minor improvement --- src/components/MnemonicPhraseInput.tsx | 108 +++++++++++++------------ 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index 9e971cc9..a614f6c2 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Bip39MnemonicWordInput } from './MnemonicWordInput' import { MNEMONIC_WORDS } from '../constants/bip39words' import * as rb from 'react-bootstrap' @@ -23,8 +23,8 @@ const MnemonicDropdown = forwardRef(({ sh {words.map((word) => ( -
- onSelect(word)} className="p-2"> +
+ onSelect(word)} className="p-2"> {word}
@@ -43,56 +43,64 @@ export default function MnemonicPhraseInput({ const [activeIndex, setActiveIndex] = useState(0) const inputRefs = useRef([]) const [showDropdown, setShowDropdown] = useState(null) - const [filteredWords, setFilteredWords] = useState(undefined) + const [filteredWords, setFilteredWords] = useState([]) const dropdownRef = useRef(null) + const focusNextInput = useCallback((index: number) => { + inputRefs.current[index]?.focus() + }, []) + useEffect(() => { if (activeIndex < mnemonicPhrase.length && isValid && isValid(activeIndex)) { const nextIndex = activeIndex + 1 setActiveIndex(nextIndex) - - if (inputRefs.current[nextIndex]) { - inputRefs.current[nextIndex].focus() - } + setShowDropdown(null) + focusNextInput(nextIndex) } - }, [mnemonicPhrase, activeIndex, isValid]) + }, [mnemonicPhrase, activeIndex, isValid, focusNextInput]) - const handleInputChange = (value: string, index: number) => { - const newPhrase = mnemonicPhrase.map((old, i) => (i === index ? value : old)) - onChange(newPhrase) - handleDropdownValue(value, index) - } - - const handleDropdownValue = (value: string, index: number) => { + const updateFilteredWords = useCallback((value: string, index: number) => { const matched = value ? MNEMONIC_WORDS.filter((word) => word.startsWith(value)) : [] - if (matched.length > 0) { - setShowDropdown(index) - setFilteredWords(matched) - } else { - setShowDropdown(null) - setFilteredWords(undefined) - } - } + setShowDropdown(matched.length > 0 ? index : null) + setFilteredWords(matched) + }, []) - const handleSelectWord = (word: string, index: number) => { - const newPhrase = mnemonicPhrase.map((old, i) => (i === index ? word : old)) - onChange(newPhrase) - setShowDropdown(null) - } + const handleInputChange = useCallback( + (value: string, index: number, selectWordFromDropdown = false) => { + const newPhrase = mnemonicPhrase.map((word, i) => (i === index ? value : word)) + onChange(newPhrase) + if (selectWordFromDropdown) { + setShowDropdown(null) + if (!isValid) { + focusNextInput(index + 1) + } + } else { + updateFilteredWords(value, index) + } + }, + [mnemonicPhrase, onChange, isValid, focusNextInput, updateFilteredWords], + ) - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault() - if (filteredWords && filteredWords.length > 0 && dropdownRef.current) { - const firstItem = dropdownRef.current.querySelector('.dropdown-item') - if (firstItem) { - ;(firstItem as HTMLElement).focus() + const handleKeyDown = useCallback( + (e: React.KeyboardEvent, value: string, wordIndex: number) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + if (filteredWords.length > 0 && dropdownRef.current) { + const firstItem = dropdownRef.current.querySelector('.dropdown-item') + if (firstItem) { + ;(firstItem as HTMLElement).focus() + } + } + } else if (e.key === 'Enter') { + const matched = MNEMONIC_WORDS.filter((word) => word.startsWith(value)) + if (matched.length === 1) { + handleInputChange(matched[0], wordIndex, true) + e.preventDefault() } } - } else if (e.key === 'Enter') { - setShowDropdown(null) - } - } + }, + [filteredWords, handleInputChange], + ) return (
@@ -102,18 +110,18 @@ export default function MnemonicPhraseInput({ const wordGroup = mnemonicPhrase.slice(outerIndex, Math.min(outerIndex + columns, mnemonicPhrase.length)) return ( -
{ - handleKeyDown(e) - }} - > +
{wordGroup.map((givenWord, innerIndex) => { const wordIndex = outerIndex + innerIndex const isCurrentActive = wordIndex === activeIndex return ( -
+
{ + handleKeyDown(e, givenWord, wordIndex) + }} + > (inputRefs.current[wordIndex] = el)} index={wordIndex} @@ -123,7 +131,7 @@ export default function MnemonicPhraseInput({ disabled={isDisabled ? isDisabled(wordIndex) : undefined} onFocus={() => { setActiveIndex(wordIndex) - handleDropdownValue(givenWord, wordIndex) + updateFilteredWords(givenWord, wordIndex) }} autoFocus={isCurrentActive} /> @@ -132,7 +140,7 @@ export default function MnemonicPhraseInput({ ref={dropdownRef} show={showDropdown === wordIndex} words={filteredWords} - onSelect={(word) => handleSelectWord(word, wordIndex)} + onSelect={(word) => handleInputChange(word, wordIndex, true)} /> )}
From dde55148f5c9ab7de15b33be5e2977f9f9adce06 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 10 Sep 2024 15:35:13 +0530 Subject: [PATCH 3/5] fix: skip dropdown focus on tab when word is fully typed --- src/components/MnemonicPhraseInput.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index a614f6c2..7bfda540 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -69,13 +69,11 @@ export default function MnemonicPhraseInput({ (value: string, index: number, selectWordFromDropdown = false) => { const newPhrase = mnemonicPhrase.map((word, i) => (i === index ? value : word)) onChange(newPhrase) + updateFilteredWords(value, index) if (selectWordFromDropdown) { - setShowDropdown(null) if (!isValid) { focusNextInput(index + 1) } - } else { - updateFilteredWords(value, index) } }, [mnemonicPhrase, onChange, isValid, focusNextInput, updateFilteredWords], @@ -94,12 +92,18 @@ export default function MnemonicPhraseInput({ } else if (e.key === 'Enter') { const matched = MNEMONIC_WORDS.filter((word) => word.startsWith(value)) if (matched.length === 1) { + e.preventDefault() handleInputChange(matched[0], wordIndex, true) + } + } else if (e.key === 'Tab') { + const matched = MNEMONIC_WORDS.filter((word) => word.startsWith(value)) + if (matched.length === 1 && value === matched[0]) { e.preventDefault() + focusNextInput(wordIndex + 1) } } }, - [filteredWords, handleInputChange], + [filteredWords, handleInputChange, focusNextInput], ) return ( @@ -131,7 +135,7 @@ export default function MnemonicPhraseInput({ disabled={isDisabled ? isDisabled(wordIndex) : undefined} onFocus={() => { setActiveIndex(wordIndex) - updateFilteredWords(givenWord, wordIndex) + givenWord && updateFilteredWords(givenWord, wordIndex) }} autoFocus={isCurrentActive} /> From eb040fc9eaf990fff48cc1504acb7ba6076731bd Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 10 Sep 2024 15:47:11 +0530 Subject: [PATCH 4/5] fix: adjust dropdown behavior on selection --- src/components/MnemonicPhraseInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index 7bfda540..f12dc930 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -71,6 +71,7 @@ export default function MnemonicPhraseInput({ onChange(newPhrase) updateFilteredWords(value, index) if (selectWordFromDropdown) { + setShowDropdown(null) if (!isValid) { focusNextInput(index + 1) } From 27410d9aff85d1cf8c27be0e7042a18198c9224e Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 11 Sep 2024 16:26:37 +0530 Subject: [PATCH 5/5] Added: On pressing Tab after writing a full-word dropdown disappear --- src/components/MnemonicPhraseInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index f12dc930..c0357c58 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -100,6 +100,7 @@ export default function MnemonicPhraseInput({ const matched = MNEMONIC_WORDS.filter((word) => word.startsWith(value)) if (matched.length === 1 && value === matched[0]) { e.preventDefault() + setShowDropdown(null) focusNextInput(wordIndex + 1) } }