Commit 68bb04d1 authored by Jim Lin's avatar Jim Lin
Browse files

[ANUE-1807] Add AnueSearchInput component

parent cb10707e
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames/bind';
import styles from './AnueSearchInput.scss';
const cx = classnames.bind(styles);
class AnueSearchInput extends React.Component {
static propTypes = {
customAttribute: PropTypes.objectOf(PropTypes.any),
customWrapperStyles: PropTypes.objectOf(PropTypes.any),
customInputStyles: PropTypes.objectOf(PropTypes.any),
customIconStyles: PropTypes.objectOf(PropTypes.any),
customInputClassName: PropTypes.string,
shouldAlwaysDisplayInput: PropTypes.bool,
onIconClick: PropTypes.func,
onInputChange: PropTypes.func,
onInputFocus: PropTypes.func,
onInputBlur: PropTypes.func,
onInputPressEnter: PropTypes.func,
onDisplayChange: PropTypes.func,
theme: PropTypes.string,
placeholder: PropTypes.string,
defaultValue: PropTypes.string,
};
static defaultProps = {
customAttribute: null,
customWrapperStyles: null,
customInputStyles: null,
customIconStyles: null,
shouldAlwaysDisplayInput: false,
theme: null, // mobile | desktop
placeholder: '搜尋新聞、代碼或名稱',
defaultValue: null,
customInputClassName: null,
onDisplayChange: () => {},
onIconClick: () => {},
onInputChange: () => {},
onInputFocus: () => {},
onInputBlur: () => {},
onInputPressEnter: () => {},
};
constructor(props) {
super(props);
this.state = {
value: props.defaultValue || '',
shouldDisplayInput: props.shouldAlwaysDisplayInput || props.theme !== 'mobile',
};
}
handleChange = e => {
const { onInputChange } = this.props;
const value = e.target.value;
this.setState({
value,
});
onInputChange(value);
};
handleClickIcon = () => {
const { onIconClick, shouldAlwaysDisplayInput, onDisplayChange } = this.props;
const { value, shouldDisplayInput } = this.state;
onIconClick(value);
if (!shouldAlwaysDisplayInput) {
this.setState({
shouldDisplayInput: !shouldDisplayInput,
});
onDisplayChange(!shouldDisplayInput);
}
};
handleKeyDown = e => {
if (e.keyCode === 13) {
const { onInputPressEnter } = this.props;
const { value } = this.state;
if (value) {
onInputPressEnter(value);
}
}
};
handleResetValue = () => {
this.setState({
value: '',
});
};
render() {
const {
placeholder,
onInputFocus,
onInputBlur,
customAttribute,
customWrapperStyles,
customInputStyles,
customIconStyles,
theme,
customInputClassName,
} = this.props;
const { shouldDisplayInput } = this.state;
return (
<div className={cx('anue-search-input--wrapper', theme, customInputClassName)} style={customWrapperStyles}>
<input
className={cx('anue-search-input', theme, {
display: shouldDisplayInput,
})}
onChange={this.handleChange}
onFocus={onInputFocus}
onBlur={onInputBlur}
onKeyDown={this.handleKeyDown}
style={customInputStyles}
placeholder={placeholder}
{...customAttribute}
/>
<div className={cx('search-icon', theme)} onClick={this.handleClickIcon} style={customIconStyles} />
</div>
);
}
}
export default AnueSearchInput;
@import "../../styles/vars";
%search-icon-bg {
background: url("../../assets/icon-search.svg") no-repeat;
background-size: cover;
background-position: center;
}
.anue-search-input--wrapper {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
z-index: 5;
&.test1 {
color: #fff;
}
&.test2 {
color: #000;
}
.anue-search-input {
position: relative;
height: 30px;
background-color: $gray-eeeeee;
border-radius: 2px;
font-size: 13px;
transition: flex 0.3s $cubicEaseInAndOut, width 0.3s $cubicEaseInAndOut;
@media only screen and (max-width: $screenSizeLg) {
flex: 0;
width: 0;
padding: 0;
&.display {
flex: 1;
width: 100%;
padding: 0 8px;
}
}
@media only screen and (min-width: $screenSizeLg) {
width: 400px;
height: 32px;
padding: 7px 8px;
border: 1px solid transparent;
transition: flex 0.3s $cubicEaseInAndOut, width 0.3s $cubicEaseInAndOut, background-color 0.2s $cubicEaseInAndOut,
border 0.2s $cubicEaseInAndOut;
}
&.desktop {
width: 400px;
height: 32px;
padding: 7px 8px;
border: 1px solid transparent;
&:focus {
background-color: $white;
border: 1px solid $gray-eeeeee;
}
}
&.mobile {
flex: 0;
width: 0;
padding: 0;
&.display {
flex: 1;
width: 100%;
padding: 0 8px;
}
}
&::placeholder {
padding: 0 8px;
}
}
.search-icon {
width: 18px;
height: 18px;
margin: 0 12px;
cursor: pointer;
@extend %search-icon-bg;
&:not(.mobile) {
@media only screen and (min-width: $screenSizeLg) {
position: absolute;
right: 0;
top: 8px;
width: 16px;
height: 16px;
z-index: 5;
}
}
&.desktop {
position: absolute;
right: 0;
top: 8px;
width: 16px;
height: 16px;
z-index: 5;
}
}
}
## Usage
```
import AnueSearchInput from 'fe-common-library/dest/components/AnueSearchInput/AnueSearchInput';
import 'fe-common-library/dest/components/AnueSearchInput/style.css';
const defaultProps = {
customAttribute: null,
customWrapperStyles: null,
customInputStyles: null,
customIconStyles: null,
shouldAlwaysDisplayInput: false,
theme: null, // mobile | desktop
placeholder: '搜尋新聞、代碼或名稱',
defaultValue: null,
onIconClick: () => {},
onInputChange: () => {},
onInputFocus: () => {},
onInputBlur: () => {},
onInputPressEnter: () => {},
}
render() {
return (
<AnueSearchInput {...defaultProps}/>;
);
}
```
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';
import { withNotes } from '@storybook/addon-notes';
import { withState } from '@dump247/storybook-state';
import * as knobs from '@storybook/addon-knobs/react';
import Header from '../../Header/Header';
import AnueSearchInput from '../AnueSearchInput';
import { MobileMenu } from '../../MobileHeader';
import AccountMenu from '../../AccountMenu/AccountMenu';
import AccountMenuStyles from '../../AccountMenu/__stories__/Container.scss';
import defaultNotes from './AnueSearchInput.md';
const WRAPPER_STYLES = {
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
flexDirection: 'column',
};
const MOBILE_WRAPPER_STYLES = {
...WRAPPER_STYLES,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
};
const MOBILE_MENU_SEARCH_ICON_WRAPPER = {
position: 'absolute',
right: 0,
top: 0,
height: '44px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
};
const stories = storiesOf('AnueSearchInput', module);
stories.addDecorator(knobs.withKnobs);
stories.add(
'Desktop',
withState({
hiddenInput: false,
isFocus: false,
})(
withNotes(defaultNotes)(
withInfo()(() => {
knobs.boolean('onFocus', false);
knobs.text('Input Value', '');
return (
<div style={WRAPPER_STYLES}>
<Header
channel="全站搜尋Bar"
displayChannelName
customMenu={<AccountMenu />}
searchBar={
<AnueSearchInput
onInputChange={e => {
const value = e.target.value;
knobs.text('Input Value', value);
}}
onInputFocus={() => {
knobs.boolean('onFocus', true);
}}
customAttribute={{
onBlur: () => {
knobs.boolean('onFocus', false);
},
}}
/>
}
/>
</div>
);
})
)
)
);
stories.add(
'Mobile',
withState({
hiddenInput: true,
isFocus: false,
})(
withNotes(defaultNotes)(
withInfo()(({ store }) => {
knobs.boolean('onFocus', false);
knobs.text('Input Value', '');
return (
<div style={MOBILE_WRAPPER_STYLES}>
<div className={AccountMenuStyles.mobileWrapper} style={{ backgroundColor: '#fff', height: '100vh' }}>
<header className={AccountMenuStyles.mobileHeader} style={{ borderBottom: '1px solid #eee' }}>
<MobileMenu />
<nav className={AccountMenuStyles.mobileLinks}>
<a>外匯</a>
<a>市場</a>
<a>虛擬貨幣</a>
</nav>
<div className={AccountMenuStyles.accountWrapper}>
<AccountMenu highlight isLogin />
</div>
<div
style={{
...MOBILE_MENU_SEARCH_ICON_WRAPPER,
width: '100%',
}}
>
<AnueSearchInput
theme="mobile"
hiddenInput={store.state.hiddenInput}
onIconClick={() => {
store.set({ hiddenInput: !store.state.hiddenInput });
}}
onInputChange={value => {
knobs.text('Input Value', value);
}}
onInputFocus={() => {
knobs.boolean('onFocus', true);
}}
customAttribute={{
onBlur: () => {
knobs.boolean('onFocus', false);
},
}}
customInputStyles={{
maxWidth: 'calc(100% - 85px)',
}}
/>
</div>
</header>
</div>
</div>
);
})
)
)
);
import React from 'react';
import { mount } from 'enzyme';
import AnueSearchInput from '../AnueSearchInput';
describe('<AnueSearchInput /> component', () => {
let makeSubject;
const defaultProps = {
customAttribute: null,
customWrapperStyles: null,
customInputStyles: null,
customIconStyles: null,
shouldAlwaysDisplayInput: false,
theme: null, // mobile | desktop
placeholder: '搜尋新聞、代碼或名稱',
defaultValue: null,
onIconClick: jest.fn(),
onInputChange: jest.fn(),
onInputFocus: jest.fn(),
onInputBlur: jest.fn(),
onInputPressEnter: jest.fn(),
};
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
makeSubject = params => mount(<AnueSearchInput {...defaultProps} {...params} />);
});
it('should display the search input and the search icon both', () => {
const subject = makeSubject();
expect(subject.find('.anue-search-input.display').length).toBe(1);
});
describe('When the prop theme is mobile', () => {
let subject;
beforeEach(() => {
subject = makeSubject({
theme: 'mobile',
});
});
it('should display the search icon only on the initial render.', () => {
expect(subject.find('.anue-search-input.display').length).toBe(0);
});
it('should toggle the search input only on the initial render if the search icon is clicked', () => {
const icon = subject.find('.search-icon');
expect(subject.find('.anue-search-input.display').length).toBe(0);
icon.simulate('click');
expect(subject.find('.anue-search-input.display').length).toBe(1);
icon.simulate('click');
expect(subject.find('.anue-search-input.display').length).toBe(0);
});
it('should not hide the input even the props.theme is mobile if the props.shouldAlwaysDisplayInput is true', () => {
subject = makeSubject({
theme: 'mobile',
shouldAlwaysDisplayInput: true,
});
const icon = subject.find('.search-icon');
expect(subject.find('.anue-search-input.display').length).toBe(1);
icon.simulate('click');
expect(subject.find('.anue-search-input.display').length).toBe(1);
});
});
describe('The actions to call the private methods with the props', () => {
let subject;
beforeEach(() => {
subject = makeSubject({
theme: 'mobile',
});
});
it('should call props.onInputChange and pass the input value', () => {
subject = makeSubject();
subject.find('.anue-search-input').simulate('change', { target: { value: 'test' } });
expect(defaultProps.onInputChange).toHaveBeenCalledWith('test');
});
it('should call props.onIconClick and pass the input value', () => {
subject = makeSubject();
subject.find('.anue-search-input').simulate('change', { target: { value: 'test' } });
subject.find('.search-icon').simulate('click');
expect(defaultProps.onIconClick).toHaveBeenCalledWith('test');
});
it('should call props.onInputPressEnter and pass the input value', () => {
subject = makeSubject();
const input = subject.find('.anue-search-input');
input.simulate('change', { target: { value: 'test' } });
input.simulate('keyDown', { keyCode: 13 });
expect(defaultProps.onInputPressEnter).toHaveBeenCalledWith('test');
});
});
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment