DateRangeFilterPicker

This commit is contained in:
Paul Sealock 2018-12-11 14:50:26 +13:00
parent bbb42e601c
commit 524bb0c96f
15 changed files with 367 additions and 84 deletions

View File

@ -1,3 +1,56 @@
`DatePicker` (component)
========================
Props
-----
### `date`
- Type: Object
- Default: null
A moment date object representing the selected date. `null` for no selection.
### `text`
- Type: String
- Default: null
The date in human-readable format. Displayed in the text input.
### `error`
- Type: String
- Default: null
A string error message, shown to the user.
### `invalidDays`
- Type: One of type: enum, func
- Default: null
(Coming Soon) Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted.
A function will be passed to react-dates' `isOutsideRange` prop
### `onUpdate`
- **Required**
- Type: Function
- Default: null
A function called upon selection of a date or input change.
### `dateFormat`
- **Required**
- Type: String
- Default: null
The date format in moment.js-style tokens.
`DateRange` (component)
=======================

View File

@ -144,8 +144,8 @@ The query string represented in object form
Which type of autocompleter should be used in the Search
`DatePicker` (component)
========================
`DateRangeFilterPicker` (component)
===================================
Select a range of dates or single dates.

View File

@ -24,7 +24,7 @@ Function called when selected results change, passed result list.
### `type`
- **Required**
- Type: One of: 'products', 'product_cats', 'orders', 'customers', 'coupons', 'taxes', 'variations'
- Type: One of: 'countries', 'coupons', 'customers', 'emails', 'orders', 'products', 'product_cats', 'taxes', 'usernames', 'variations'
- Default: null
The object type to be used in searching.
@ -39,7 +39,7 @@ A placeholder for the search input.
### `selected`
- Type: Array
- id: Number
- id: One of type: number, string
- label: String
- Default: `[]`

View File

@ -11,7 +11,7 @@ Props
### `id`
- Type: Number
- Type: One of type: number, string
- Default: null
The ID for this item, used in the remove function.

View File

@ -0,0 +1,141 @@
/** @format */
/**
* External dependencies
*/
import 'core-js/fn/object/assign';
import 'core-js/fn/array/from';
import { __, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Dropdown, DatePicker as WpDatePicker } from '@wordpress/components';
import { partial } from 'lodash';
import { TAB } from '@wordpress/keycodes';
import moment from 'moment';
/**
* Internal dependencies
*/
import DateInput from './input';
import { toMoment } from '@woocommerce/date';
import { H, Section } from '../section';
import PropTypes from 'prop-types';
class DatePicker extends Component {
constructor( props ) {
super( props );
this.onDateChange = this.onDateChange.bind( this );
this.onInputChange = this.onInputChange.bind( this );
}
handleKeyDown( isOpen, onToggle, { keyCode } ) {
if ( TAB === keyCode && isOpen ) {
onToggle();
}
}
handleFocus( isOpen, onToggle ) {
if ( ! isOpen ) {
onToggle();
}
}
onDateChange( onToggle, dateString ) {
const { onUpdate, dateFormat } = this.props;
const date = moment( dateString );
onUpdate( {
date,
text: dateString ? date.format( dateFormat ) : '',
error: null,
} );
onToggle();
}
onInputChange( event ) {
const value = event.target.value;
const { dateFormat } = this.props;
const date = toMoment( dateFormat, value );
const error = date ? null : __( 'Invalid date', 'wc-admin' );
this.props.onUpdate( {
date,
text: value,
error: value.length > 0 ? error : null,
} );
}
render() {
const { date, text, dateFormat, error } = this.props;
// @TODO: make upstream Gutenberg change to invalidate certain days.
// const isOutsideRange = getOutsideRange( invalidDays );
return (
<Dropdown
position="bottom center"
focusOnMount={ false }
renderToggle={ ( { isOpen, onToggle } ) => (
<DateInput
value={ text }
onChange={ this.onInputChange }
dateFormat={ dateFormat }
label={ __( 'Choose a date', 'wc-admin' ) }
error={ error }
describedBy={ sprintf(
__( 'Date input describing a selected date in format %s', 'wc-admin' ),
dateFormat
) }
onFocus={ partial( this.handleFocus, isOpen, onToggle ) }
aria-expanded={ isOpen }
focusOnMount={ false }
onKeyDown={ partial( this.handleKeyDown, isOpen, onToggle ) }
errorPosition="top center"
/>
) }
renderContent={ ( { onToggle } ) => (
<Section component={ false }>
<H className="woocommerce-calendar__date-picker-title">
{ __( 'select a date', 'wc-admin' ) }
</H>
<div className="woocommerce-calendar__react-dates is-core-datepicker">
<WpDatePicker
currentDate={ date }
onChange={ partial( this.onDateChange, onToggle ) }
/>
</div>
</Section>
) }
/>
);
}
}
DatePicker.propTypes = {
/**
* A moment date object representing the selected date. `null` for no selection.
*/
date: PropTypes.object,
/**
* The date in human-readable format. Displayed in the text input.
*/
text: PropTypes.string,
/**
* A string error message, shown to the user.
*/
error: PropTypes.string,
/**
* (Coming Soon) Optionally invalidate certain days. `past`, `future`, `none`, or function are accepted.
* A function will be passed to react-dates' `isOutsideRange` prop
*/
invalidDays: PropTypes.oneOfType( [
PropTypes.oneOf( [ 'past', 'future', 'none' ] ),
PropTypes.func,
] ),
/**
* A function called upon selection of a date or input change.
*/
onUpdate: PropTypes.func.isRequired,
/**
* The date format in moment.js-style tokens.
*/
dateFormat: PropTypes.string.isRequired,
};
export default DatePicker;

