web: Components: Reworked ComboBox component. Fixed readme and story.

This commit is contained in:
Ilya Oleshko 2019-08-14 15:53:05 +03:00
parent 8dda4ddeab
commit 810d8c23d8
3 changed files with 87 additions and 103 deletions

View File

@ -26,15 +26,20 @@ const StyledComboBox = styled.div`
user-select: none; user-select: none;
background: #FFFFFF; background: #FFFFFF;
border: 1px solid #D0D5DA;
border-radius: 3px;
${props => props.isDisabled && ` ${props => !props.noBorder && `
border: 1px solid #D0D5DA;
border-radius: 3px;
`}
${props => props.isDisabled && !props.noBorder && `
border-color: #ECEEF1; border-color: #ECEEF1;
background: #F8F9F9; background: #F8F9F9;
`} `}
height: 32px; ${props => !props.noBorder && `
height: 32px;
`}
:hover{ :hover{
border-color: ${state => state.isOpen ? '#2DA7DB' : '#A3A9AE'}; border-color: ${state => state.isOpen ? '#2DA7DB' : '#A3A9AE'};
@ -49,7 +54,9 @@ const StyledComboButton = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 30px;
height: ${props => props.noBorder ? `18px` : `30px`};
margin-left: 8px; margin-left: 8px;
`; `;
@ -72,16 +79,26 @@ font-size: 13px;
white-space: nowrap; white-space: nowrap;
margin-right: 8px; margin-right: 8px;
${props => props.noBorder && `
line-height: none;
:hover{
border-bottom: 1px dashed;
}
`};
`; `;
const StyledArrowIcon = styled.div` const StyledArrowIcon = styled.div`
display: flex;
align-self: start;
width: 8px; width: 8px;
margin-top: ${props => props.noBorder ? `5px` : `12px`};
margin-right: 8px; margin-right: 8px;
margin-left: auto; margin-left: auto;
${state => state.isOpen && ` ${props => props.isOpen && `
transform: scale(1, -1); transform: scale(1, -1);
margin-top: 8px;
`} `}
`; `;
@ -91,26 +108,19 @@ class ComboBox extends React.PureComponent {
this.ref = React.createRef(); this.ref = React.createRef();
const selectedItem = this.getSelected();
this.state = { this.state = {
isOpen: props.opened, isOpen: props.opened,
boxLabel: selectedItem && selectedItem.label, selectedOption: props.selectedOption
boxIcon: selectedItem && selectedItem.icon,
options: props.options
}; };
this.handleClick = this.handleClick.bind(this);
this.stopAction = this.stopAction.bind(this);
this.toggle = this.toggle.bind(this);
this.comboBoxClick = this.comboBoxClick.bind(this);
this.optionClick = this.optionClick.bind(this);
if (props.opened) if (props.opened)
handleAnyClick(true, this.handleClick); handleAnyClick(true, this.handleClick);
} }
handleClick = (e) => this.state.isOpen && !this.ref.current.contains(e.target) && this.toggle(false); handleClick = (e) =>
this.state.isOpen
&& !this.ref.current.contains(e.target)
&& this.toggle(false);
stopAction = (e) => e.preventDefault(); stopAction = (e) => e.preventDefault();
@ -118,69 +128,43 @@ class ComboBox extends React.PureComponent {
comboBoxClick = (e) => { comboBoxClick = (e) => {
if (this.props.isDisabled || e.target.closest('.optionalBlock')) return; if (this.props.isDisabled || e.target.closest('.optionalBlock')) return;
this.toggle(!this.state.isOpen);
this.setState({
option: this.props.option,
isOpen: !this.state.isOpen
});
}; };
optionClick = (option) => { optionClick = (option) => {
this.setState({ this.toggle(!this.state.isOpen);
boxLabel: option.label, this.setState({
boxIcon: option.icon, isOpen: !this.state.isOpen,
isOpen: !this.state.isOpen selectedOption: option
}); });
this.props.onSelect && this.props.onSelect(option); this.props.onSelect && this.props.onSelect(option);
}; };
componentWillUnmount() { componentWillUnmount() {
handleAnyClick(false, this.handleClick); handleAnyClick(false, this.handleClick);
} };
getSelected = () => {
const selectedItem = this.props.options.find(x => x.key === this.props.selectedOption)
|| this.props.options[0];
return selectedItem;
}
getSelectedLabel = () => {
const selectedItem = this.getSelected();
return selectedItem ? selectedItem.label : this.props.emptyOptionsPlaceholder;
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.props.opened !== prevProps.opened) { if (this.props.opened !== prevProps.opened) {
this.toggle(this.props.opened); handleAnyClick(this.props.opened, this.handleClick);
} }
if (this.state.isOpen !== prevState.isOpen) { if (this.state.isOpen !== prevState.isOpen) {
handleAnyClick(this.state.isOpen, this.handleClick); handleAnyClick(this.state.isOpen, this.handleClick);
} }
if (this.props.options.length !== prevProps.options.length) { //TODO: Move options from state
const label = this.getSelectedLabel();
this.setState({
options: this.props.options,
boxLabel: label
});
}
if (this.props.selectedOption !== prevProps.selectedOption) { if (this.props.selectedOption !== prevProps.selectedOption) {
const label = this.getSelectedLabel(); this.setState({selectedOption: this.props.selectedOption});
this.setState({ boxLabel: label });
} }
} };
render() { render() {
//console.log("ComboBox render"); console.log("ComboBox render");
const { dropDownMaxHeight, isDisabled, directionX, directionY, scaled, children } = this.props; const { dropDownMaxHeight, isDisabled, directionX, directionY, scaled, children, options, noBorder } = this.props;
const { boxLabel, boxIcon, isOpen, options } = this.state; const { isOpen, selectedOption } = this.state;
const dropDownMaxHeightProp = dropDownMaxHeight ? { maxHeight: dropDownMaxHeight} : {}; const dropDownMaxHeightProp = dropDownMaxHeight ? { maxHeight: dropDownMaxHeight } : {};
const dropDownManualWidthProp = scaled ? { manualWidth: '100%' } : {}; const dropDownManualWidthProp = scaled ? { manualWidth: '100%' } : {};
const boxIconColor = isDisabled ? '#D0D5DA' : '#333333'; const boxIconColor = isDisabled ? '#D0D5DA' : '#333333';
const arrowIconColor = isDisabled ? '#D0D5DA' : '#A3A9AE'; const arrowIconColor = isDisabled ? '#D0D5DA' : '#A3A9AE';
@ -189,17 +173,17 @@ class ComboBox extends React.PureComponent {
<StyledComboBox ref={this.ref} <StyledComboBox ref={this.ref}
{...this.props} {...this.props}
{...this.state} {...this.state}
data={boxLabel} data={selectedOption}
onClick={this.comboBoxClick} onClick={this.comboBoxClick}
onSelect={this.stopAction} onSelect={this.stopAction}
> >
<StyledComboButton> <StyledComboButton noBorder={noBorder}>
<StyledOptionalItem className='optionalBlock'> <StyledOptionalItem className='optionalBlock'>
{children} {children}
</StyledOptionalItem> </StyledOptionalItem>
{boxIcon && {selectedOption && selectedOption.icon &&
<StyledIcon> <StyledIcon>
{React.createElement(Icons[boxIcon], {React.createElement(Icons[selectedOption.icon],
{ {
size: 'scale', size: 'scale',
color: boxIconColor, color: boxIconColor,
@ -208,10 +192,10 @@ class ComboBox extends React.PureComponent {
} }
</StyledIcon> </StyledIcon>
} }
<StyledLabel> <StyledLabel noBorder={noBorder}>
{boxLabel} {selectedOption.label}
</StyledLabel> </StyledLabel>
<StyledArrowIcon {...this.state}> <StyledArrowIcon noBorder={noBorder} isOpen={this.state.isOpen}>
{React.createElement(Icons['ExpanderDownIcon'], {React.createElement(Icons['ExpanderDownIcon'],
{ {
size: 'scale', size: 'scale',
@ -231,7 +215,7 @@ class ComboBox extends React.PureComponent {
> >
{options.map((option) => {options.map((option) =>
<DropDownItem {...option} <DropDownItem {...option}
disabled={option.label === boxLabel} disabled={option.label === selectedOption.label}
onClick={this.optionClick.bind(this, option)} onClick={this.optionClick.bind(this, option)}
/> />
)} )}
@ -242,25 +226,20 @@ class ComboBox extends React.PureComponent {
}; };
ComboBox.propTypes = { ComboBox.propTypes = {
noBorder: PropTypes.bool,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
withBorder: PropTypes.bool, selectedOption: PropTypes.object,
selectedOption: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
options: PropTypes.array, options: PropTypes.array,
onSelect: PropTypes.func, onSelect: PropTypes.func,
dropDownMaxHeight: PropTypes.number, dropDownMaxHeight: PropTypes.number,
emptyOptionsPlaceholder: PropTypes.string,
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge', 'content']), size: PropTypes.oneOf(['base', 'middle', 'big', 'huge', 'content']),
scaled: PropTypes.bool, scaled: PropTypes.bool,
} }
ComboBox.defaultProps = { ComboBox.defaultProps = {
noBorder: false,
isDisabled: false, isDisabled: false,
withBorder: true,
emptyOptionsPlaceholder: 'Select',
size: 'base', size: 'base',
scaled: true scaled: true
} }

View File

@ -37,13 +37,25 @@ const options = [
key: 5, key: 5,
icon: 'CopyIcon', icon: 'CopyIcon',
label: 'Option 5' label: 'Option 5'
},
{
key: 6,
label: 'Option 6'
},
{
key: 7,
label: 'Option 7'
} }
]; ];
<ComboBox options={options} <ComboBox options={options}
isDisabled={false} isDisabled={false}
selectedOption={25} selectedOption={{
key: 0,
label: 'Select'
}}
dropDownMaxHeight='200px' dropDownMaxHeight='200px'
noBorder={false}
scale={true} scale={true}
size='content' size='content'
onSelect={option => console.log('selected', option)} onSelect={option => console.log('selected', option)}
@ -56,9 +68,9 @@ const options = [
| ---------------------- | ----------------- | :------: | ---------------------------- | ------- | -------------------------------------------- | | ---------------------- | ----------------- | :------: | ---------------------------- | ------- | -------------------------------------------- |
| `options` | `array` | ✅ | - | - | Combo box options | | `options` | `array` | ✅ | - | - | Combo box options |
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled | | `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
| `selectedOption` | `string`,`number` | - | - | `0` | Index of option selected by default | | `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
| `selectedOption` | `object` | - | - | - | Selected option |
| `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option | | `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option |
| `dropDownMaxHeight` | `string` | - | - | - | Height of Dropdown | | `dropDownMaxHeight` | `string` | - | - | - | Height of Dropdown |
| `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent | | `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent |
| `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default | | `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default |
| `emptyOptionsPlaceholder`| `string` | - | - | `Select`| Label displayed in absence of options |

View File

@ -11,17 +11,6 @@ import Section from '../../../.storybook/decorators/section';
const iconNames = Object.keys(Icons); const iconNames = Object.keys(Icons);
const sizeOptions = ['base', 'middle', 'big', 'huge', 'content']; const sizeOptions = ['base', 'middle', 'big', 'huge', 'content'];
const appendOptions = (comboOptions, optionsCount) => {
let newOptions = comboOptions;
for (let i = 0; i <= optionsCount; i++) {
newOptions.push({
key: (i + 6),
label: 'Option ' + (i + 6)
})
}
return newOptions;
};
iconNames.push("NONE"); iconNames.push("NONE");
storiesOf('Components|Input', module) storiesOf('Components|Input', module)
@ -52,26 +41,25 @@ storiesOf('Components|Input', module)
key: 5, key: 5,
icon: 'CopyIcon', icon: 'CopyIcon',
label: 'Option 5' label: 'Option 5'
},
{
key: 6,
label: 'Option 6'
},
{
key: 7,
label: 'Option 7'
} }
]; ];
const optionsCount = number('Add options', 1,
{
range: true,
min: 1,
max: 100,
step: 1
}
);
const needScrollDropDown = boolean('Need scroll dropdown', false); const needScrollDropDown = boolean('Need scroll dropdown', false);
const dropDownMaxHeight = needScrollDropDown && number('dropDownMaxHeight', 200); const dropDownMaxHeight = needScrollDropDown && number('dropDownMaxHeight', 200);
const optionsMultiSelect = options('Children', const optionsMultiSelect = options('children',
{ {
button: 'button', button: 'button',
icon: 'icon' icon: 'icon'
}, },
['icon'], [],
{ {
display: 'multi-select', display: 'multi-select',
}); });
@ -80,10 +68,10 @@ storiesOf('Components|Input', module)
optionsMultiSelect.forEach(function (item, i) { optionsMultiSelect.forEach(function (item, i) {
switch (item) { switch (item) {
case "button": case "button":
children.push(<Button label="OK" key={i} />); children.push(<Button label="button" key={i} />);
break; break;
case "icon": case "icon":
children.push(<Icons.SettingsIcon size="medium" key={i} />); children.push(<Icons.NavLogoIcon size="medium" key={i} />);
break; break;
default: default:
break; break;
@ -93,9 +81,14 @@ storiesOf('Components|Input', module)
return ( return (
<Section> <Section>
<ComboBox <ComboBox
options={appendOptions(comboOptions, optionsCount)} options={comboOptions}
onSelect={option => action("Selected option")(option)} onSelect={option => action("Selected option")(option)}
selectedOption={{
key: 0,
label: 'Select'
}}
isDisabled={boolean('isDisabled', false)} isDisabled={boolean('isDisabled', false)}
noBorder={boolean('noBorder', false)}
dropDownMaxHeight={dropDownMaxHeight} dropDownMaxHeight={dropDownMaxHeight}
scaled={boolean('scaled', false)} scaled={boolean('scaled', false)}
size={select('size', sizeOptions, 'content')} size={select('size', sizeOptions, 'content')}