Replace Dropdown with Autocomplete

dev 2.0.0-beta.23
Vitaliy Filippov 2019-06-19 20:13:48 +03:00
parent ba3993c811
commit 8690e3a709
10 changed files with 30 additions and 658 deletions

View File

@ -21,11 +21,11 @@
}
.withclear input {
text-indent: 28px !important;
text-indent: 28px;
}
.withclear label {
transition-property: top, left, font-size, color !important;
transition-property: top, left, font-size, color;
}
.inputFilled {
@ -33,7 +33,7 @@
}
.withclear input:not(:focus):not(.inputFilled) ~ label {
left: 28px !important;
left: 28px;
}
.values {

View File

@ -1,132 +0,0 @@
import * as React from "react";
import ReactToolbox from "../index";
export interface DropdownTheme {
/**
* Added to the root element when the dropdown is active.
*/
active?: string;
/**
* Added to the root element when it's disabled.
*/
disabled?: string;
/**
* Root element class.
*/
dropdown?: string;
/**
* Used for the error element.
*/
error?: string;
/**
* Added to the inner wrapper if it's errored.
*/
errored?: string;
/**
* Used for the inner wrapper of the component.
*/
field?: string;
/**
* Used for the the label element.
*/
label?: string;
/**
* Used when dropdown has required attribute.
*/
required?: string;
/**
* Used to highlight the selected value.
*/
selected?: string;
/**
* Used as a wrapper for the given template value.
*/
templateValue?: string;
/**
* Added to the root element when it's opening up.
*/
up?: string;
/**
* Used for each value in the dropdown component.
*/
value?: string;
/**
* Used for the list of values.
*/
values?: string;
}
export interface DropdownProps extends ReactToolbox.Props {
/**
* If true the dropdown will preselect the first item if the supplied value matches none of the options' values.
* @default true
*/
allowBlank?: boolean;
/**
* If true, the dropdown will open up or down depending on the position in the screen.
* @default true
*/
auto?: boolean;
/**
* Set the component as disabled.
* @default false
*/
disabled?: boolean;
/**
* Give an error string to display under the field.
*/
error?: string;
/**
* The text string to use for the floating label element.
*/
label?: string;
/**
* Used for setting the label from source
*/
labelKey?: string;
/**
* Name for the input field.
*/
name?: string;
/**
* Callback function that is fired when the component is blurred.
*/
onBlur?: Function;
/**
* Callback function that is fired when the component's value changes.
*/
onChange?: Function;
/**
* Callback function that is fired when the component is focused.
*/
onFocus?: Function;
/**
* If true, the dropdown has a required attribute.
* @default false
*/
required?: boolean;
/**
* Array of data objects with the data to represent in the dropdown.
*/
source: any[];
/**
* Callback function that returns a JSX template to represent the element.
*/
template?: Function;
/**
* Classnames object defining the component style.
*/
theme?: DropdownTheme;
/**
* Default value using JSON data.
*/
value?: string | number;
/**
* Used for setting the value from source
*/
valueKey?: string;
}
export class Dropdown extends React.Component<DropdownProps, {}> { }
export default Dropdown;

View File

@ -1,247 +0,0 @@
/* eslint-disable */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import { themr } from 'react-css-themr';
import { DROPDOWN } from '../identifiers';
import InjectInput from '../input/Input';
import events from '../utils/events';
const factory = (Input) => {
class Dropdown extends Component {
static propTypes = {
allowBlank: PropTypes.bool,
allowClear: PropTypes.bool,
clearTooltip: PropTypes.string,
auto: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool,
error: PropTypes.string,
label: PropTypes.string,
labelKey: PropTypes.string,
name: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
required: PropTypes.bool,
source: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
])).isRequired,
template: PropTypes.func,
theme: PropTypes.shape({
active: PropTypes.string,
disabled: PropTypes.string,
dropdown: PropTypes.string,
error: PropTypes.string,
errored: PropTypes.string,
field: PropTypes.string,
label: PropTypes.string,
required: PropTypes.string,
selected: PropTypes.string,
templateValue: PropTypes.string,
up: PropTypes.string,
value: PropTypes.string,
values: PropTypes.string,
}),
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
valueKey: PropTypes.string,
};
static defaultProps = {
auto: true,
className: '',
allowBlank: true,
allowClear: false,
clearTooltip: 'Clear',
disabled: false,
labelKey: 'label',
required: false,
valueKey: 'value',
};
state = {
active: false,
up: false,
};
componentWillUpdate(nextProps, nextState) {
if (!this.state.active && nextState.active) {
events.addEventsToDocument(this.getDocumentEvents());
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.active && !this.state.active) {
events.removeEventsFromDocument(this.getDocumentEvents());
}
}
componentWillUnmount() {
if (this.state.active) {
events.removeEventsFromDocument(this.getDocumentEvents());
}
}
getDocumentEvents = () => ({
click: this.handleDocumentClick,
touchend: this.handleDocumentClick,
});
getSelectedItem = () => {
for (const item of this.props.source) {
if (item[this.props.valueKey] === this.props.value) return item;
}
return !this.props.allowBlank
? this.props.source[0]
: undefined;
};
handleSelect = (item, event) => {
if (!this.props.disabled && this.props.onChange) {
if (this.props.name) event.target.name = this.props.name;
this.props.onChange(item, event);
this.close();
}
};
handleClick = (event) => {
this.open(event);
events.pauseEvent(event);
if (this.props.onClick) this.props.onClick(event);
};
handleDocumentClick = (event) => {
if (this.state.active && !events.targetIsDescendant(event, ReactDOM.findDOMNode(this))) {
this.setState({ active: false });
}
};
close = () => {
if (this.state.active) {
this.setState({ active: false });
if (this.props.onBlur) this.props.onBlur(event);
}
}
open = (event) => {
if (this.state.active) return;
const client = event.target.getBoundingClientRect();
const screenHeight = window.innerHeight || document.documentElement.offsetHeight;
const up = this.props.auto ? client.top > ((screenHeight / 2) + client.height) : false;
this.setState({ active: true, up });
if (this.props.onFocus) this.props.onFocus(event);
};
handleFocus = (event) => {
event.stopPropagation();
if (!this.props.disabled) this.open(event);
};
handleBlur = (event) => {
event.stopPropagation();
if (this.state.active) this.close();
}
renderTemplateValue(selected) {
const { theme } = this.props;
const className = classnames(theme.field, {
[theme.errored]: this.props.error,
[theme.disabled]: this.props.disabled,
[theme.required]: this.props.required,
});
return (
<div className={className} onClick={this.handleClick}>
<div className={`${theme.templateValue} ${theme.value}`}>
{this.props.template(selected)}
</div>
{this.props.label
? (
<label className={theme.label}>
{this.props.label}
{this.props.required ? <span className={theme.required}> * </span> : null}
</label>
) : null}
{this.props.error ? <span className={theme.error}>{this.props.error}</span> : null}
</div>
);
}
renderValue = (item, idx) => {
const { labelKey, theme, valueKey } = this.props;
const className = classnames({
[theme.selected]: item[valueKey] === this.props.value,
[theme.disabled]: item.disabled,
});
return (
<li
key={idx}
className={className}
onMouseDown={!item.disabled && this.handleSelect.bind(this, item[valueKey])}
>
{this.props.template ? this.props.template(item) : item[labelKey]}
</li>
);
};
render() {
const {
allowBlank, allowClear, clearTooltip, auto, labelKey, required, onChange, onFocus, onBlur, // eslint-disable-line no-unused-vars
source, template, theme, valueKey, ...others
} = this.props;
const selected = this.getSelectedItem();
const className = classnames(theme.dropdown, {
[theme.up]: this.state.up,
[theme.active]: this.state.active,
[theme.disabled]: this.props.disabled,
[theme.required]: this.props.required,
[theme.withclear]: allowClear && selected,
}, this.props.className);
return (
<div
className={className}
style={{outline: 'none'}}
data-react-toolbox="dropdown"
tabIndex="-1"
>
{allowClear && selected ? <span
className={'material-icons '+theme.clear}
title={clearTooltip}
onClick={(e) => this.props.onChange(null, e)}>clear</span> : null}
<Input
{...others}
tabIndex="0"
className={theme.value}
onClick={this.handleClick}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
required={this.props.required}
readOnly
type={template && selected ? 'hidden' : null}
theme={theme}
themeNamespace="input"
value={selected && selected[labelKey] ? selected[labelKey] : ''}
/>
{template && selected ? this.renderTemplateValue(selected) : null}
<ul className={theme.values} style={this.state.up ? {bottom: '100%'} : {top: '100%'}}>
{source.map(this.renderValue)}
</ul>
</div>
);
}
}
return Dropdown;
};
const Dropdown = factory(InjectInput);
export default themr(DROPDOWN)(Dropdown);
export { factory as dropdownFactory };
export { Dropdown };