View File

@ -7,11 +7,7 @@ import 'core-js/fn/array/from';
import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { Component } from '@wordpress/element';
import {
DayPickerRangeController,
isInclusivelyAfterDay,
isInclusivelyBeforeDay,
} from 'react-dates';
import { DayPickerRangeController } from 'react-dates';
import moment from 'moment';
import { partial } from 'lodash';
import PropTypes from 'prop-types';
@ -27,6 +23,7 @@ import { validateDateInputForRange } from '@woocommerce/date';
*/
import DateInput from './input';
import phrases from './phrases';
import { getOutsideRange } from './utils';
/**
* This is wrapper for a [react-dates](https://github.com/airbnb/react-dates) powered calendar.
@ -38,7 +35,6 @@ class DateRange extends Component {
this.onDatesChange = this.onDatesChange.bind( this );
this.onFocusChange = this.onFocusChange.bind( this );
this.onInputChange = this.onInputChange.bind( this );
this.getOutsideRange = this.getOutsideRange.bind( this );
}
onDatesChange( { startDate, endDate } ) {
@ -76,22 +72,6 @@ class DateRange extends Component {
} );
}
getOutsideRange() {
const { invalidDays } = this.props;
if ( 'string' === typeof invalidDays ) {
switch ( invalidDays ) {
case 'past':
return day => isInclusivelyBeforeDay( day, moment() );
case 'future':
return day => isInclusivelyAfterDay( day, moment() );
case 'none':
default:
return undefined;
}
}
return 'function' === typeof invalidDays ? invalidDays : undefined;
}
setTnitialVisibleMonth( isDoubleCalendar, before ) {
return () => {
const visibleDate = before || moment();
@ -114,8 +94,9 @@ class DateRange extends Component {
shortDateFormat,
isViewportMobile,
isViewportSmall,
invalidDays,
} = this.props;
const isOutsideRange = this.getOutsideRange();
const isOutsideRange = getOutsideRange( invalidDays );
const isDoubleCalendar = isViewportMobile && ! isViewportSmall;
return (
<div

View File

@ -1,30 +1,60 @@
```jsx
import { DateRange } from '@woocommerce/components';
import { DateRange, DatePicker } from '@woocommerce/components';
import moment from 'moment';
const dateFormat = 'MM/DD/YYYY';
const MyDateRange = withState( {
after: moment( '2018-09-10' ),
afterText: '09/10/2018',
before: moment( '2018-09-20' ),
beforeText: '09/20/2018',
} )( ( { after, afterText, before, beforeText, setState } ) => {
function onUpdate( { after, afterText, before, beforeText } ) {
setState( { after, afterText, before, beforeText } );
after: null,
afterText: '',
before: null,
beforeText: '',
afterError: null,
beforeError: null,
focusedInput: 'startDate',
} )( ( { after, afterText, before, beforeText, afterError, beforeError, focusedInput, setState } ) => {
function onRangeUpdate( update ) {
setState( update );
}
function onDatePickerUpdate( { date, text, error } ) {
setState( {
after: date,
afterText: text,
afterError: error,
} );
}
return (
<DateRange
after={ after }
afterText={ afterText }
before={ before }
beforeText={ beforeText }
onUpdate={ onUpdate }
shortDateFormat={ dateFormat }
focusedInput="startDate"
invalidDays="none"
/>
<div>
<H>Date Range Picker</H>
<Section component={ false }>
<DateRange
after={ after }
afterText={ afterText }
before={ before }
beforeText={ beforeText }
onUpdate={ onRangeUpdate }
shortDateFormat={ dateFormat }
focusedInput={ focusedInput }
invalidDays="future"
/>
</Section>
<H>Date Picker</H>
<Section component={ false }>
<DatePicker
date={ after }
text={ afterText }
error={ afterError }
onUpdate={ onDatePickerUpdate }
dateFormat={ dateFormat }
invalidDays="none"
onUpdate={ onDatePickerUpdate }
invalidDays="future"
/>
</Section>
</div>
)
} );
```

View File

@ -7,7 +7,17 @@ import classnames from 'classnames';
import { uniqueId } from 'lodash';
import PropTypes from 'prop-types';
const DateInput = ( { value, onChange, dateFormat, label, describedBy, error } ) => {
const DateInput = ( {
value,
onChange,
dateFormat,
label,
describedBy,
error,
onFocus,
onKeyDown,
errorPosition,
} ) => {
const classes = classnames( 'woocommerce-calendar__input', {
'is-empty': value.length === 0,
'is-error': error,
@ -24,12 +34,14 @@ const DateInput = ( { value, onChange, dateFormat, label, describedBy, error } )
id={ id }
aria-describedby={ `${ id }-message` }
placeholder={ dateFormat.toLowerCase() }
onFocus={ onFocus }
onKeyDown={ onKeyDown }
/>
{ error && (
<Popover
className="woocommerce-calendar__input-error"
focusOnMount={ false }
position="bottom center"
position={ errorPosition }
>
{ error }
</Popover>
@ -49,6 +61,13 @@ DateInput.propTypes = {
label: PropTypes.string.isRequired,
describedBy: PropTypes.string.isRequired,
error: PropTypes.string,
errorPosition: PropTypes.string,
onFocus: PropTypes.func,
};
DateInput.defaultProps = {
onFocus: () => {},
errorPosition: 'bottom center',
};
export default DateInput;

View File

@ -60,7 +60,23 @@
outline: 2px solid #bfe7f3;
}
}
}
// Make exceptions for wp Core DatePicker.
&.is-core-datepicker {
.components-datetime__date {
padding-left: 0;
}
.CalendarDay__default {
background-color: transparent;
}
.CalendarDay__selected {
background: $woocommerce-700;
border: none;
}
}
}
.woocommerce-calendar__inputs {
padding: 1em;
@ -143,32 +159,50 @@
}
.woocommerce-filters-date__content {
.woocommerce-calendar__input-error {
display: none;
.components-popover__content {
background-color: $core-grey-dark-400;
color: $white;
padding: 0.5em;
border: none;
}
&.components-popover {
.components-popover__content {
min-width: 100px;
width: 100px;
text-align: center;
}
&:not(.no-arrow):not(.is-mobile).is-bottom::before {
border-bottom-color: $core-grey-dark-400;
z-index: 1;
top: -6px;
}
}
}
&.is-mobile .woocommerce-calendar__input-error .components-popover__content {
height: initial;
}
}
.woocommerce-calendar__input-error {
display: none;
.components-popover__content {
background-color: $core-grey-dark-400;
color: $white;
padding: 0.5em;
border: none;
}
&.components-popover {
.components-popover__content {
min-width: 100px;
width: 100px;
text-align: center;
}
&:not(.no-arrow):not(.is-mobile).is-bottom::before {
border-bottom-color: $core-grey-dark-400;
z-index: 1;
top: -6px;
}
&:not(.no-arrow):not(.is-mobile).is-top::after {
border-top-color: $core-grey-dark-400;
z-index: 1;
top: 0px;
}
}
}
.woocommerce-calendar__date-picker-title {
@include font-size( 12 );
font-weight: 100;
text-transform: uppercase;
text-align: center;
color: $core-grey-dark-300;
width: 100%;
margin: 0;
padding: 1em;
background-color: $white;
}

View File

@ -0,0 +1,20 @@
/** @format */
/**
* External dependencies
*/
import moment from 'moment';
export function getOutsideRange( invalidDays ) {
if ( 'string' === typeof invalidDays ) {
switch ( invalidDays ) {
case 'past':
return day => moment().isAfter( day, 'day' );
case 'future':
return day => moment().isBefore( day, 'day' );
case 'none':
default:
return undefined;
}
}
return 'function' === typeof invalidDays ? invalidDays : undefined;
}

