소스 검색

Add Animation To SearchBar, Add CancelButton, Add Keyboard Shortcuts

Francesco Baccetti 4 년 전
부모
커밋
348e553541

+ 11 - 9
src/components/LayoutWithRouting.tsx

@@ -9,19 +9,21 @@ import routes from '@/config/routes'
 import { sizes } from '@/shared/theme'
 
 const LayoutWithRouting: React.FC = () => (
-  <MainContainer>
+  <>
     <GlobalStyle />
     <Router primary>
       <Navbar default />
     </Router>
-    <Router primary={false}>
-      <HomeView default />
-      <VideoView path={routes.video()} />
-      <SearchView path={routes.search()} />
-      <BrowseView path={routes.browse()} />
-      <ChannelView path={routes.channel()} />
-    </Router>
-  </MainContainer>
+    <MainContainer>
+      <Router primary={false}>
+        <HomeView default />
+        <VideoView path={routes.video()} />
+        <SearchView path={routes.search()} />
+        <BrowseView path={routes.browse()} />
+        <ChannelView path={routes.channel()} />
+      </Router>
+    </MainContainer>
+  </>
 )
 
 const MainContainer = styled.main`

+ 20 - 24
src/components/Navbar/Navbar.style.tsx

@@ -4,15 +4,9 @@ import { Searchbar, Icon } from '@/shared/components'
 import { colors, sizes } from '@/shared/theme'
 import { ReactComponent as UnstyledLogo } from '@/assets/logo.svg'
 
-export const Header = styled.header<{ isSearching: boolean }>`
-  display: grid;
-  grid-template-columns: ${(props) => (props.isSearching ? `134px 1fr 134px` : `repeat(3, 1fr)`)};
-  grid-template-areas: ${(props) => (props.isSearching ? `". searchbar cancel"` : `"navigation searchbar ."`)};
-  width: 100%;
-  padding: ${(props) => (props.isSearching ? `${sizes.b2}px` : `${sizes.b3}px ${sizes.b8}px`)};
-  border-bottom: 1px solid ${colors.gray[800]};
-  background-color: ${(props) => (props.isSearching ? colors.gray[900] : colors.black)};
-`
+type NavbarStyleProps = {
+  hasFocus: boolean
+}
 
 export const Logo = styled(UnstyledLogo)`
   width: ${sizes.b12}px;
@@ -21,7 +15,6 @@ export const Logo = styled(UnstyledLogo)`
 
 export const NavigationContainer = styled.div`
   display: flex;
-  grid-area: navigation;
   align-items: center;
   > * + * {
     margin-left: ${sizes.b6}px;
@@ -29,10 +22,13 @@ export const NavigationContainer = styled.div`
 `
 
 export const StyledSearchbar = styled(Searchbar)`
-  width: 100%;
-  grid-area: searchbar;
+  transition: width 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
+  will-change: width;
+`
+export const SearchbarContainer = styled.div`
+  display: flex;
+  justify-content: center;
 `
-
 export const StyledIcon = styled(Icon)`
   color: ${colors.gray[300]};
   &:hover {
@@ -41,16 +37,16 @@ export const StyledIcon = styled(Icon)`
   }
 `
 
-export const CancelButton = styled.div`
-  width: ${sizes.b12}px;
-  height: ${sizes.b12}px;
-  color: ${colors.white};
-  grid-area: cancel;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  justify-self: end;
-  :hover {
-    cursor: pointer;
+export const Header = styled.header<NavbarStyleProps>`
+  display: grid;
+  grid-template-columns: 1fr 3fr 1fr;
+  width: 100%;
+  padding: ${(props) => (props.hasFocus ? `${sizes.b2}px` : `${sizes.b3}px ${sizes.b8}px`)};
+  border-bottom: 1px solid ${colors.gray[800]};
+  background-color: ${(props) => (props.hasFocus ? colors.gray[900] : colors.black)};
+  transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
+
+  ${StyledSearchbar} {
+    width: ${(props) => (props.hasFocus ? '1156px' : '385px')};
   }
 `

+ 46 - 39
src/components/Navbar/Navbar.tsx

@@ -1,60 +1,67 @@
 import React, { useState } from 'react'
-import { navigate, Link, RouteComponentProps } from '@reach/router'
+import { RouteComponentProps, Link, navigate } from '@reach/router'
 
 import routes from '@/config/routes'
-import { Icon } from '@/shared/components'
-import { Header, NavigationContainer, StyledIcon, StyledSearchbar, CancelButton, Logo } from './Navbar.style'
+import { Header, NavigationContainer, StyledIcon, StyledSearchbar, SearchbarContainer, Logo } from './Navbar.style'
 
-const Navbar: React.FC<RouteComponentProps> = () => {
-  const [search, setSearch] = useState('')
-  const [isSearching, setIsSearching] = useState(false)
+type NavbarProps = RouteComponentProps
 
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    setSearch(e.currentTarget.value)
-  }
+const Navbar: React.FC<NavbarProps> = () => {
+  const [search, setSearch] = useState('')
+  const [isFocused, setIsFocused] = useState(false)
 
   const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    if (e.key === 'Enter' || (e.key === 'NumpadEnter' && search.trim() !== '')) {
+    if ((e.key === 'Enter' || e.key === 'NumpadEnter') && search.trim()) {
       navigate(routes.search(search))
     }
+    if (e.key === 'Escape' || e.key === 'Esc') {
+      setIsFocused(false)
+      setSearch('')
+      e.currentTarget.blur()
+    }
+  }
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setIsFocused(true)
+    setSearch(e.currentTarget.value)
   }
 
   const handleFocus = () => {
-    setIsSearching(true)
+    setIsFocused(true)
   }
 
   const handleCancel = () => {
     setSearch('')
-    setIsSearching(false)
+    setIsFocused(false)
   }
   return (
-    <Header isSearching={isSearching}>
-      {!isSearching && (
-        <NavigationContainer>
-          <Link to="/">
-            <Logo />
-          </Link>
-          <Link to="/">
-            <StyledIcon name="home" />
-          </Link>
-          <Link to={routes.browse()}>
-            <StyledIcon name="binocular" />
-          </Link>
-        </NavigationContainer>
-      )}
-
-      <StyledSearchbar
-        placeholder="Search..."
-        onChange={handleChange}
-        value={search}
-        onKeyPress={handleKeyPress}
-        onFocus={handleFocus}
-      />
-      {isSearching && (
-        <CancelButton onClick={handleCancel}>
-          <Icon name="times" />
-        </CancelButton>
-      )}
+    <Header hasFocus={isFocused}>
+      <div>
+        {!isFocused && (
+          <NavigationContainer>
+            <Link to="/">
+              <Logo />
+            </Link>
+            <Link to="/">
+              <StyledIcon name="home" />
+            </Link>
+            <Link to={routes.browse()}>
+              <StyledIcon name="binocular" />
+            </Link>
+          </NavigationContainer>
+        )}
+      </div>
+      <SearchbarContainer>
+        <StyledSearchbar
+          placeholder="Search..."
+          onChange={handleChange}
+          value={search}
+          onKeyDown={handleKeyPress}
+          onFocus={handleFocus}
+          onCancel={handleCancel}
+          controlled
+        />
+      </SearchbarContainer>
     </Header>
   )
 }