View File

@ -1,10 +0,0 @@
:root {
--dropdown-value-border-size: calc(var(--input-field-height) / 7);
--dropdown-color-white: var(--color-white);
--dropdown-color-primary: var(--color-primary);
--dropdown-color-primary-contrast: var(--color-primary-contrast);
--dropdown-color-disabled: color(var(--color-black) a(26%));
--dropdown-value-hover-background: var(--palette-grey-200);
--dropdown-overflow-max-height: 45vh;
--dropdown-value-border-radius: calc(0.2 * var(--unit));
}

View File

@ -1,5 +0,0 @@
import { Dropdown } from './Dropdown';
export { DropdownProps, DropdownTheme } from './Dropdown';
export { Dropdown }
export default Dropdown;

View File

@ -1,11 +1,26 @@
/* eslint-disable */
import { themr } from 'react-css-themr';
import { DROPDOWN } from '../identifiers';
import { dropdownFactory } from './Dropdown';
import { autocompleteFactory } from '../autocomplete';
import { Chip } from '../chip';
import { Input } from '../input';
import theme from './theme.css';
const Dropdown = dropdownFactory(Input);
const ThemedDropdown = themr(DROPDOWN, theme)(Dropdown);
import theme from '../autocomplete/theme.css';
import overrides from './overrides.css';
export default ThemedDropdown;
export { ThemedDropdown as Dropdown };
const overriddenTheme = { ...theme, inputInputElement: theme.inputInputElement+' '+overrides.inputInputElement };
const Autocomplete = autocompleteFactory(Chip, Input);
const ThemedAutocomplete = themr(AUTOCOMPLETE, overriddenTheme, { withRef: true })(Autocomplete);
export const Dropdown = (props) => <Autocomplete
readOnly
multiple={false}
keepFocusOnChange={false}
allowCreate={false}
showSelectedWhenNotInSource={false}
suggestionMatch="disabled"
{...props}
/>;
export default Dropdown;

