From 24ff48fbfb436b6a7f457edec4cb04afdf578c4d Mon Sep 17 00:00:00 2001 From: Kirill Volkovich Date: Thu, 23 Aug 2018 16:07:44 +0300 Subject: [PATCH] Add possibility to define disabled days (PR #62, ISSUE #55) API changes === Added new property `modifiers`. Implemented default `disabled` modifier. Disabled days validation and display for `DateInput` and `DateRangeInput` components can be implemented by validation at parent component level and passing validation result to `isValid` property. For validation use `import { ModifiersUtils } from @opuscapita/react-dates`. See component documentation for details. --- package.json | 2 +- .../DateInput/DateInput.DOCUMENTATION.md | 4 + .../components/DateInput/DateInput.react.js | 20 +++- .../DatePicker/DatePicker.DOCUMENTATION.md | 24 ++-- .../components/DatePicker/DatePicker.react.js | 12 +- .../DateRangeInput.DOCUMENTATION.md | 43 ++++--- .../DateRangeInput/DateRangeInput.react.js | 11 +- .../components/DayPicker/Caption.react.js | 85 +++++++++++++ .../DayPicker/DayPicker.DOCUMENTATION.md | 14 ++- .../components/DayPicker/DayPicker.less | 9 +- .../components/DayPicker/DayPicker.react.js | 113 +++--------------- .../components/DayPicker/DayPicker.spec.js | 6 +- src/client/index.js | 3 +- 13 files changed, 206 insertions(+), 140 deletions(-) create mode 100644 src/client/components/DayPicker/Caption.react.js diff --git a/package.json b/package.json index e5f78195..58f7c6db 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint-fix": "eslint src --fix", "start": "cross-env NODE_ENV=development && showroom-scan src && babel-node www/index", "test": "mocha src/**/*.spec.js --compilers js:babel-register --require config/test/mocha-setup.js --require ignore-styles", - "npm-build": "rimraf lib && cross-env NODE_ENV=production WEBPACK_BUNDLE_ANALYZE=true webpack --config ./config/webpack.config.js", + "npm-build": "rimraf lib && cross-env NODE_ENV=production webpack --config ./config/webpack.config.js", "npm-publish": "npm run npm-build && npm publish", "publish-release": "npm run npm-publish" }, diff --git a/src/client/components/DateInput/DateInput.DOCUMENTATION.md b/src/client/components/DateInput/DateInput.DOCUMENTATION.md index 87301d6b..0a34a1ce 100644 --- a/src/client/components/DateInput/DateInput.DOCUMENTATION.md +++ b/src/client/components/DateInput/DateInput.DOCUMENTATION.md @@ -15,6 +15,7 @@ Based on configured to OpusCapita defaults [react-day-picker](https://github.com | disabled | bool | If true - became inactive | | isValid | bool | If false - highlight input as error | | locale | string | `en`, `de`, etc. Days and months translations, first day of week, etc. depends on this property | +| modifiers | object | [Info](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/modifiers.md). Use `disabled` modifier to select disabled days. Check, is it using [`import { ModifiersUtils } from @opuscapita/react-dates`](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/utils-modifiers.md) | | onBlur | func | Callback fired on input blur `(e) => {}` | | onChange | func | Callback fired on date change `Date date => {}` | | onFocus | func | Callback fired on input focus `(e) => {}` | @@ -55,6 +56,9 @@ Based on configured to OpusCapita defaults [react-day-picker](https://github.com onFocus={(e) => console.log('Focus!', e)} showToLeft={true} showToTop={true} + modifiers={{ + disabled: { after: new Date() } + }} /> ``` diff --git a/src/client/components/DateInput/DateInput.react.js b/src/client/components/DateInput/DateInput.react.js index 28dfe36a..a458705c 100644 --- a/src/client/components/DateInput/DateInput.react.js +++ b/src/client/components/DateInput/DateInput.react.js @@ -21,6 +21,7 @@ let propTypes = { disabled: PropTypes.bool, isValid: PropTypes.bool, locale: PropTypes.string, + modifiers: PropTypes.object, onBlur: PropTypes.func, onChange: PropTypes.func, onFocus: PropTypes.func, @@ -41,6 +42,7 @@ let defaultProps = { disabled: false, isValid: true, locale: 'en', + modifiers: {}, onBlur: () => {}, onFocus: () => {}, onChange: () => {}, @@ -113,11 +115,6 @@ class DateInput extends Component { document.body.removeEventListener('keydown', this.handleBodyKeyDown); } - handleDayClick = day => { - this.setState({ showPicker: false }); - this.handleDateChange(day); - }; - handleBodyClick = event => { let clickedOutside = ( !this.container.contains(event.target) && @@ -171,9 +168,18 @@ class DateInput extends Component { handleDateChange = value => { this.props.onChange(zeroTime(value)); + this.hidePicker(); this.setState({ error: null }); }; + handleDayClick = (value, modifiers) => { + if (modifiers.disabled) { + return; + } + + this.handleDateChange(value); + } + showPicker() { let month = this.props.value || new Date(); this.reactDayPicker.showMonth(month); @@ -243,8 +249,10 @@ class DateInput extends Component { selectedDays={value} tabIndex={-1} fixedWeeks={true} - onChange={this.handleDateChange} onDayClick={this.handleDayClick} + onDayKeyDown={this.handleDateChange} + onDayTouchEnd={this.handleDateChange} + onChange={this.handleDateChange} { ...dayPickerSpecificProps } /> ); diff --git a/src/client/components/DatePicker/DatePicker.DOCUMENTATION.md b/src/client/components/DatePicker/DatePicker.DOCUMENTATION.md index 976ddcd8..a1d3bc13 100644 --- a/src/client/components/DatePicker/DatePicker.DOCUMENTATION.md +++ b/src/client/components/DatePicker/DatePicker.DOCUMENTATION.md @@ -8,16 +8,17 @@ Based on configured to OpusCapita defaults [react-day-picker](https://github.com ### Props Reference -| Name | Type | Description | -| ------------------------------ | :---------------------- | ----------------------------------------------------------- | -| className | string | Default HTML behavior | -| disabled | bool | If true - became inactive | -| locale | string | `en`, `de`, etc. Days and months translations, first day of week, etc. depends on this property | -| onChange | func | Callback fired on date change `[Date from, Date to] => {}` | -| showToLeft | bool | Show picker popup to left relative to button | -| showToTop | bool | Show picker popup to top relative to button | -| tabIndex | number | Default HTML behavior | -| value | instanceOf(Date) | Instance of `Date` | +| Name | Type | Description | +| ------------------------------ | :---------------------- | ----------------------------------------------------------- | +| className | string | Default HTML behavior | +| disabled | bool | If true - became inactive | +| locale | string | `en`, `de`, etc. Days and months translations, first day of week, etc. depends on this property | +| modifiers | object | [Info](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/modifiers.md). Use `disabled` modifier to select disabled days. Check, is it using [`import { ModifiersUtils } from @opuscapita/react-dates`](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/utils-modifiers.md) | +| onChange | func | Callback fired on date change `[Date from, Date to] => {}` | +| showToLeft | bool | Show picker popup to left relative to button | +| showToTop | bool | Show picker popup to top relative to button | +| tabIndex | number | Default HTML behavior | +| value | instanceOf(Date) | Instance of `Date` | *** @@ -35,6 +36,9 @@ See react-day-picker [methods reference](http://react-day-picker.js.org/APIMetho locale="de" onChange={_scope.handleChange.bind(_scope)} value={_scope.state.value} + modifiers={{ + disabled: { after: new Date() } + }} /> diff --git a/src/client/components/DatePicker/DatePicker.react.js b/src/client/components/DatePicker/DatePicker.react.js index e008dcb9..e2dbd966 100644 --- a/src/client/components/DatePicker/DatePicker.react.js +++ b/src/client/components/DatePicker/DatePicker.react.js @@ -14,6 +14,7 @@ let propTypes = { className: PropTypes.string, disabled: PropTypes.bool, locale: PropTypes.string, + modifiers: PropTypes.object, onChange: PropTypes.func, showToLeft: PropTypes.bool, showToTop: PropTypes.bool, @@ -25,6 +26,7 @@ let defaultProps = { className: '', disabled: false, locale: 'en', + modifiers: {}, onChange: () => {}, showToLeft: false, showToTop: false, @@ -68,9 +70,13 @@ class DatePicker extends Component { this.hidePicker(); }; - handleDayClick = day => { - this.handleDateChange(day); - this.setState({ showPicker: false }); + handleDayClick = (value, modifiers) => { + if (modifiers.disabled) { + return; + } + + this.handleDateChange(value); + this.hidePicker(); }; handleToggleClick = () => { diff --git a/src/client/components/DateRangeInput/DateRangeInput.DOCUMENTATION.md b/src/client/components/DateRangeInput/DateRangeInput.DOCUMENTATION.md index bf98a83a..ffb69bf0 100644 --- a/src/client/components/DateRangeInput/DateRangeInput.DOCUMENTATION.md +++ b/src/client/components/DateRangeInput/DateRangeInput.DOCUMENTATION.md @@ -6,21 +6,22 @@ Allows select date range using mouse. ### Props Reference -| Name | Type | Description | -| ------------------------------ | :---------------------- | ----------------------------------------------------------- | -| className | string | Default behavior | -| dateFormat | string | `dd/MM/yyyy`, `MM.dd.yyyy`, etc. | -| disabled | bool | If true - became inactive | -| isValid | bool | If false - highlight input as error | -| locale | string | `en`, `de`, etc. Days and months translations, first day of week, etc. depends on this property | -| onFocus | func | Callback fired on input focus `(e, inputName) => {}` where `inputName === 'from | -| onBlur | func | Callback fired on input blur `(e, inputName) => {}` where `inputName === 'from | -| onChange | func | Callback fired on date change `[Date from, Date to] => {}` | -| showToLeft | bool | Show picker popup to left relative to input | -| showToTop | bool | Show picker popup to top relative to input | -| tabIndex | number | Default behavior | -| value | array | `[Date from, Date to]` | -| variants | array | `[ { getLabel: (locale) => string, getRange: (locale) => [ from, to] } ]` List of pre-defined date-ranges. Examples: current month, last week, next week, etc. | +| Name | Type | Description | +| ------------------------------ | :---------------------- | ----------------------------------------------------------- | +| className | string | Default behavior | +| dateFormat | string | `dd/MM/yyyy`, `MM.dd.yyyy`, etc. | +| disabled | bool | If true - became inactive | +| isValid | bool | If false - highlight input as error | +| locale | string | `en`, `de`, etc. Days and months translations, first day of week, etc. depends on this property | +| modifiers | object | [Info](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/modifiers.md). Use `disabled` modifier to select disabled days. Check, is it using [`import { ModifiersUtils } from @opuscapita/react-dates`](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/utils-modifiers.md) | +| onFocus | func | Callback fired on input focus `(e, inputName) => {}` where `inputName === 'from | +| onBlur | func | Callback fired on input blur `(e, inputName) => {}` where `inputName === 'from | +| onChange | func | Callback fired on date change `[Date from, Date to] => {}` | +| showToLeft | bool | Show picker popup to left relative to input | +| showToTop | bool | Show picker popup to top relative to input | +| tabIndex | number | Default behavior | +| value | array | `[Date from, Date to]` | +| variants | array | `[ { getLabel: (locale) => string, getRange: (locale) => [ from, to] } ]` List of pre-defined date-ranges. Examples: current month, last week, next week, etc. | ### Code Example @@ -41,6 +42,18 @@ Allows select date range using mouse. onChange={_scope.handleChange3.bind(_scope)} value={_scope.state.value2} /> + +
+ + ``` ### Component Name diff --git a/src/client/components/DateRangeInput/DateRangeInput.react.js b/src/client/components/DateRangeInput/DateRangeInput.react.js index 8ca525ae..729e1b6c 100644 --- a/src/client/components/DateRangeInput/DateRangeInput.react.js +++ b/src/client/components/DateRangeInput/DateRangeInput.react.js @@ -36,6 +36,7 @@ let propTypes = { disabled: PropTypes.bool, isValid: PropTypes.bool, locale: PropTypes.string, + modifiers: PropTypes.object, onBlur: PropTypes.func, onChange: PropTypes.func, onFocus: PropTypes.func, @@ -55,6 +56,7 @@ let defaultProps = { disabled: false, isValid: true, locale: 'en', + modifiers: {}, onBlur: () => {}, onChange: () => {}, onFocus: () => {}, @@ -198,7 +200,11 @@ class DateRangeInput extends Component { this.props.onChange(normalizedRange); }; - handleDayClick = dayValue => { + handleDayClick = (dayValue, modifiers) => { + if (modifiers.disabled) { + return; + } + const day = zeroTime(dayValue); let from = this.props.value[0]; @@ -352,12 +358,11 @@ class DateRangeInput extends Component { let pickerElement = ( { + if (isRange) { + onChange({ month, year, captionIndex }); + } else { + onChange({ month, year, captionIndex: 0 }); + } + }; + + let handleYearChange = (event) => { + handleChange(event.target.value, date.getMonth()); + }; + + let handleMonthChange = (event) => { + handleChange(date.getFullYear(), event.target.value); + }; + + if (!monthToDisplay) { + return null; + } + + return ( +
+
+ + +
+
+ ); +} diff --git a/src/client/components/DayPicker/DayPicker.DOCUMENTATION.md b/src/client/components/DayPicker/DayPicker.DOCUMENTATION.md index 10f3a59c..98612f8d 100644 --- a/src/client/components/DayPicker/DayPicker.DOCUMENTATION.md +++ b/src/client/components/DayPicker/DayPicker.DOCUMENTATION.md @@ -15,6 +15,7 @@ DayPicker is a styled to OpusCapita defaults [react-day-picker](https://github.c | hideTodayButton | bool | | | onChange | func | Callback fired when new date selected `Date date => {}` | | locale | string | `de`, `en`, etc. | +| modifiers | object | [Info](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/modifiers.md). Use `disabled` modifier to select disabled days. Check, is it using [`import { ModifiersUtils } from @opuscapita/react-dates`](https://github.com/gpbl/react-day-picker/blob/v6.1.0/docs/docs/utils-modifiers.md) | | pickerClassName | string | Class name passed to react day picker | | getTodayButtonLabel | string | | @@ -32,7 +33,18 @@ See react-day-picker [methods reference](http://react-day-picker.js.org/APIMetho (window.picker = ref)} onChange={ day => console.log('day:', day) } - locale="de" + locale="en" +/> + + (window.picker = ref)} + onChange={ day => console.log('day:', day) } + numberOfMonths={2} + isRange={true} + locale="en" + modifiers={{ + disabled: { after: new Date() } + }} /> ``` diff --git a/src/client/components/DayPicker/DayPicker.less b/src/client/components/DayPicker/DayPicker.less index 13a270ec..6e7943a1 100644 --- a/src/client/components/DayPicker/DayPicker.less +++ b/src/client/components/DayPicker/DayPicker.less @@ -31,15 +31,20 @@ position: relative; transition: transform 0.1s ease; - &:active:focus { + &:active:focus:not(.DayPicker-Day--disabled) { z-index: 10; transform: scale(1.4); } - &:focus { + &:focus:not(.DayPicker-Day--disabled) { transform: scale(1); } } + +.DayPicker-Day--disabled.DayPicker-Day--outside { + pointer-events: none; +} + .opuscapita_day-picker__picker .DayPicker-wrapper { flex-wrap: nowrap; &:focus { diff --git a/src/client/components/DayPicker/DayPicker.react.js b/src/client/components/DayPicker/DayPicker.react.js index 7308031c..fdf1e87f 100644 --- a/src/client/components/DayPicker/DayPicker.react.js +++ b/src/client/components/DayPicker/DayPicker.react.js @@ -5,82 +5,16 @@ import "react-day-picker/lib/style.css"; import localeUtils from '../../dayjs/reactDayPickerUtils'; import getMessage from '../translations'; import { splitProps, zeroTime } from '../utils'; +import Caption from './Caption.react'; +import dayjs from 'dayjs'; import './DayPicker.less'; -function Caption(props) { - let { - date, // eslint-disable-line react/prop-types - locale, // eslint-disable-line react/prop-types - localeUtils, // eslint-disable-line react/prop-types - isRange, // eslint-disable-line react/prop-types - onChange, // eslint-disable-line react/prop-types - currentMonth // eslint-disable-line react/prop-types - } = props; - - let months = localeUtils.getMonths(locale); - let dateNow = new Date(); - let years = []; - for (let i = dateNow.getFullYear() - 100; i <= dateNow.getFullYear() + 100; i += 1) { - years.push(i); - } - - let handleChange = (year, month) => { - let newDate = new Date(year, month); - if (isRange) { - let isCaptionFrom = date.getMonth() === currentMonth.getMonth(); - let captionIndex = isCaptionFrom ? 0 : 1; - onChange(newDate, captionIndex); - } else { - onChange(newDate); - } - }; - - let handleYearChange = (event) => { - handleChange(event.target.value, date.getMonth()); - }; - - let handleMonthChange = (event) => { - handleChange(date.getFullYear(), event.target.value); - }; - - return ( -
-
- - -
-
- ); -} - let propTypes = { ...ReactDayPicker.propTypes, className: PropTypes.string, dayPickerRef: PropTypes.func, - hideTodayButton: PropTypes.bool, getTodayButtonLabel: PropTypes.func, + hideTodayButton: PropTypes.bool, isRange: PropTypes.bool, onChange: PropTypes.func, pickerClassName: PropTypes.string @@ -89,10 +23,10 @@ let propTypes = { let defaultProps = { className: '', dayPickerRef: () => {}, + getTodayButtonLabel: (locale) => getMessage(locale, 'today'), hideTodayButton: false, isRange: false, labels: ReactDayPicker.defaultProps.labels, - getTodayButtonLabel: (locale) => getMessage(locale, 'today'), onChange: () => {}, pickerClassName: '' }; @@ -103,29 +37,20 @@ class DayPicker extends Component { super(props); this.state = { date: props.date, - currentMonth: null + monthToDisplay: props.date }; } - handleDateChange = (dateValue, captionIndex) => { - const date = zeroTime(dateValue); - if (this.props.isRange) { - let range = this.props.selectedDays[1]; - let fromChanged = captionIndex === 0; - let toChanged = captionIndex === 1; - if (fromChanged) { - this.props.onChange([date, range.to]); - } - if (toChanged) { - this.props.onChange([range.from, date]); - } - } else { - this.props.onChange(date); - } - }; + handleCaptionChange = ({ month, year, captionIndex }) => { + let monthToDisplay = captionIndex === 0 ? + new Date(year, month) : + dayjs(new Date(year, month)).subtract(1, 'month').toDate(); + + this.setState({ monthToDisplay }); + } - handleMonthChange = month => { - this.setState({ currentMonth: month }); + handleMonthChange = date => { + this.setState({ monthToDisplay: date }); }; handleTodayClick = () => { @@ -145,7 +70,7 @@ class DayPicker extends Component { ...restProps } = this.props; - let { currentMonth } = this.state; + let { monthToDisplay } = this.state; let splittedProps = splitProps(restProps, Object.keys(ReactDayPicker.propTypes)); let commonProps = splittedProps[0]; @@ -165,9 +90,9 @@ class DayPicker extends Component { let caption = ( this.handleCaptionChange(captionData)} isRange={isRange} - currentMonth={currentMonth} + monthToDisplay={monthToDisplay} /> ); @@ -181,15 +106,13 @@ class DayPicker extends Component { localeUtils={localeUtils} locale={locale} firstDayOfWeek={locale === 'en' ? 0 : 1} - onDayClick={this.handleDateChange} - onDayKeyDown={this.handleDateChange} - onDayTouchEnd={this.handleDateChange} onMonthChange={this.handleMonthChange} onWeekClick={() => {}} captionElement={caption} tabIndex={-1} showWeekNumbers={true} { ...pickerSpecificProps } + month={monthToDisplay} />
diff --git a/src/client/components/DayPicker/DayPicker.spec.js b/src/client/components/DayPicker/DayPicker.spec.js index 9b267e4e..ac657f55 100644 --- a/src/client/components/DayPicker/DayPicker.spec.js +++ b/src/client/components/DayPicker/DayPicker.spec.js @@ -78,12 +78,12 @@ describe('', () => { expect(spy.args[0][0]).to.be.instanceOf(ReactDayPicker); }); - it('should call onChange when new date selected', () => { + it('should call onChange when clicked on "Today" button', () => { let spy = sinon.spy(); let wrapper = mount(); - let dayElement = wrapper.find('.DayPicker-Day[aria-selected]').first(); - dayElement.simulate('click'); + let todayButton = wrapper.find('.opuscapita_day-picker__today-button').first(); + todayButton.simulate('click'); expect(spy).to.have.been.called; expect(spy.args[0][0]).to.be.instanceOf(Date); diff --git a/src/client/index.js b/src/client/index.js index 30ed543c..c39fd59f 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -3,5 +3,6 @@ module.exports = { DateInput: require('./components/DateInput').default, DateRangeInput: require('./components/DateRangeInput').default, - DatePicker: require('./components/DatePicker').default + DatePicker: require('./components/DatePicker').default, + ModifiersUtils: require('react-day-picker').ModifiersUtils };