diff --git a/packages/docs/components/pages/i18n.md b/packages/docs/components/pages/i18n.md index e0267b1d2..33f57c6a8 100644 --- a/packages/docs/components/pages/i18n.md +++ b/packages/docs/components/pages/i18n.md @@ -84,7 +84,7 @@ return ( {/* this is also supported but leads to much larger bundles */} - + ) ``` @@ -203,19 +203,23 @@ var localizer = { formats: { day: 'DD', month: 'mmm', - // we always pass a function for more advanced formats such as returning a year 'range' - // to represent a decade e.g "2000 - 2009". Notice the localizer instance is the third argument, - // which can be used to format or parse as needed. - decade: (date, culture, localizer) => { - return localizer.format(date, 'YYYY') + ' - ' + localizer.format(lastYearOfDecade(date), 'YYYY') + // function formats are useful for more advanced formatting, such as a + // year 'range' to represent a decade e.g "2000 - 2009". + // Notice the localizer instance is the third argument, which can be + // used to format or parse as needed. + decade: (date, cultureStr, localizer) => { + return ( + localizer.format(date, 'YYYY') + ' - ' + + localizer.format(lastYearOfDecade(date), 'YYYY') + ) } }, - parse(value, format, cultureStr){ + parse(value, format, cultureStr) { return parsedDate }, - format(value, format, cultureStr){ + format(value, format, cultureStr) { return formattedDatestring } } diff --git a/packages/react-widgets/src/Autocomplete.js b/packages/react-widgets/src/Autocomplete.js new file mode 100644 index 000000000..af8c28107 --- /dev/null +++ b/packages/react-widgets/src/Autocomplete.js @@ -0,0 +1,320 @@ +import cn from 'classnames'; +import * as PropTypes from 'prop-types'; +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import uncontrollable from 'uncontrollable'; + +import List from './List'; +import Popup from './Popup'; +import Input from './Input'; +import Select from './Select'; +import Widget from './Widget'; +import WidgetPicker from './WidgetPicker'; +import { getMessages } from './messages'; +import focusManager from './util/focusManager'; +import listDataManager from './util/listDataManager'; +import * as CustomPropTypes from './util/PropTypes'; +import accessorManager from './util/accessorManager'; +import scrollManager from './util/scrollManager'; +import * as Props from './util/Props'; +import withRightToLeft from './util/withRightToLeft'; +import { widgetEditable } from './util/interaction'; +import { instanceId, notify, isFirstFocusedRender } from './util/widgetHelpers'; + +const propTypes = { + //-- controlled props ----------- + value: PropTypes.any, + onChange: PropTypes.func, + open: PropTypes.bool, + onToggle: PropTypes.func, + //------------------------------------ + + itemComponent: CustomPropTypes.elementType, + selectComponent: CustomPropTypes.elementType, + listComponent: CustomPropTypes.elementType, + groupComponent: CustomPropTypes.elementType, + groupBy: CustomPropTypes.accessor, + + data: PropTypes.array, + valueField: CustomPropTypes.accessor, + textField: CustomPropTypes.accessor, + + onKeyDown: PropTypes.func, + onSelect: PropTypes.func, + autoFocus: PropTypes.bool, + disabled: CustomPropTypes.disabled.acceptsArray, + readOnly: CustomPropTypes.disabled, + busy: PropTypes.bool, + + delay: PropTypes.number, + dropUp: PropTypes.bool, + duration: PropTypes.number, + + placeholder: PropTypes.string, + inputProps: PropTypes.object, + listProps: PropTypes.object, + messages: PropTypes.shape({ + openCombobox: CustomPropTypes.message, + emptyList: CustomPropTypes.message, + emptyFilter: CustomPropTypes.message + }) +}; + +@withRightToLeft +class Autocomplete extends React.Component { + + static defaultProps = { + data: [], + open: false, + listComponent: List, + selectComponent: Select, + }; + + constructor(props, context) { + super(props, context); + + this.messages = getMessages(props.messages) + this.inputId = instanceId(this, '_input') + this.listId = instanceId(this, '_listbox') + this.activeId = instanceId(this, '_listbox_active_option') + + this.list = listDataManager(this) + this.accessors = accessorManager(this) + this.handleScroll = scrollManager(this) + this.focusManager = focusManager(this, { + didHandle: this.handleFocusChanged + }) + + this.state = { + ...this.getStateFromProps(props), + open: false, + }; + } + + componentWillReceiveProps(nextProps) { + this.messages = getMessages(nextProps.messages) + this.setState(this.getStateFromProps(nextProps)) + } + + getStateFromProps(props) { + let { accessors, list } = this + let { value, data } = props; + let { focusedItem = null } = this.state || {}; + + let index = accessors.indexOf(data, value); + list.setData(data); + + return { + data, + selectedItem: list.nextEnabled(data[index]), + focusedItem: ~index ? list.nextEnabled(data[index]) : focusedItem + } + } + + handleFocusChanged = (focused) => { + if (!focused) this.close() + }; + + @widgetEditable + handleSelect = (data, originalEvent) => { + this.close() + notify(this.props.onSelect, [data, { originalEvent }]) + this.change(data, originalEvent) + this.focus(); + }; + + handleInputChange = (event) => { + this.change(event.target.value, event) + this.open() + }; + + @widgetEditable + handleKeyDown = (e) => { + let key = e.key + , list = this.list + , focusedItem = this.state.focusedItem + , isOpen = this.props.open; + + notify(this.props.onKeyDown, [e]) + + if (e.defaultPrevented) return + + if (!isOpen) { + if (key === 'ArrowDown') this.open() + return; + } + + if (key === 'End') { + e.preventDefault() + this.setState({ focusedItem: list.last() }) + } + else if (key === 'Home') { + e.preventDefault() + this.setState({ focusedItem: list.first() }) + } + else if (key === 'Escape') + this.close() + + else if (key === 'Enter') { + if (!focusedItem) { + return void this.close(); + } + + e.preventDefault(); + this.handleSelect(focusedItem, e) + this.change(focusedItem, false, e) + } + else if (key === 'ArrowDown') { + e.preventDefault() + this.setState({ focusedItem: list.next(focusedItem) }) + } + else if ( key === 'ArrowUp' ) { + e.preventDefault() + this.setState({ focusedItem: list.prev(focusedItem) }) + } + }; + + renderList(messages) { + let { activeId, inputId, listId, accessors } = this; + + let { open } = this.props; + let { selectedItem, focusedItem } = this.state; + let List = this.props.listComponent + let props = this.list.defaultProps(); + + return ( + + ) + } + + render() { + let { + className + , duration + , data + , value + , busy + , dropUp + , open + , autoFocus + , placeholder + , inputProps + , selectComponent: SelectComponent } = this.props; + + let { focused } = this.state; + + let disabled = this.props.disabled === true + , readOnly = this.props.readOnly === true + + let elementProps = Props.pickElementProps(this); + let shouldRenderPopup = open || isFirstFocusedRender(this); + + let messages = this.messages + let valueItem = this.accessors.findOrSelf(data, value) + + return ( + + + + + + + {!!value && !!data.length && shouldRenderPopup && + this.refs.list.forceUpdate()} + > + + {this.renderList(messages)} + + + } + + ); + } + + focus() { + this.refs.input && + findDOMNode(this.refs.input).focus() + } + + change(nextValue, originalEvent) { + const { onChange, value: lastValue } = this.props; + notify(onChange, [nextValue, { + lastValue, + originalEvent, + }]) + } + + open() { + if (!this.props.open) + notify(this.props.onToggle, true) + } + + close() { + this.setState({ focusedItem: null }, () => { + notify(this.props.onToggle, false) + }) + } +} + +Autocomplete.propTypes = propTypes; + +export default uncontrollable(Autocomplete, { + open: 'onToggle', + value: 'onChange', +}, ['focus']); diff --git a/packages/react-widgets/src/DropdownList.js b/packages/react-widgets/src/DropdownList.js index 7c0046cae..21927024b 100644 --- a/packages/react-widgets/src/DropdownList.js +++ b/packages/react-widgets/src/DropdownList.js @@ -2,7 +2,6 @@ import React from 'react'; import { findDOMNode } from 'react-dom'; import PropTypes from 'prop-types'; import activeElement from 'dom-helpers/activeElement'; -import contains from 'dom-helpers/query/contains'; import cn from 'classnames'; import { autoFocus, mountManager, timeoutManager } from 'react-component-managers'; @@ -64,6 +63,8 @@ class DropdownList extends React.Component { disabled: CustomPropTypes.disabled.acceptsArray, readOnly: CustomPropTypes.disabled, + + inputProps: PropTypes.object, listProps: PropTypes.object, messages: PropTypes.shape({ @@ -217,6 +218,7 @@ class DropdownList extends React.Component { , placeholder , value , open + , inputProps , valueComponent } = this.props; let { focused } = this.state; @@ -265,6 +267,7 @@ class DropdownList extends React.Component { className="rw-widget-input" > + diff --git a/packages/storybook/stories/Autocomplete.js b/packages/storybook/stories/Autocomplete.js new file mode 100644 index 000000000..0f5b009a7 --- /dev/null +++ b/packages/storybook/stories/Autocomplete.js @@ -0,0 +1,123 @@ +import React from 'react'; +import { storiesOf } from '@kadira/storybook'; + +import Container from './Container'; +import Autocomplete from 'react-widgets/lib/Autocomplete'; + +let generateNames = global.generateNames; + +let props = { + data: generateNames(), + valueField: 'id', + textField: 'fullName', + placeholder: 'type something…', +} + +class PlacesAutoComplete extends React.Component { + state = { data: [], value: '' }; + autocompleteService = new window.google.maps.places.AutocompleteService(); + + handleChange = (value) => { + this.setState({ value }) + + if (!value || value.placeId) return; + + this.setState({ busy: true }) + + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.autocompleteService + .getPlacePredictions({ input: value.text || value }, (predictions) => { + this.setState({ busy: false }) + if (!predictions) return; + + this.setState({ + data: predictions.map(p => ({ + text: p.description, + placeId: p.place_id, + ...p.structured_formatting, + })) + }) + }); + }, 500) + } + + render() { + return ( + + ) + } +} + + +storiesOf('Autocomplete', module) + .add('Autocomplete', () => + + + + ) + .add('busy', () => + + + + ) + .add('disabled', () => + + + + ) + .add('disabled items', () => + + + + ) + .add('disabled item, first focused', () => + + + + ) + .add('readOnly', () => + + + + ) + .add('right to left', () => + + + + ) diff --git a/packages/storybook/stories/Container.js b/packages/storybook/stories/Container.js index 68a8a8397..26564269e 100644 --- a/packages/storybook/stories/Container.js +++ b/packages/storybook/stories/Container.js @@ -13,7 +13,11 @@ class Container extends React.Component { margin: '30px auto', ...this.props.style, }} - /> + > + + {this.props.children} + + ); } }