View File

@ -0,0 +1,4 @@
.inputInputElement {
caret-color: transparent;
cursor: pointer;
}

View File

@ -1,73 +0,0 @@
# Dropdown
The Dropdown selects an option between multiple selections. The element displays the current state and a down arrow. When it is clicked, it displays the list of available options.
<!-- example -->
```jsx
import Dropdown from 'react-toolbox/lib/dropdown';
const countries = [
{ value: 'EN-gb', label: 'England' },
{ value: 'ES-es', label: 'Spain'},
{ value: 'TH-th', label: 'Thailand' },
{ value: 'EN-en', label: 'USA'}
];
class DropdownTest extends React.Component {
state = { value: 'ES-es' };
handleChange = (value) => {
this.setState({value: value});
};
render () {
return (
<Dropdown
auto
onChange={this.handleChange}
source={countries}
value={this.state.value}
/>
);
}
}
```
If you want to provide a theme via context, the component key is `RTDropdown`.
## Properties
| Name | Type | Default | Description |
|:-------------|:-----------|:--------|:------------|
| `allowBlank` | `Boolean` | `true` | If false the dropdown will preselect the first item if the supplied value matches none of the options' values.|
| `auto` | `Boolean` | `true` | If true, the dropdown will open up or down depending on the position in the screen.|
| `className` | `String` | `''` | Set the class to give custom styles to the dropdown.|
| `disabled` | `Boolean` | `false` | Set the component as disabled.|
| `error` | `String` | | Give an error string to display under the field.|
| `label` | `String` | | The text string to use for the floating label element.|
| `onBlur` | `Function` | | Callback function that is fired when the component is blurred.|
| `onChange` | `Function` | | Callback function that is fired when the component's value changes.|
| `onFocus` | `Function` | | Callback function that is fired when the component is focused.|
| `source` | `Array` | | Array of data objects with the data to represent in the dropdown.|
| `template` | `Function` | | Callback function that returns a JSX template to represent the element.|
| `value` | `String` | | Default value using JSON data.|
| `required` | `Boolean` | `false` | If true, the dropdown has a required attribute.|
## Theming
This component uses an `Input` under the covers. The theme object is passed down namespaced under `input` keyword. This means you can use the same theme classNames from `Input` component but namespaced with `input`. For example, to style the label you have to use `inputLabel` className.
| Name | Description|
|:----------------|:-----------|
| `active` | Added to the root element when the dropdown is active.|
| `disabled` | Added to the root element when it's disabled.|
| `dropdown` | Root element class.|
| `error` | Used for the error element.|
| `errored` | Added to the inner wrapper if it's errored.|
| `field` | Used for the inner wrapper of the component.|
| `label` | Used for the the label element.|
| `selected` | Used to highlight the selected value.|
| `templateValue` | Used as a wrapper for the given template value.|
| `up` | Added to the root element when it's opening up.|
| `value` | Used for each value in the dropdown component.|
| `values` | Used for the list of values.|

