mirror of
https://github.com/uetchy/namae.git
synced 2025-07-01 22:10:04 +09:00
chore: add animation
This commit is contained in:
parent
769548ccc9
commit
6f87b7fc5b
@ -4,24 +4,29 @@ import {useTranslation} from 'react-i18next';
|
|||||||
import {Link, useHistory} from 'react-router-dom';
|
import {Link, useHistory} from 'react-router-dom';
|
||||||
import {sanitize} from '../util/text';
|
import {sanitize} from '../util/text';
|
||||||
import {sendQueryEvent} from '../util/analytics';
|
import {sendQueryEvent} from '../util/analytics';
|
||||||
import {useDeferredState} from '../util/hooks';
|
import {mobile, slideUp} from '../util/css';
|
||||||
import {mobile} from '../util/css';
|
|
||||||
import Suggestion from './Suggestion';
|
import Suggestion from './Suggestion';
|
||||||
|
import {useDeferredState} from '../util/hooks';
|
||||||
|
|
||||||
const Form: React.FC<{
|
const Form: React.FC<{
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
}> = ({initialValue = ''}) => {
|
}> = ({initialValue = ''}) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [query, setQuery] = useDeferredState(800, '');
|
|
||||||
const [inputValue, setInputValue] = useState(initialValue);
|
const [inputValue, setInputValue] = useState(initialValue);
|
||||||
|
const [suggestionQuery, setSuggestionQuery] = useDeferredState(800, '');
|
||||||
|
const [isFocused, setFocus] = useState(false);
|
||||||
const [suggested, setSuggested] = useState(false);
|
const [suggested, setSuggested] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
function search(query: string) {
|
||||||
|
sendQueryEvent(sanitize(query));
|
||||||
|
history.push(`/s/${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
// set input value
|
// set input value
|
||||||
function onInputChange(e: React.FormEvent<HTMLInputElement>): void {
|
function onInputChange(e: React.FormEvent<HTMLInputElement>): void {
|
||||||
const value = e.currentTarget.value;
|
setInputValue(e.currentTarget.value);
|
||||||
setInputValue(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear input form and focus on it
|
// clear input form and focus on it
|
||||||
@ -34,53 +39,56 @@ const Form: React.FC<{
|
|||||||
// invoke when user clicked one of the suggested items
|
// invoke when user clicked one of the suggested items
|
||||||
function onSuggestionCompleted(name: string): void {
|
function onSuggestionCompleted(name: string): void {
|
||||||
setInputValue(name);
|
setInputValue(name);
|
||||||
|
search(name);
|
||||||
setSuggested(true);
|
setSuggested(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryGiven = query && query.length > 0;
|
function onSubmitQuery(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
useEffect(() => {
|
if (!inputValue || inputValue === '') {
|
||||||
function onQuery(query: string) {
|
return;
|
||||||
if (!query || query === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendQueryEvent(query);
|
|
||||||
history.push(`/s/${query}`);
|
|
||||||
}
|
}
|
||||||
|
search(inputValue);
|
||||||
if (query.length === 0) {
|
}
|
||||||
setSuggested(false);
|
|
||||||
} else {
|
|
||||||
onQuery(query);
|
|
||||||
}
|
|
||||||
}, [query, history]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modifiedValue = sanitize(inputValue);
|
const modifiedValue = sanitize(inputValue);
|
||||||
setQuery(modifiedValue);
|
setSuggestionQuery(modifiedValue);
|
||||||
}, [inputValue, setQuery]);
|
}, [inputValue, setSuggestionQuery]);
|
||||||
|
|
||||||
|
const queryGiven = suggestionQuery && suggestionQuery !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Logo to="/" onClick={onLogoClick}>
|
<Logo to="/" onClick={onLogoClick}>
|
||||||
<LogoImage src="/logo.svg" />
|
<LogoImage src="/logo.svg" />
|
||||||
</Logo>
|
</Logo>
|
||||||
<InputView
|
<form onSubmit={onSubmitQuery}>
|
||||||
onChange={onInputChange}
|
<InputView
|
||||||
value={inputValue}
|
onChange={onInputChange}
|
||||||
ref={inputRef}
|
onFocus={() => setFocus(true)}
|
||||||
placeholder={t('placeholder')}
|
onBlur={() => setFocus(false)}
|
||||||
aria-label="search query"
|
value={inputValue}
|
||||||
/>
|
ref={inputRef}
|
||||||
|
placeholder={t('placeholder')}
|
||||||
|
aria-label="search form"
|
||||||
|
/>
|
||||||
|
{isFocused && (
|
||||||
|
<Tips>
|
||||||
|
<span>
|
||||||
|
Press <kbd>Enter</kbd> to search
|
||||||
|
</span>
|
||||||
|
</Tips>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
{queryGiven && !suggested ? (
|
{queryGiven && !suggested ? (
|
||||||
<Suggestion onSubmit={onSuggestionCompleted} query={query} />
|
<Suggestion onSubmit={onSuggestionCompleted} query={suggestionQuery} />
|
||||||
) : null}
|
) : null}
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
||||||
|
|
||||||
const InputContainer = styled.div`
|
const InputContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -120,7 +128,8 @@ const LogoImage = styled.img`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const InputView = styled.input.attrs({
|
const InputView = styled.input.attrs({
|
||||||
type: 'text',
|
type: 'input',
|
||||||
|
name: 'search',
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
autocorrect: 'off',
|
autocorrect: 'off',
|
||||||
autocapitalize: 'off',
|
autocapitalize: 'off',
|
||||||
@ -142,3 +151,15 @@ const InputView = styled.input.attrs({
|
|||||||
color: #c8cdda;
|
color: #c8cdda;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Tips = styled.div`
|
||||||
|
color: #ababab;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
animation: ${slideUp} 0.6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -7,7 +7,7 @@ import {motion} from 'framer-motion';
|
|||||||
|
|
||||||
import {capitalize, stem, germanify, njoin, lower, upper} from '../util/text';
|
import {capitalize, stem, germanify, njoin, lower, upper} from '../util/text';
|
||||||
import {sampleFromArray, fillArray} from '../util/array';
|
import {sampleFromArray, fillArray} from '../util/array';
|
||||||
import {mobile} from '../util/css';
|
import {mobile, slideUp} from '../util/css';
|
||||||
import {sanitize} from '../util/text';
|
import {sanitize} from '../util/text';
|
||||||
import {
|
import {
|
||||||
sendShuffleSuggestionEvent,
|
sendShuffleSuggestionEvent,
|
||||||
@ -225,8 +225,11 @@ const Suggestion: React.FC<{
|
|||||||
<Items>
|
<Items>
|
||||||
{bestWords &&
|
{bestWords &&
|
||||||
bestWords.map((name, i) => (
|
bestWords.map((name, i) => (
|
||||||
<Item key={name + i} onClick={(): void => applyQuery(name)}>
|
<Item
|
||||||
{name}
|
key={name + i}
|
||||||
|
onClick={(): void => applyQuery(name)}
|
||||||
|
delay={i + 1}>
|
||||||
|
<span>{name}</span>
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</Items>
|
</Items>
|
||||||
@ -276,7 +279,7 @@ const Items = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Item = styled.div`
|
const Item = styled.div<{delay: number}>`
|
||||||
margin: 10px 10px 0;
|
margin: 10px 10px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -285,6 +288,14 @@ const Item = styled.div`
|
|||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
border-bottom: 1px dashed black;
|
border-bottom: 1px dashed black;
|
||||||
color: black;
|
color: black;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
animation: ${slideUp} 0.6s ${(props) => `${props.delay * 0.1}s`}
|
||||||
|
cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
${mobile} {
|
${mobile} {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
|
@ -1 +1,12 @@
|
|||||||
|
import {keyframes} from 'styled-components';
|
||||||
|
|
||||||
export const mobile = '@media screen and (max-width: 800px)';
|
export const mobile = '@media screen and (max-width: 800px)';
|
||||||
|
|
||||||
|
export const slideUp = keyframes`
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user