Skip to content

Commit

Permalink
clean up and add Autocomplete (private)
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed May 1, 2017
1 parent 246a766 commit 8fe4685
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 13 deletions.
20 changes: 12 additions & 8 deletions packages/docs/components/pages/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ return (

{/* this is also supported but leads to much larger bundles */}
<DateTimePicker format="mmm YY" />
<NumberPicker current={{ currency: 'USD', style: 'accounting' }} />
<NumberPicker format={{ currency: 'USD', style: 'accounting' }} />
</div>
)
```
Expand Down Expand Up @@ -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
}
}
Expand Down
320 changes: 320 additions & 0 deletions packages/react-widgets/src/Autocomplete.js
Original file line number Diff line number Diff line change
@@ -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 (
<List
ref="list"
{...props}
id={listId}
activeId={activeId}
valueAccessor={accessors.value}
textAccessor={accessors.text}
selectedItem={selectedItem}
focusedItem={open ? focusedItem : null}
aria-hidden={!open}
aria-labelledby={inputId}
aria-live={open && 'polite'}
onSelect={this.handleSelect}
onMove={this.handleScroll}
messages={messages}
/>
)
}

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 (
<Widget
{...elementProps}
onBlur={this.focusManager.handleBlur}
onFocus={this.focusManager.handleFocus}
onKeyDown={this.handleKeyDown}
className={cn(className, 'rw-autocomplete')}
>
<WidgetPicker
picker={false}
open={open}
dropUp={dropUp}
focused={focused}
disabled={disabled}
readOnly={readOnly}
>
<Input
{...inputProps}
ref='input'
role='combobox'
id={this.inputId}
autoFocus={autoFocus}
disabled={disabled === true}
readOnly={readOnly === true}
aria-busy={!!busy}
aria-owns={this.listId}
aria-autocomplete="list"
aria-activedescendant={open ? this.activeId : null}
aria-expanded={open}
aria-haspopup={true}
placeholder={placeholder}
value={this.accessors.text(valueItem)}
onChange={this.handleInputChange}
onKeyDown={this.handleInputKeyDown}
/>
<SelectComponent
busy={busy}
aria-hidden="true"
role="presentational"
disabled={disabled || readOnly}
label={messages.openDropdown(this.props)}
/>
</WidgetPicker>

{!!value && !!data.length && shouldRenderPopup &&
<Popup
open={open}
dropUp={dropUp}
duration={duration}
onOpening={() => this.refs.list.forceUpdate()}
>
<div>
{this.renderList(messages)}
</div>
</Popup>
}
</Widget>
);
}

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']);
5 changes: 4 additions & 1 deletion packages/react-widgets/src/DropdownList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -217,6 +218,7 @@ class DropdownList extends React.Component {
, placeholder
, value
, open
, inputProps
, valueComponent } = this.props;

let { focused } = this.state;
Expand Down Expand Up @@ -265,6 +267,7 @@ class DropdownList extends React.Component {
className="rw-widget-input"
>
<DropdownListInput
{...inputProps}
value={valueItem}
textField={textField}
placeholder={placeholder}
Expand Down
Loading

0 comments on commit 8fe4685

Please sign in to comment.