From 524bb0c96f12972e985d4729ca6aa23ea9688d0a Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 11 Dec 2018 14:50:26 +1300 Subject: [PATCH 1/2] DateRangeFilterPicker --- .../docs/components/calendar.md | 53 +++++++ .../docs/components/filters.md | 4 +- .../docs/components/search.md | 4 +- .../woocommerce-admin/docs/components/tag.md | 2 +- .../components/src/calendar/date-picker.js | 141 ++++++++++++++++++ .../src/calendar/{index.js => date-range.js} | 27 +--- .../components/src/calendar/example.md | 68 ++++++--- .../packages/components/src/calendar/input.js | 23 ++- .../components/src/calendar/style.scss | 86 +++++++---- .../packages/components/src/calendar/utils.js | 20 +++ .../components/src/filters/date/content.js | 2 +- .../components/src/filters/date/index.js | 8 +- .../packages/components/src/filters/index.js | 4 +- .../components/src/filters/style.scss | 4 + .../packages/components/src/index.js | 5 +- 15 files changed, 367 insertions(+), 84 deletions(-) create mode 100644 plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js rename plugins/woocommerce-admin/packages/components/src/calendar/{index.js => date-range.js} (89%) create mode 100644 plugins/woocommerce-admin/packages/components/src/calendar/utils.js diff --git a/plugins/woocommerce-admin/docs/components/calendar.md b/plugins/woocommerce-admin/docs/components/calendar.md index 7140f0f13ca..95fecb2f318 100644 --- a/plugins/woocommerce-admin/docs/components/calendar.md +++ b/plugins/woocommerce-admin/docs/components/calendar.md @@ -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) ======================= diff --git a/plugins/woocommerce-admin/docs/components/filters.md b/plugins/woocommerce-admin/docs/components/filters.md index eb92acc7e01..6312ecbbfb3 100644 --- a/plugins/woocommerce-admin/docs/components/filters.md +++ b/plugins/woocommerce-admin/docs/components/filters.md @@ -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. diff --git a/plugins/woocommerce-admin/docs/components/search.md b/plugins/woocommerce-admin/docs/components/search.md index 75e3385aafc..0408f6b2c8b 100644 --- a/plugins/woocommerce-admin/docs/components/search.md +++ b/plugins/woocommerce-admin/docs/components/search.md @@ -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: `[]` diff --git a/plugins/woocommerce-admin/docs/components/tag.md b/plugins/woocommerce-admin/docs/components/tag.md index f11fd5a67ab..c60cd369f5d 100644 --- a/plugins/woocommerce-admin/docs/components/tag.md +++ b/plugins/woocommerce-admin/docs/components/tag.md @@ -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. diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js b/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js new file mode 100644 index 00000000000..4781d3e49f5 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/calendar/date-picker.js @@ -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 ( + ( + + ) } + renderContent={ ( { onToggle } ) => ( +
+ + { __( 'select a date', 'wc-admin' ) } + +
+ +
+
+ ) } + /> + ); + } +} + +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; diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/index.js b/plugins/woocommerce-admin/packages/components/src/calendar/date-range.js similarity index 89% rename from plugins/woocommerce-admin/packages/components/src/calendar/index.js rename to plugins/woocommerce-admin/packages/components/src/calendar/date-range.js index 1711ffff7a8..9d8deb3b7e4 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/index.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/date-range.js @@ -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 (
{ - 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 ( - +
+ Date Range Picker +
+ +
+ + Date Picker +
+ +
+
) } ); ``` diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/input.js b/plugins/woocommerce-admin/packages/components/src/calendar/input.js index 0c88e299077..330b08d5987 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/input.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/input.js @@ -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 && ( { error } @@ -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; diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/style.scss b/plugins/woocommerce-admin/packages/components/src/calendar/style.scss index 8f85f829cfa..246d94ecef8 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/calendar/style.scss @@ -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; +} diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/utils.js b/plugins/woocommerce-admin/packages/components/src/calendar/utils.js new file mode 100644 index 00000000000..206d110e573 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/calendar/utils.js @@ -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; +} diff --git a/plugins/woocommerce-admin/packages/components/src/filters/date/content.js b/plugins/woocommerce-admin/packages/components/src/filters/date/content.js index a0534e517ba..af4f1164a04 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/date/content.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/date/content.js @@ -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'; diff --git a/plugins/woocommerce-admin/packages/components/src/filters/date/index.js b/plugins/woocommerce-admin/packages/components/src/filters/date/index.js index 4d3806430ba..4afa779c932 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/date/index.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/date/index.js @@ -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; diff --git a/plugins/woocommerce-admin/packages/components/src/filters/index.js b/plugins/woocommerce-admin/packages/components/src/filters/index.js index 8e69ae966e4..791e45eaa78 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/index.js +++ b/plugins/woocommerce-admin/packages/components/src/filters/index.js @@ -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 {
{ showDatePicker && ( - + ) } { filters.map( config => { if ( config.showFilters( query ) ) { diff --git a/plugins/woocommerce-admin/packages/components/src/filters/style.scss b/plugins/woocommerce-admin/packages/components/src/filters/style.scss index 41f8c0c6113..ff6131517a0 100644 --- a/plugins/woocommerce-admin/packages/components/src/filters/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/filters/style.scss @@ -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%; diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js index af501d1acdf..96806ae7117 100644 --- a/plugins/woocommerce-admin/packages/components/src/index.js +++ b/plugins/woocommerce-admin/packages/components/src/index.js @@ -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'; From c76ceb25915fff785913214d7e77f9f8eb9ac378 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Fri, 21 Dec 2018 15:19:10 +1300 Subject: [PATCH 2/2] DatePicker: cleanup example, propTypes --- .../packages/components/src/calendar/example.md | 2 -- .../packages/components/src/calendar/input.js | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/example.md b/plugins/woocommerce-admin/packages/components/src/calendar/example.md index a95d04e0a1a..17f9e8858c6 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/example.md +++ b/plugins/woocommerce-admin/packages/components/src/calendar/example.md @@ -50,8 +50,6 @@ const MyDateRange = withState( { onUpdate={ onDatePickerUpdate } dateFormat={ dateFormat } invalidDays="none" - onUpdate={ onDatePickerUpdate } - invalidDays="future" />
diff --git a/plugins/woocommerce-admin/packages/components/src/calendar/input.js b/plugins/woocommerce-admin/packages/components/src/calendar/input.js index 330b08d5987..02844dd4c33 100644 --- a/plugins/woocommerce-admin/packages/components/src/calendar/input.js +++ b/plugins/woocommerce-admin/packages/components/src/calendar/input.js @@ -4,7 +4,7 @@ */ import { Dashicon, Popover } from '@wordpress/components'; import classnames from 'classnames'; -import { uniqueId } from 'lodash'; +import { uniqueId, noop } from 'lodash'; import PropTypes from 'prop-types'; const DateInput = ( { @@ -63,11 +63,13 @@ DateInput.propTypes = { error: PropTypes.string, errorPosition: PropTypes.string, onFocus: PropTypes.func, + onKeyDown: PropTypes.func, }; DateInput.defaultProps = { onFocus: () => {}, errorPosition: 'bottom center', + onKeyDown: noop, }; export default DateInput;