View File

@ -12,7 +12,7 @@ import classnames from 'classnames';
* Internal dependencies
*/
import ComparePeriods from './compare-periods';
import DateRange from '../../calendar';
import DateRange from '../../calendar/date-range';
import { H, Section } from '../../section';
import PresetPeriods from './preset-periods';

View File

@ -24,7 +24,7 @@ const shortDateFormat = __( 'MM/DD/YYYY', 'wc-admin' );
/**
* Select a range of dates or single dates.
*/
class DatePicker extends Component {
class DateRangeFilterPicker extends Component {
constructor( props ) {
super( props );
this.state = this.getResetState();
@ -156,7 +156,7 @@ class DatePicker extends Component {
}
}
DatePicker.propTypes = {
DateRangeFilterPicker.propTypes = {
/**
* The `path` parameter supplied by React-Router.
*/
@ -167,8 +167,8 @@ DatePicker.propTypes = {
query: PropTypes.object,
};
DatePicker.defaultProps = {
DateRangeFilterPicker.defaultProps = {
query: {},
};
export default DatePicker;
export default DateRangeFilterPicker;

View File

@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
*/
import AdvancedFilters from './advanced';
import CompareFilter from './compare';
import DatePicker from './date';
import DateRangeFilterPicker from './date';
import FilterPicker from './filter';
import { H, Section } from '../section';
@ -64,7 +64,7 @@ class ReportFilters extends Component {
<Section component="div" className="woocommerce-filters">
<div className="woocommerce-filters__basic-filters">
{ showDatePicker && (
<DatePicker key={ JSON.stringify( query ) } query={ query } path={ path } />
<DateRangeFilterPicker key={ JSON.stringify( query ) } query={ query } path={ path } />
) }
{ filters.map( config => {
if ( config.showFilters( query ) ) {

View File

@ -59,6 +59,10 @@
background-color: $white;
}
.woocommerce-calendar__input-error .components-popover__content {
background-color: $core-grey-dark-400;
}
&.is-mobile {
.components-popover__content {
width: 100%;

View File

@ -12,8 +12,9 @@ export { default as ChartPlaceholder } from './chart/placeholder';
export { default as Card } from './card';
export { default as Count } from './count';
export { default as CompareFilter } from './filters/compare';
export { default as DatePicker } from './filters/date';
export { default as DateRange } from './calendar';
export { default as DateRangeFilterPicker } from './filters/date';
export { default as DateRange } from './calendar/date-range';
export { default as DatePicker } from './calendar/date-picker';
export { default as DropdownButton } from './dropdown-button';
export { default as EllipsisMenu } from './ellipsis-menu';
export { default as EmptyContent } from './empty-content';