View File

@ -1,180 +0,0 @@
@import '../colors.css';
@import '../variables.css';
@import '../input/config.css';
@import './config.css';
.dropdown {
position: relative;
@apply --reset;
&:not(.active) {
& > .values {
max-height: 0;
visibility: hidden;
}
}
&.active {
& > .label,
& > .value {
opacity: 0.5;
}
& > .values {
box-shadow: var(--zdepth-shadow-1);
max-height: var(--dropdown-overflow-max-height);
visibility: visible;
}
}
&:not(.up) > .values {
bottom: auto;
top: 0;
}
&.up > .values {
bottom: 0;
top: auto;
}
&.disabled {
cursor: normal;
pointer-events: none;
}
&.withclear input {
text-indent: 28px !important;
}
}
.value {
& > input {
cursor: pointer;
}
&::after {
border-left: var(--dropdown-value-border-size) solid transparent;
border-right: var(--dropdown-value-border-size) solid transparent;
border-top: var(--dropdown-value-border-size) solid var(--input-text-bottom-border-color);
content: '';
height: 0;
pointer-events: none;
position: absolute;
right: var(--input-chevron-offset);
top: 50%;
transition: transform var(--animation-duration) var(--animation-curve-default);
width: 0;
}
}
.field {
cursor: pointer;
padding: var(--input-padding) 0;
position: relative;
&.errored {
padding-bottom: 0;
& > .label {
color: var(--input-text-error-color);
}
& > .templateValue {
border-bottom: 1px solid var(--input-text-error-color);
}
& > .label > .required {
color: var(--input-text-error-color);
}
}
&.disabled {
cursor: normal;
pointer-events: none;
& > .templateValue {
border-bottom-style: dotted;
opacity: 0.7;
}
}
}
.templateValue {
background-color: var(--input-text-background-color);
border-bottom: 1px solid var(--input-text-bottom-border-color);
color: var(--color-text);
min-height: var(--input-field-height);
padding: var(--input-field-padding) 0;
position: relative;
}
.label {
color: var(--input-text-label-color);
font-size: var(--input-label-font-size);
left: 0;
line-height: var(--input-field-font-size);
position: absolute;
top: var(--input-focus-label-top);
& .required {
color: var(--input-text-error-color);
}
}
.error {
color: var(--input-text-error-color);
font-size: var(--input-label-font-size);
line-height: var(--input-underline-height);
margin-bottom: calc(-1 * var(--input-underline-height));
}
.values {
background-color: var(--dropdown-color-white);
border-radius: var(--dropdown-value-border-radius);
list-style: none;
margin: 0;
overflow-y: auto;
padding: 0;
position: absolute;
transition-duration: var(--animation-duration);
transition-property: max-height, box-shadow;
transition-timing-function: var(--animation-curve-default);
width: 100%;
z-index: var(--z-index-high);
& > * {
cursor: pointer;
overflow: hidden;
padding: var(--unit);
position: relative;
&:hover:not(.disabled) {
background-color: var(--dropdown-value-hover-background);
}
&.selected {
color: var(--dropdown-color-primary);
}
&.disabled {
color: var(--dropdown-color-disabled);
cursor: not-allowed;
}
}
&::-webkit-scrollbar {
height: 0;
width: 0;
}
}
.clear {
cursor: pointer;
display: block;
left: -4px;
padding: 4px;
position: absolute;
top: 12px;
z-index: 10;
}

View File

@ -2,7 +2,7 @@
"name": "react-toolbox",
"description": "A set of React components implementing Google's Material Design specification with the power of CSS Modules.",
"homepage": "http://www.react-toolbox.io",
"version": "2.0.0-beta.22",
"version": "2.0.0-beta.23",
"main": "./lib",
"module": "./components",
"author": {