From 07c4b1db70f182d6dceea866b1696c4acaec3cd5 Mon Sep 17 00:00:00 2001
From: Dave Snider
Date: Wed, 25 Oct 2017 11:39:49 -0700
Subject: [PATCH] combo box design prototype
---
docs/src/services/routes/routes.js | 7 +
docs/src/views/combo_box/combo_box.js | 50 ++++
docs/src/views/combo_box/combo_box_example.js | 45 ++++
src/components/form/_index.scss | 1 +
src/components/form/combo_box/_combo_box.scss | 74 ++++++
.../form/combo_box/_combo_box_pill.scss | 40 +++
.../form/combo_box/_combo_box_row.scss | 17 ++
src/components/form/combo_box/_index.scss | 3 +
src/components/form/combo_box/combo_box.js | 242 ++++++++++++++++++
.../form/combo_box/combo_box_pill.js | 32 +++
.../form/combo_box/combo_box_row.js | 38 +++
src/components/form/combo_box/index.js | 11 +
src/components/form/index.js | 1 +
src/components/index.js | 3 +
src/global_styling/mixins/_helpers.scss | 6 +-
15 files changed, 567 insertions(+), 3 deletions(-)
create mode 100644 docs/src/views/combo_box/combo_box.js
create mode 100644 docs/src/views/combo_box/combo_box_example.js
create mode 100644 src/components/form/combo_box/_combo_box.scss
create mode 100644 src/components/form/combo_box/_combo_box_pill.scss
create mode 100644 src/components/form/combo_box/_combo_box_row.scss
create mode 100644 src/components/form/combo_box/_index.scss
create mode 100644 src/components/form/combo_box/combo_box.js
create mode 100644 src/components/form/combo_box/combo_box_pill.js
create mode 100644 src/components/form/combo_box/combo_box_row.js
create mode 100644 src/components/form/combo_box/index.js
diff --git a/docs/src/services/routes/routes.js b/docs/src/services/routes/routes.js
index 812b9003219..d542b7b3edf 100644
--- a/docs/src/services/routes/routes.js
+++ b/docs/src/services/routes/routes.js
@@ -27,6 +27,9 @@ import CallOutExample
import CodeExample
from '../../views/code/code_example';
+import ComboBoxExample
+ from '../../views/combo_box/combo_box_example';
+
import ContextMenuExample
from '../../views/context_menu/context_menu_example';
@@ -135,6 +138,10 @@ const components = [{
name: 'Code',
component: CodeExample,
hasReact: true,
+}, {
+ name: 'ComboBox',
+ component: ComboBoxExample,
+ hasReact: true,
}, {
name: 'ContextMenu',
component: ContextMenuExample,
diff --git a/docs/src/views/combo_box/combo_box.js b/docs/src/views/combo_box/combo_box.js
new file mode 100644
index 00000000000..dd2ecb562fd
--- /dev/null
+++ b/docs/src/views/combo_box/combo_box.js
@@ -0,0 +1,50 @@
+import React, {
+ Component,
+} from 'react';
+
+import {
+ EuiComboBox,
+} from '../../../../src/components';
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isPopoverOpen: false,
+ };
+ }
+
+ closePopover() {
+ this.setState({
+ isPopoverOpen: false,
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/docs/src/views/combo_box/combo_box_example.js b/docs/src/views/combo_box/combo_box_example.js
new file mode 100644
index 00000000000..212fb9d2151
--- /dev/null
+++ b/docs/src/views/combo_box/combo_box_example.js
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { renderToHtml } from '../../services';
+
+import {
+ GuidePage,
+ GuideSection,
+ GuideSectionTypes,
+} from '../../components';
+
+import {
+ EuiCode,
+ EuiCallOut,
+ EuiSpacer,
+} from '../../../../src/components';
+
+import ComboBox from './combo_box';
+const comboBoxSource = require('!!raw-loader!./combo_box');
+const comboBoxHtml = renderToHtml(ComboBox);
+
+export default props => (
+
+
+
+
+ Design prototype of ComboBox component. It is not usable.
+
+ }
+ demo={}
+ />
+
+);
diff --git a/src/components/form/_index.scss b/src/components/form/_index.scss
index f0661ab900c..57d0323e8fd 100644
--- a/src/components/form/_index.scss
+++ b/src/components/form/_index.scss
@@ -68,6 +68,7 @@ $euiFormBackgroundColor: tintOrShade($euiColorLightestShade, 60%, 25%);
}
@import 'checkbox/index';
+@import 'combo_box/index';
@import 'field_number/index';
@import 'field_password/index';
@import 'field_search/index';
diff --git a/src/components/form/combo_box/_combo_box.scss b/src/components/form/combo_box/_combo_box.scss
new file mode 100644
index 00000000000..818149c890e
--- /dev/null
+++ b/src/components/form/combo_box/_combo_box.scss
@@ -0,0 +1,74 @@
+.euiComboBox {
+ @include euiFormControlSize;
+ position: relative;
+
+ .euiComboBox__inputWrap {
+ @include euiFormControlStyle;
+ @include euiFormControlWithIcon($isIconOptional: true);
+ padding: $euiSizeXS;
+
+ &:hover {
+ cursor: text;
+ }
+ }
+
+ .euiComboBox__input {
+ padding: 0;
+ border: none;
+ background: transparent;
+ font-size: $euiFontSizeS;
+ font-family: $euiFontFamily;
+ color: $euiTextColor;
+ margin: $euiSizeXS;
+ }
+
+ .euiComboBox__panel {
+ position: absolute;
+ top: 100%;
+ left: -1px;
+ right: -1px;
+ border-radius: 0 0 $euiBorderRadius $euiBorderRadius;
+ visibility: hidden;
+ opacity: 0;
+ border-top: none;
+ transition:
+ visibility $euiAnimSpeedNormal $euiAnimSlightResistance,
+ opacity $euiAnimSpeedNormal $euiAnimSlightResistance;
+ }
+
+ .euiComoboBox__empty {
+ @include euiFontSizeS;
+ padding: $euiSizeS;
+ text-align: center;
+ color: $euiColorDarkShade;
+ }
+
+ .euiComboBox__rowWrap {
+ @include euiScrollBar;
+
+ padding: $euiSizeS;
+ max-height: 200px;
+ overflow-y: auto;
+ }
+
+ .euiComboBox__footer {
+ padding: $euiSizeS;
+ border-top: $euiBorderThin;
+ background: $euiFormBackgroundColor;
+ }
+ &.euiComboBox-isOpen {
+ .euiComboBox__panel {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ .euiComboBox__inputWrap {
+ background: $euiColorEmptyShade;
+ box-shadow:
+ 0 4px 4px -2px rgba(0, 0, 0, 0.1),
+ 0 0 0 1px rgba(0,0,0,0.16),
+ inset 0 0 0 0 $euiColorEmptyShade,
+ inset 0 -2px 0 0 $euiColorPrimary;
+ }
+ }
+}
diff --git a/src/components/form/combo_box/_combo_box_pill.scss b/src/components/form/combo_box/_combo_box_pill.scss
new file mode 100644
index 00000000000..d3e1fe081f8
--- /dev/null
+++ b/src/components/form/combo_box/_combo_box_pill.scss
@@ -0,0 +1,40 @@
+.euiComboBoxPill {
+ font-size: $euiSizeM;
+ line-height: $euiSizeL;
+ display: inline-block;
+ text-decoration: none;
+ border: solid 1px transparent;
+ border-radius: $euiSizeL;
+ padding: 0 $euiSizeS;
+ background-color: transparent;
+ white-space: nowrap;
+ vertical-align: middle;
+ border-color: $euiColorLightShade;
+ background-color: $euiColorEmptyShade;
+ margin: $euiSizeXS;
+ margin-right: 0;
+
+ .euiComboBoxPill__close {
+ width: $euiSizeM;
+ height: $euiSizeM;
+ margin-left: $euiSizeXS;
+ border: none;
+ vertical-align: middle;
+
+ &:hover {
+ .euiComboBoxPill__closeIcon {
+ fill: $euiColorDanger;
+ }
+ }
+
+ &:focus {
+ background: transparentize($euiColorDanger, .9);
+ }
+
+ svg {
+ width: $euiSizeM;
+ height: $euiSizeM;
+ vertical-align: top !important;
+ }
+ }
+}
diff --git a/src/components/form/combo_box/_combo_box_row.scss b/src/components/form/combo_box/_combo_box_row.scss
new file mode 100644
index 00000000000..5f6cfb86ed8
--- /dev/null
+++ b/src/components/form/combo_box/_combo_box_row.scss
@@ -0,0 +1,17 @@
+.euiComboBoxRow {
+ font-size: $euiFontSizeS;
+ padding: $euiSizeXS $euiSizeS;
+ width: 100%;
+ text-align: left;
+ border: $euiBorderThin;
+ border-color: transparent;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ &:focus {
+ cursor: pointer;
+ color: $euiColorPrimary;
+ background-color: $euiFocusBackgroundColor;
+ }
+}
diff --git a/src/components/form/combo_box/_index.scss b/src/components/form/combo_box/_index.scss
new file mode 100644
index 00000000000..7fd95d6daa4
--- /dev/null
+++ b/src/components/form/combo_box/_index.scss
@@ -0,0 +1,3 @@
+@import 'combo_box';
+@import 'combo_box_pill';
+@import 'combo_box_row';
diff --git a/src/components/form/combo_box/combo_box.js b/src/components/form/combo_box/combo_box.js
new file mode 100644
index 00000000000..01107a7c06c
--- /dev/null
+++ b/src/components/form/combo_box/combo_box.js
@@ -0,0 +1,242 @@
+import React, {
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiTextColor,
+ EuiPanel,
+ EuiComboBoxPill,
+ EuiComboBoxRow,
+ EuiFormControlLayout,
+ EuiValidatableControl,
+ EuiOutsideClickDetector,
+} from '../../../components';
+
+export class EuiComboBox extends Component {
+ static propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isPopoverOpen: this.props.isPopoverOpen,
+ value: '',
+ matches: this.props.options,
+ focusedRow: -1,
+ };
+
+ this.handleSearchInputFocus = this.handleSearchInputFocus.bind(this);
+ this.handleShowPopover = this.handleShowPopover.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.isPopoverOpen !== this.state.isPopoverOpen) {
+ this.setState({ isPopoverOpen: nextProps.isPopoverOpen });
+ }
+ }
+
+ handleSearchInputFocus() {
+ this.searchInput.focus();
+ }
+
+ handleShowPopover() {
+ this.setState({
+ isPopoverOpen: true,
+ });
+ }
+
+ filterItems(query) {
+ return this.props.options.filter(function(option) {
+ return option.text.toLowerCase().indexOf(query.toLowerCase()) !== -1;
+ });
+ }
+
+ handleChange(event) {
+ this.setState({
+ value: event.target.value,
+ });
+ }
+
+ handleClosePopover() {
+ this.setState({
+ isPopoverOpen: false,
+ });
+ }
+
+ handleKeyDown(e) {
+ switch (e.keyCode) {
+ case 40: // Down
+ this.focusRowNext();
+ break;
+ case 38: // Up
+ break;
+ case 8: // Backspace
+ break;
+ case 13: // Enter
+ break;
+ }
+ }
+
+ focusRowNext() {
+
+ let rowIndex;
+ if (this.state.focusedRow >= this.props.options.length - 1) {
+ rowIndex = this.props.options.length - 1;
+ } else {
+ rowIndex = this.props.focusedRow + 1;
+ }
+
+ this.setState({
+ focusedRow: rowIndex,
+ });
+ }
+
+ render() {
+ const {
+ children,
+ options,
+ isInvalid,
+ isPopoverOpen,
+ className,
+ closePopover,
+ selectedOptions,
+ optionTypeName,
+ ...rest,
+ } = this.props;
+
+ const searchString = this.state.value.toLowerCase();
+
+ const matches =
+ this.props.options.filter(option => (
+ option.text.toLowerCase().indexOf(searchString) !== -1
+ )).map((option, index) => {
+ return (
+ {option.text}
+ );
+ });
+
+ const exactMatches = this.props.options.filter(function (option) {
+ return (option.text.toLowerCase() === searchString);
+ });
+
+ let matchesOrEmpty = null;
+ if (matches.length === 0) {
+ matchesOrEmpty = (
+
+ No {optionTypeName} matches your search.
+
+ );
+ } else {
+ matchesOrEmpty = matches;
+ }
+
+ const classes = classNames(
+ 'euiComboBox',
+ {
+ 'euiComboBox-isOpen': this.state.isPopoverOpen,
+ },
+ className
+ );
+
+ const panelClasses = classNames(
+ 'euiComboBox__panel',
+ );
+
+ let footer = null;
+
+ if ((exactMatches.length === 0) && (searchString !== '')) {
+ footer = (
+
+
+
+
+
+ Not listed?
+
+
+
+
+ Add {this.state.value}
+
+
+
+ );
+ } else if (searchString === '') {
+ footer = (
+
+
+
+ Start typing to add a new {optionTypeName}.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {selectedOptions.map((option, index) => {
+ return {option.text};
+ })}
+
+
+ { this.searchInput = input; }}
+ />
+
+
+
+
+
+ {matchesOrEmpty}
+
+ {footer}
+
+
+
+ );
+ }
+}
+
+EuiComboBox.propTypes = {
+ name: PropTypes.string,
+ id: PropTypes.string,
+ // options: PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ // selectedOptions: PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ isInvalid: PropTypes.bool,
+ isPopoverOpen: PropTypes.bool,
+};
+
+EuiComboBox.defaultProps = {
+ options: [],
+ selectedOptions: [],
+};
diff --git a/src/components/form/combo_box/combo_box_pill.js b/src/components/form/combo_box/combo_box_pill.js
new file mode 100644
index 00000000000..9619158368b
--- /dev/null
+++ b/src/components/form/combo_box/combo_box_pill.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import {
+ EuiIcon,
+} from '../../../components';
+
+export const EuiComboBoxPill = ({
+ children,
+ className,
+ ...rest,
+}) => {
+ const classes = classNames('euiComboBoxPill', className);
+
+ return (
+
+ {children}
+
+
+ );
+};
+
+EuiComboBoxPill.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+};
diff --git a/src/components/form/combo_box/combo_box_row.js b/src/components/form/combo_box/combo_box_row.js
new file mode 100644
index 00000000000..ec44a341f8d
--- /dev/null
+++ b/src/components/form/combo_box/combo_box_row.js
@@ -0,0 +1,38 @@
+import React, {
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export class EuiComboBoxRow extends Component {
+ static propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ }
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ children,
+ className,
+ ...rest,
+ } = this.props;
+
+ const classes = classNames(
+ 'euiComboBoxRow',
+ className
+ );
+
+ return (
+
+ );
+ }
+}
diff --git a/src/components/form/combo_box/index.js b/src/components/form/combo_box/index.js
new file mode 100644
index 00000000000..d0ef23f2b81
--- /dev/null
+++ b/src/components/form/combo_box/index.js
@@ -0,0 +1,11 @@
+export {
+ EuiComboBox,
+} from './combo_box';
+
+export {
+ EuiComboBoxPill,
+} from './combo_box_pill';
+
+export {
+ EuiComboBoxRow,
+} from './combo_box_row';
diff --git a/src/components/form/index.js b/src/components/form/index.js
index 2c0c408108b..92cadf3bb43 100644
--- a/src/components/form/index.js
+++ b/src/components/form/index.js
@@ -2,6 +2,7 @@ export {
EuiCheckbox,
EuiCheckboxGroup,
} from './checkbox';
+export { EuiComboBox, EuiComboBoxPill, EuiComboBoxRow } from './combo_box';
export { EuiFieldNumber } from './field_number';
export { EuiFieldPassword } from './field_password';
export { EuiFieldSearch } from './field_search';
diff --git a/src/components/index.js b/src/components/index.js
index af424d801f8..08cc7bae0ae 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -52,6 +52,9 @@ export {
export {
EuiCheckbox,
EuiCheckboxGroup,
+ EuiComboBox,
+ EuiComboBoxPill,
+ EuiComboBoxRow,
EuiFieldNumber,
EuiFieldPassword,
EuiFieldSearch,
diff --git a/src/global_styling/mixins/_helpers.scss b/src/global_styling/mixins/_helpers.scss
index 52ccd801d08..5593155a2aa 100644
--- a/src/global_styling/mixins/_helpers.scss
+++ b/src/global_styling/mixins/_helpers.scss
@@ -29,12 +29,12 @@
*/
@mixin euiScrollBar {
&::-webkit-scrollbar {
- width: 16px;
- height: 16px;
+ width: $euiSize;
+ height: $euiSize;
}
&::-webkit-scrollbar-thumb {
- background-color: rgba($euiColorDarkShade, 0.5);
+ background-color: $euiColorLightShade;
border: 6px solid transparent;
background-clip: content-box;
}