Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DatePicker] Add ability to use keyboard to write date #5677

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import DatePicker from 'material-ui/DatePicker';

/**
* This example allows you to use keyboard to write date.
*
* Unfortunately by now it is only working with default locale (`en-US`).
* In other cases `keyboardEnabled` prop will be ignored.
* Also it will not work if it is controlled component or has disabled dates.
*/
const DatePickerExampleKeyboardInput = () => (
<div>
<DatePicker
hintText="YYYY-MM-DD"
keyboardEnabled={true}
/>
</div>
);

export default DatePickerExampleKeyboardInput;
8 changes: 8 additions & 0 deletions docs/src/app/components/pages/components/DatePicker/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import DatePickerExampleDisableDates from './ExampleDisableDates';
import datePickerExampleDisableDatesCode from '!raw!./ExampleDisableDates';
import DatePickerExampleInternational from './ExampleInternational';
import datePickerExampleInternationalCode from '!raw!./ExampleInternational';
import DatePickerExampleKeyboardInput from './ExampleKeyboardInput';
import datePickerExampleKeyboardInputCode from '!raw!./ExampleKeyboardInput';
import datePickerCode from '!raw!material-ui/DatePicker/DatePicker';

const DatePickerPage = () => (
Expand Down Expand Up @@ -60,6 +62,12 @@ const DatePickerPage = () => (
>
<DatePickerExampleInternational />
</CodeExample>
<CodeExample
title="Keyboard input example"
code={datePickerExampleKeyboardInputCode}
>
<DatePickerExampleKeyboardInput />
</CodeExample>
<PropTypeDescription code={datePickerCode} />
</div>
);
Expand Down
111 changes: 104 additions & 7 deletions src/DatePicker/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import React, {Component, PropTypes} from 'react';
import {dateTimeFormat, formatIso, isEqualDate} from './dateUtils';
import DatePickerDialog from './DatePickerDialog';
import TextField from '../TextField';
import IconButton from '../IconButton';
import DateRangeIcon from '../svg-icons/action/date-range';

export function getStyles() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exporting this function?

const styles = {
iconButtonStyle: {
verticalAlign: 'top',
},
};

return styles;
}

