해당 포스팅은 이전 포스팅과 이어집니다.
링크
오늘 포스팅 내용은 제목 그대로 검색어 자동완성된 데이터를 키보드로 접근해서 선택하는 기능이다.
-> 지난 포스팅 결과물에서 시작을 하겠다.
우선 첫번째로 useRef hooks를 이용해야한다.
useRef에 대하여 간단히 소개하자면 일반 자바스크립트에서 getElementById, querySelector 같은 DOM Selector 함수를 사용해서 DOM 을 선택하는데 react에서는 useRef hook으로 dom을 접근할 수있다.
useRef에 관한 추가적인 설명은 링크
그렇다면 키보드 컨트롤을 하는데 왜 Dom을 접근해야 하는가? 사진을 확인해보자
해당 사진은 우리의 자동완성 검색어 html 형태이다.
div -> ul -> li 로 접근해야만.우리의 자동완성 키워드에 도달할 수 있다.
생략 코드는 이전 포스팅 최종 코드에서 확인하실 수 있습니다.
function Header() {
...생략
const autoRef = useRef<HTMLUListElement>(null);
}
console.log(autoRef);
return (
...생략
<AutoSearchWrap ref={autoRef}>
{keyItems.map((search, idx) => (
<AutoSearchData
isFocus={index === idx ? true : false}
key={search.city}
onClick={() => {
setKeyword(search.city);
}}
)
styled-component로 선언한 ulTag에 ref값을 props로 넘겨주었다.
또한 콘솔을 확인해 보면
사진과 같은 속성을 확인할 수 있다.
우리가 봐야할 속성은 표시한 children이다.
children 속성에는 아까위에서 설명한 li tag리스트들이 존재한다.
즉. ref로 children에 접근하여 우리는 div tag를 클릭하지 않고도 li tag에 있는 값에 접근하면 끝난다.
function Header() {
const AutoSearchData = styled.li<{isFocus?: boolean}>`
padding: 10px 8px;
width: 100%;
font-size: 14px;
font-weight: bold;
z-index: 4;
letter-spacing: 2px;
&:hover {
background-color: #edf5f5;
cursor: pointer;
}
background-color: ${props => props.isFocus? "#edf5f5" : "#fff"};
position: relative;
img {
position: absolute;
right: 5px;
width: 18px;
top: 50%;
transform: translateY(-50%);
}
`;
const [index,setIndex] = useState<number>(-1);
return (
<AutoSearchWrap ref={autoRef}>
{keyItems.map((search, idx) => (
<AutoSearchData
isFocus={index === idx ? true : false}
key={search.city}
onClick={() => {
setKeyword(search.city);
}}
)}
해당 코드를 살펴보자 유의깊게 봐야할 부분은 index 변수의 초기값이 -1인 점인데
우리가 자동완성 데이터를 받아오면 해당 배열에 첫번째 인덱스값이 0으로 시작하기때문에 -1값으로 지정해줬다.
그리고 AutoSearchData 컴포넌트에 isFocus라는 props를 전달하여 키보드로도 자동완성 키워드 hover 스타일처럼 만들고자 index값과 ES6 map 인자에 인덱스를 받아와 true,false를 반환해줄 수 있게되었다.
const ArrowDown = "ArrowDown";
const ArrowUp = "ArrowUp";
const Escape = "Escape";
const handleKeyArrow = (e:React.KeyboardEvent) => {
if (keyItems.length > 0) {
switch (e.key) {
case ArrowDown: //키보드 아래 키
setIndex(index + 1);
if (autoRef.current?.childElementCount === index + 1) setIndex(0);
break;
case ArrowUp: //키보드 위에 키
setIndex(index - 1);
if (index <= 0) {
setKeyItems([]);
setIndex(-1);
}
break;
case Escape: // esc key를 눌렀을때,
setKeyItems([]);
setIndex(-1);
break;
}
}
}
return (
<Search
value={keyword}
onChange={onChangeData}
onKeyDown={handleKeyArrow}
/>
...생략
)
우선 key가 눌리기때문에 event속성을 사용해야한다.
위 방향키 "ArrowDown" 아래 방향키 "ArrowUp"를 의미한다
즉 e.key === "ArrowDown" 케이스는 아래방향키를 눌렀을때 실행된다.
e.key === '문자열'은 오타와 실수방지를 위하여 상수처리해줬다.
if (autoRef.current?.childElementCount === index + 1) setIndex(0); 이부분은 만약 마지막 인덱스 키워드에서 또 아래 방향키를 누르면 맨처음 인덱스 키워드로 돌아가라는 의미이다.
Ex: childElementCount-> li tag의 개수를 의미한다.
import React, { useRef } from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import styled from 'styled-components';
const SearchContainer = styled.div`
width: 400px;
height: 45px;
position: relative;
border: 0;
img {
position: absolute;
right: 10px;
top: 10px;
}
`;
const Search = styled.input`
border: 0;
padding-left: 10px;
background-color: #eaeaea;
width: 100%;
height: 100%;
outline: none;
`;
const AutoSearchContainer = styled.div`
z-index: 3;
height: 50vh;
width: 400px;
background-color: #fff;
position: absolute;
top: 45px;
border: 2px solid;
padding: 15px;
`;
const AutoSearchWrap = styled.ul`
`;
const AutoSearchData = styled.li<{isFocus?: boolean}>`
padding: 10px 8px;
width: 100%;
font-size: 14px;
font-weight: bold;
z-index: 4;
letter-spacing: 2px;
&:hover {
background-color: #edf5f5;
cursor: pointer;
}
background-color: ${props => props.isFocus? "#edf5f5" : "#fff"};
position: relative;
img {
position: absolute;
right: 5px;
width: 18px;
top: 50%;
transform: translateY(-50%);
}
`;
interface autoDatas {
city: string;
growth_from_2000_to_2013: string;
latitude:number;
longitude:number;
population:string;
rank:string;
state:string;
}
function Header() {
const [keyword, setKeyword] = useState<string>("");
const [index,setIndex] = useState<number>(-1);
const [keyItems, setKeyItems] = useState<autoDatas[]>([]);
const autoRef = useRef<HTMLUListElement>(null);
const onChangeData = (e:React.FormEvent<HTMLInputElement>) => {
setKeyword(e.currentTarget.value);
};
const handleKeyArrow = (e:React.KeyboardEvent) => {
if (keyItems.length > 0) {
switch (e.key) {
case ArrowDown:
setIndex(index + 1);
if (autoRef.current?.childElementCount === index + 1) setIndex(0);
break;
case ArrowUp:
setIndex(index - 1);
if (index <= 0) {
setKeyItems([]);
setIndex(-1);
}
break;
case Escape:
setKeyItems([]);
setIndex(-1);
break;
}
}
}
const fetchData = () =>{
return fetch(
`https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json`
)
.then((res) => res.json())
.then((data) => data.slice(0,100))
}
interface ICity {
includes(data:string): boolean;
city?: any;
}
const updateData = async() => {
const res = await fetchData();
let b = res.filter((list: ICity) => list.city.includes(keyword) === true)
.slice(0,10);
// console.log(b);
setKeyItems(b);
}
useEffect(() => {
updateData();
},[keyword])
return (
<SearchContainer>
<Search value={keyword} onChange={onChangeData} onKeyDown={handleKeyArrow}/>
<img src="assets/imgs/search.svg" alt="searchIcon" />
{keyItems.length > 0 && keyword && (
<AutoSearchContainer>
<AutoSearchWrap ref={autoRef}>
{keyItems.map((search, idx) => (
<AutoSearchData
isFocus={index === idx ? true : false}
key={search.city}
onClick={() => {
setKeyword(search.city);
}}
>
<a href="#">{search.city}</a>
<img src="assets/imgs/north_west.svg" alt="arrowIcon" />
</AutoSearchData>
))}
</AutoSearchWrap>
</AutoSearchContainer>
)}
</SearchContainer>
);
}
export default Header;
감사합니다
낄낄