반응형
mui 라이브러리를 사용하지 않고 처음부터 끝까지 AutoComplete 를 구현했다.
생각보다 신경쓸게 많았다.
제일 빡셌던건 방향키로 목록을 탐색할 때 스크롤을 자연스럽게 움직이게 하는 것이었다.
처음엔 자바스크립트 기본 함수인 scrollIntoView() 를 사용해볼까 했는데 내가 생각하는거랑 전혀 다르게 움직여서 그것까지 직접 구현했다.
(그래서 코드가 좀 더러움;)



AutoComplete.tsx
import { useState, useRef, useEffect } from "react"; import { getLocalStorageValue } from "./utils/localstorage.util"; import styles from "./AutoComplete.module.css"; import { Button } from "..."; //버튼은 기존에 쓰던게 있었음 interface AutoCompleteProps { placeholder: string; storageKey: string; handleSearch?: Function; onSelected?: any; onChange?: any; inputRef?: any; } // ipnut에서 엔터가 자동완성선택 / 검색 이 있기 때문에 구분을 하기 위한 변수 // 키보드 방향키로 움직였을 때에는 false 로 했다가 엔터를 누르면 true 로 변경 let enterKeyFor: "search" | "autoComplete" = "search"; const AutoComplete = (props: AutoCompleteProps) => { let storagedSuggestions: string[] = getLocalStorageValue(props.storageKey) || []; const [suggestions, setSuggestions] = useState<string[]>(storagedSuggestions); const [showSuggstions, setShowSuggstions] = useState(false); const [selectedVal, setSelectedVal] = useState(""); const [activeSuggestionIdx, setActiveSuggestionIdx] = useState(-1); const [showCloseButton, setShowCloseButton] = useState(false); const suggestionRef = useRef<HTMLDivElement>(null); const handleKeyUp = (e: any) => { setSuggestions(storagedSuggestions.filter(i => i.startsWith(e.target.value.toUpperCase()))); }; const handleChange = (e: any) => { const input = e.target.value; setActiveSuggestionIdx(-1); setShowSuggstions(true); setSelectedVal(input); props.onChange(input); }; /** * 로컬스토리지 값 가져오기 */ const refreshStoragedSuggestions = () => { storagedSuggestions = getLocalStorageValue(props.storageKey) || []; setSuggestions(storagedSuggestions.filter(i => i.startsWith(selectedVal.toUpperCase()))); }; /** * x 버튼 설정 */ useEffect(() => { if (!selectedVal) { setShowCloseButton(false); } else { setShowCloseButton(true); } }, [selectedVal]); /** * 스크롤 관련 */ useEffect(() => { //선택중인 suggestion const currentChildren = suggestionRef.current?.children[activeSuggestionIdx]; const currentChildrenTop = currentChildren?.getBoundingClientRect().top || 0; const currentChildrenBottom = currentChildren?.getBoundingClientRect().bottom || 0; // suggestions 컨테이너 const containerTop = suggestionRef.current?.getBoundingClientRect().top || 0; const containerBottom = suggestionRef.current?.getBoundingClientRect().bottom || 0; const scrollTop = suggestionRef.current?.scrollTop || 0; if (currentChildrenTop < containerTop) { suggestionRef.current?.scroll({ top: scrollTop - (containerTop - currentChildrenTop), }); } else if (currentChildrenBottom > containerBottom) { suggestionRef.current?.scroll({ top: scrollTop + (currentChildrenBottom - containerBottom), }); } }, [activeSuggestionIdx]); /** * 마우스 클릭 / 엔터키로으로 값 셋팅 */ const setSuggestion = (value: string) => { props.onSelected(value); setSelectedVal(value); setShowSuggstions(false); setActiveSuggestionIdx(-1); }; /** * 방향키, 엔터키 관련 */ const onKeyDown = (e: any) => { //엔터키 if (e.keyCode === 13) { // 검색을 위한 엔터인 경우 if (enterKeyFor === "search") { props.handleSearch!(); addLocalStorageListValue( props.storageKey, selectedVal?.trim().toUpperCase(), true, true, ); setShowSuggstions(false); } // 자동완성을 위한 엔터인 경우 else if (enterKeyFor === "autoComplete") { setSuggestion(suggestions[activeSuggestionIdx]); enterKeyFor = "search"; } } //방향키 (위) else if (e.keyCode === 38) { enterKeyFor = "autoComplete"; if (activeSuggestionIdx === 0) { setActiveSuggestionIdx(suggestions.length - 1); } else { setActiveSuggestionIdx(activeSuggestionIdx - 1); } } // 방향키 (아래) else if (e.keyCode === 40) { enterKeyFor = "autoComplete"; if (activeSuggestionIdx === suggestions.length - 1) { setActiveSuggestionIdx(0); } else { setActiveSuggestionIdx(activeSuggestionIdx + 1); } } // 기타 다른 키 else { enterKeyFor = "search"; } }; /** * 마우스 동작 */ const handleMouseOver = (idx: number) => { setActiveSuggestionIdx(idx); }; return ( <div className={styles.sugesstionAuto}> <div className={styles.formControlAuto}> <input placeholder={props.placeholder} type="search" value={selectedVal || ''} ref={props.inputRef} onChange={handleChange} onKeyUp={handleKeyUp} onKeyDown={onKeyDown} onFocus={() => { refreshStoragedSuggestions(); setShowSuggstions(true); }} onBlur={() => { setTimeout(() => { setShowSuggstions(false); }, 200); }} /> {showCloseButton && ( <Button onClick={() => { setSuggestion(""); }} display="icon" iconName="close" variant="clear" color="grey" size={20} /> )} </div> {showSuggstions && suggestions.length > 0 && ( <div className={styles.suggestions} style={{ display: showSuggstions ? "block" : "none" }} ref={suggestionRef} > {suggestions.map((item, idx) => ( <div className={idx === activeSuggestionIdx ? styles.active : ""} key={"" + item + idx} onMouseOver={() => handleMouseOver(idx)} onKeyDown={onKeyDown} onClick={() => setSuggestion(item)} > {item} </div> ))} </div> )} </div> ); }; export default AutoComplete;
AutoComplete.module.css
.sugesstionAuto { display: block; position: relative; width: 184px; padding: 1px 2px 1px 8px; } .formControlAuto { display: flex; justify-content: center; align-items: center; justify-content: space-between; padding-left: 5px; } .formControlAuto input { border: 0; width: 100%; /* padding: 15px 20px; */ outline: none; background-color: transparent !important } .formControlAuto label { font-size: 10px; text-transform: uppercase; color: #949494; padding: 10px 0px 10px 20px; } .suggestions { position: absolute; top: 31px; left: -1px; z-index: 999; cursor: pointer; width: 222px; max-height: 200px; overflow: auto; border: 1px solid #dde0e2; background-color: white; } .suggestions > div { padding: 10px; } .active{ background-color: #dbdbdb !important; color: #077110 !important; } /* .suggestions > div, .formControlAuto input { color: #ffffff; } */
localstorage.util.ts
const getLocalStorageValue = (key: string) => { const value = localStorage.getItem(key); if (value && isJSONObject(value)) { return JSON.parse(value); } else { return value; } }; /** * 값 덮어씌우기 */ const setLocalStorage = (key: string, value: string) => { localStorage.setItem(key, value); }; /** * list 형태에 값 추가하기. */ const addLocalStorageListValue = ( key: string, value: any, sorted: boolean = false, noDuplication: boolean = false, ) => { const oldValueStr = localStorage.getItem(key); //값이 없으면 리스트 생성 후 저장 if (!oldValueStr) { const newValueObj = [value]; localStorage.setItem(key, JSON.stringify(newValueObj)); return newValueObj; } else if (!isJSONObject(oldValueStr)) return; //JSON이 아니면 아무동작 하지 않음 else { const oldValueObj = JSON.parse(oldValueStr); if (Array.isArray(oldValueObj)) { let newValueObj: string[] = []; //중복 제거 newValueObj = noDuplication && oldValueObj.indexOf(value) > -1 ? [...oldValueObj] : [...oldValueObj, value]; //sorting if (sorted) { newValueObj.sort(); } localStorage.setItem(key, JSON.stringify(newValueObj)); return newValueObj; } } }; const isJSONObject = (jsonString: string) => { try { var o = JSON.parse(jsonString); if (o && typeof o === "object") { return true; } } catch (e) {} return false; }; export { getLocalStorageValue, setLocalStorage, addLocalStorageListValue };
const STORAGE_KEY = "quickSearch"; const SearchField = () => { const searchValueRef = useRef<HTMLInputElement>(null); const [searchValue, setSearchValue] = useState(""); const getSelectedVal = (value: string) => { setSearchValue(value); }; const getChanges = (value: string) => { setSearchValue(value); }; const search = () => { //검색 구현 } return ( <Autocomplete placeholder="type any word..." onSelected={getSelectedVal} onChange={getChanges} handleSearch={search} storageKey={STORAGE_KEY} inputRef={searchValueRef} /> <Button onClick={() => search()} /> ) } export default SearchField
반응형
'프론트엔드 > 리액트 | React' 카테고리의 다른 글
[React] html2canvas, jspdf 로 pdf 다운로드 시 s3 이미지가 안보임 - 해결 (0) | 2023.11.24 |
---|---|
axios query param, request param (0) | 2023.08.20 |
리액트 웹앱 - 상단 타이틀 동적으로 적용하기 (redux 활용) (0) | 2022.04.13 |
리액트 하단바 만들기 - React Bottom Navigation (0) | 2022.04.13 |
리액트 Font Awesome 아이콘 적용하기 (0) | 2022.04.12 |