class DatePicker extends Component {
static propTypes = {
Expand Down Expand Up @@ -50,6 +62,10 @@ class DatePicker extends Component {
* Disables the DatePicker.
*/
disabled: PropTypes.bool,
/**
* Text for validation error.
*/
errorText: PropTypes.string,
/**
* Used to change the first day of week. It varies from
* Saturday to Monday between different locales.
Expand All @@ -65,6 +81,15 @@ class DatePicker extends Component {
* @returns {any} The formatted date.
*/
formatDate: PropTypes.func,
/**
* Override the inline-styles of calendar IconButton (it is showing when `keyboardEnabled` is `true`).
*/
iconButtonStyle: PropTypes.object,
/**
* Tells the datepicker to handle keyboard input.
* Will not work if `locale` or `shouldDisableDate` props are specified or if it is controlled component.
*/
keyboardEnabled: PropTypes.bool,
/**
* Locale used for formatting the `DatePicker` date strings. Other than for 'en-US', you
* must provide a `DateTimeFormat` that supports the chosen `locale`.
Expand Down Expand Up @@ -140,7 +165,9 @@ class DatePicker extends Component {
container: 'dialog',
disabled: false,
disableYearSelection: false,
errorText: 'Enter a valid date in "YYYY-MM-DD" format',
firstDayOfWeek: 1,
keyboardEnabled: false,
style: {},
};

Expand All @@ -150,11 +177,15 @@ class DatePicker extends Component {

state = {
date: undefined,
hasError: false,
textDate: '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for this new state?

};

componentWillMount() {
const defaultDate = this.isControlled() ? this.getControlledDate() : this.props.defaultDate;
this.setState({
date: this.isControlled() ? this.getControlledDate() : this.props.defaultDate,
date: defaultDate,
textDate: defaultDate ? this.formatDate(defaultDate) : '',
});
}

Expand All @@ -164,6 +195,7 @@ class DatePicker extends Component {
if (!isEqualDate(this.state.date, newDate)) {
this.setState({
date: newDate,
textDate: this.formatDate(newDate),
});
}
}
Expand Down Expand Up @@ -200,10 +232,18 @@ class DatePicker extends Component {
this.openDialog();
}

shouldHandleKeyboard() {
return this.props.keyboardEnabled && !this.props.disabled &&
!this.props.locale && !this.props.shouldDisableDate &&
!this.isControlled();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about this feature being disabled for controlled component.
On the next branch, we only focus on stateless component, hence, they are all controlled at their lowest implementation level.
What's the issue?

}


handleAccept = (date) => {
if (!this.isControlled()) {
this.setState({
date: date,
textDate: this.formatDate(date),
});
}
if (this.props.onChange) {
Expand All @@ -212,12 +252,37 @@ class DatePicker extends Component {
};

handleFocus = (event) => {
event.target.blur();
if (!this.shouldHandleKeyboard()) {
event.target.blur();
}

if (this.props.onFocus) {
this.props.onFocus(event);
}
};

handleBlur = () => {
if (this.shouldHandleKeyboard()) {
if (this.getDate()) {
this.setState({
textDate: this.formatDate(this.getDate()),
});
}

this.setState({
hasError: !this.getDate() && !!this.state.textDate.trim(),
});
}
}

handleTextChange = (event, value) => {
const parsedDate = this.parseDate(value);
this.setState({
date: parsedDate,
textDate: value,
});
};

handleTouchTap = (event) => {
if (this.props.onTouchTap) {
this.props.onTouchTap(event);
Expand All @@ -241,7 +306,9 @@ class DatePicker extends Component {
}

formatDate = (date) => {
if (this.props.locale) {
if (this.props.formatDate) {
return this.props.formatDate(date);
} else if (this.props.locale) {
const DateTimeFormat = this.props.DateTimeFormat || dateTimeFormat;
return new DateTimeFormat(this.props.locale, {
day: 'numeric',
Expand All @@ -253,6 +320,21 @@ class DatePicker extends Component {
}
};

parseDate = (textDate) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to allow users using a different date format.
Like DD-MM-YYYY.

// regex to parse date is based on ISO 8601 (YYYY-MM-DD)
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;
const match = reg.exec(textDate);
if (!match) {
return;
}

const year = match[1];
const month = match[2] === 0 ? 11 : (match[2] - 1);
const day = match[3];

return new Date(year, month, day);
};

render() {
const {
DateTimeFormat,
Expand All @@ -264,7 +346,8 @@ class DatePicker extends Component {
dialogContainerStyle,
disableYearSelection,
firstDayOfWeek,
formatDate: formatDateProp,
formatDate, // eslint-disable-line no-unused-vars
iconButtonStyle,
locale,
maxDate,
minDate,
Expand All @@ -277,22 +360,36 @@ class DatePicker extends Component {
shouldDisableDate,
style,
textFieldStyle,
keyboardEnabled, // eslint-disable-line no-unused-vars
errorText,
...other
} = this.props;

const {prepareStyles} = this.context.muiTheme;
const formatDate = formatDateProp || this.formatDate;
const styles = getStyles(this.props, this.context);

return (
<div className={className} style={prepareStyles(Object.assign({}, style))}>
<TextField
{...other}
onBlur={this.handleBlur}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, that's a breaking change.

onChange={this.handleTextChange}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, that's a breaking change.

onFocus={this.handleFocus}
onTouchTap={this.handleTouchTap}
onTouchTap={!this.shouldHandleKeyboard() && this.handleTouchTap}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, that's a breaking change.

ref="input"
style={textFieldStyle}
value={this.state.date ? formatDate(this.state.date) : ''}
errorText={this.state.hasError && errorText}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a breaking change as users might be using this errorText property independently from the hasError state. I would rather move that error logic away into another state error wrapper component.

value={this.state.textDate}
/>
{this.shouldHandleKeyboard() &&
<IconButton
onTouchTap={this.handleTouchTap}
style={Object.assign({}, styles.iconButtonStyle, iconButtonStyle)}
touch={true}
>
<DateRangeIcon />
</IconButton>
}
<DatePickerDialog
DateTimeFormat={DateTimeFormat}
autoOk={autoOk}
Expand Down