+ 31 - 2
src/shared/components/Searchbar/Searchbar.style.tsx

@@ -1,17 +1,46 @@
 import styled from '@emotion/styled'
 import { colors, sizes } from '../../theme'
+import Button from '../Button'
 
 export const Input = styled.input`
+  width: 100%;
   border: unset;
   padding: 14px ${sizes.b3}px;
   height: ${sizes.b12}px;
   background-color: ${colors.gray[800]};
   color: ${colors.white};
-  &::placeholder {
+  ::placeholder {
     color: ${colors.gray[400]};
   }
-  &:focus {
+  :focus {
     background-color: ${colors.gray[900]};
     outline: 1px solid ${colors.gray[500]};
   }
+  &::-webkit-search-cancel-button {
+    -webkit-appearance: none;
+  }
+`
+
+export const CancelButton = styled(Button)`
+  position: absolute;
+  right: 0;
+  border: none;
+  padding: 14px ${sizes.b3}px;
+  color: ${colors.white};
+  :focus,
+  :hover {
+    color: ${colors.white};
+  }
+  > svg {
+    width: 100%;
+    max-width: 17px;
+    max-height: 17px;
+  }
+`
+
+export const Container = styled.div`
+  position: relative;
+  display: flex;
+  align-items: center;
+  height: ${sizes.b12}px;
 `

+ 42 - 14
src/shared/components/Searchbar/Searchbar.tsx

@@ -1,29 +1,57 @@
-import React from 'react'
-import { Input } from './Searchbar.style'
+import React, { useState } from 'react'
+import { Input, CancelButton, Container } from './Searchbar.style'
 
 type SearchbarProps = {
   value: string
+  onCancel?: () => void
+  controlled?: boolean
 } & React.DetailedHTMLProps<React.HTMLAttributes<HTMLInputElement>, HTMLInputElement>
 const Searchbar: React.FC<SearchbarProps> = ({
   placeholder,
   onChange,
   onFocus,
-  value,
+  onCancel,
+  controlled = false,
+  value: externalValue,
   onBlur,
   onSubmit,
-  ...hmtlProps
+  ...htmlProps
 }) => {
+  const [value, setValue] = useState('')
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (onChange) {
+      onChange(e)
+    }
+    if (!controlled) {
+      setValue(e.currentTarget.value)
+    }
+  }
+  const handleCancel = () => {
+    if (onCancel) {
+      onCancel()
+    }
+    if (!controlled) {
+      setValue('')
+    }
+  }
+
+  const hasValue = value !== '' || externalValue !== ''
   return (
-    <Input
-      value={value}
-      placeholder={placeholder}
-      type="search"
-      onChange={onChange}
-      onFocus={onFocus}
-      onBlur={onBlur}
-      onSubmit={onSubmit}
-      {...hmtlProps}
-    />
+    <Container>
+      <Input
+        value={controlled ? externalValue : value}
+        placeholder={placeholder}
+        type="search"
+        onChange={handleChange}
+        onFocus={onFocus}
+        onFocusCapture={onFocus}
+        onBlur={onBlur}
+        onSubmit={onSubmit}
+        {...htmlProps}
+      />
+      {hasValue && <CancelButton onClick={handleCancel} variant="tertiary" icon="times" size="smaller" />}
+    </Container>
   )
 }
 export default Searchbar