diff --git a/README.md b/README.md index 7b663e34..2a79d65c 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,12 @@ import React from 'react'; import SemanticDatepicker from 'react-semantic-ui-datepickers'; import 'react-semantic-ui-datepickers/dist/react-semantic-ui-datepickers.css'; -const AppWithBasic = ({ onDateChange }) => ( - +const AppWithBasic = ({ onChange }) => ( + ); -const AppWithRangeAndInPortuguese = ({ onDateChange }) => ( - +const AppWithRangeAndInPortuguese = ({ onChange }) => ( + ); ``` @@ -75,7 +75,7 @@ More examples [here](https://react-semantic-ui-datepickers.now.sh). | keepOpenOnSelect | boolean | no | false | Keeps the datepicker open when a date is selected | | locale | string | no | 'en-US' | Filename of the locale to be used. PS: Feel free to submit PR's with more locales! | | onBlur | function | no | () => {} | Callback fired when the input loses focus | -| onDateChange | function | yes | | Callback fired when the value changes | +| onChange | function | no | () => {} | Callback fired when the value changes | | pointing | string | no | 'left' | Location to render the component around input. Available options: 'left', 'right', 'top left', 'top right' | | type | string | no | basic | Type of input to render. Available options: 'basic' and 'range' | diff --git a/src/__tests__/datepicker.test.tsx b/src/__tests__/datepicker.test.tsx index 7c72e6a7..4f758b90 100644 --- a/src/__tests__/datepicker.test.tsx +++ b/src/__tests__/datepicker.test.tsx @@ -3,10 +3,11 @@ import { fireEvent, render } from '@testing-library/react'; import { SemanticDatepickerProps } from '../types'; import localeEn from '../locales/en-US.json'; import localePt from '../locales/pt-BR.json'; +import { getShortDate } from '../utils'; import DatePicker from '../'; const setup = (props?: Partial) => { - const options = render(); + const options = render(); return { ...options, @@ -17,7 +18,7 @@ const setup = (props?: Partial) => { }, rerender: (newProps?: Partial) => options.rerender( - + ), }; }; @@ -55,6 +56,26 @@ describe('Basic datepicker', () => { expect(todayButton.textContent).toBe(localeEn.todayButton); }); + + it('fires onChange with event and selected date as arguments', async () => { + const onChange = jest.fn(); + const today = getShortDate(new Date()) as string; + const { getByTestId, openDatePicker } = setup({ onChange }); + + openDatePicker(); + + const todayCell = getByTestId(RegExp(today)); + + fireEvent.click(todayCell); + + expect(onChange).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + expect.objectContaining({ + value: expect.any(Date), + }) + ); + }); }); describe('Range datepicker', () => { @@ -86,8 +107,46 @@ describe('Range datepicker', () => { expect(todayButton.textContent).toBe(localeEn.todayButton); // @ts-ignore - rerender({ locale: 'invalid language' }); + rerender({ locale: 'invalid' }); expect(todayButton.textContent).toBe(localeEn.todayButton); }); + + it('fires onChange with event and selected dates as arguments', async () => { + const onChange = jest.fn(); + const now = new Date(); + const today = getShortDate(now) as string; + const tomorrow = getShortDate( + new Date(now.setDate(now.getDate() + 1)) + ) as string; + const { getByTestId, openDatePicker } = setup({ + onChange, + type: 'range', + }); + + openDatePicker(); + + const todayCell = getByTestId(RegExp(today)); + const tomorrowCell = getByTestId(RegExp(tomorrow)); + + fireEvent.click(todayCell); + + expect(onChange).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + expect.objectContaining({ + value: [expect.any(Date)], + }) + ); + + fireEvent.click(tomorrowCell); + + expect(onChange).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + expect.objectContaining({ + value: [expect.any(Date), expect.any(Date)], + }) + ); + }); }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 2e8c9f41..42d6be4a 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -4,6 +4,7 @@ import localeEn from '../locales/en-US.json'; import { formatDate, formatSelectedDate, + getShortDate, getToday, isSelectable, moveElementsByN, @@ -14,7 +15,8 @@ import { } from '../utils'; const objectTest = { a: 'a', b: 'b', c: 'c' }; -const dateTest = parse('2018-06-21'); +const dateTestString = '2018-06-21'; +const dateTest = parse(dateTestString); const june14 = parse('2018-06-14'); const june20 = parse('2018-06-20'); const june25 = parse('2018-06-25'); @@ -159,3 +161,13 @@ describe('onlyNumbers', () => { expect(onlyNumbers('ABC-1025.4.8')).toBe('102548'); }); }); + +describe('getShortDate', () => { + it('should return undefined if date is not provided', () => { + expect(getShortDate(undefined)).toBe(undefined); + }); + + it('should return the date provided in the right format', () => { + expect(getShortDate(new Date(dateTestString))).toBe(dateTestString); + }); +}); diff --git a/src/components/calendar/calendar.tsx b/src/components/calendar/calendar.tsx index 404ed08f..51a12921 100644 --- a/src/components/calendar/calendar.tsx +++ b/src/components/calendar/calendar.tsx @@ -2,7 +2,7 @@ import cn from 'classnames'; import React, { Fragment } from 'react'; import { Segment } from 'semantic-ui-react'; import { DateFns, Locale, SemanticDatepickerProps } from 'types'; -import { getToday } from '../../utils'; +import { getShortDate, getToday } from '../../utils'; import Button from '../button'; import CalendarCell from '../cell'; import TodayButton from '../today-button'; @@ -124,12 +124,14 @@ const Calendar: React.FC = ({ const selectable = dateObj.selectable && filterDate(dateObj.date); + const shortDate = getShortDate(dateObj.date); return ( {dateObj.date.getDate()} diff --git a/src/index.tsx b/src/index.tsx index 60d02718..2a421e9d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,8 +10,7 @@ import { parseOnBlur, pick, } from './utils'; -import BasicDatePicker from './pickers/basic'; -import RangeDatePicker from './pickers/range'; +import { BasicDatePicker, RangeDatePicker } from './pickers'; import { Locale, SemanticDatepickerProps } from './types'; import Calendar from './components/calendar'; import Input from './components/input'; @@ -80,22 +79,23 @@ class SemanticDatepicker extends React.Component< locale: 'en-US', name: undefined, onBlur: () => {}, + onChange: () => {}, placeholder: null, pointing: 'left', + readOnly: false, required: false, - selected: null, showOutsideDays: false, type: 'basic', - readOnly: false, + value: null, }; el = React.createRef(); componentDidUpdate(prevProps: SemanticDatepickerProps) { - const { locale, selected } = this.props; + const { locale, value } = this.props; - if (!isEqual(selected, prevProps.selected)) { - this.onDateSelected(selected); + if (!isEqual(value, prevProps.value)) { + this.onDateSelected(undefined, value); } if (locale !== prevProps.locale) { @@ -108,14 +108,14 @@ class SemanticDatepicker extends React.Component< } get initialState() { - const { format, selected } = this.props; + const { format, value } = this.props; const initialSelectedDate = this.isRangeInput ? [] : null; return { isVisible: false, locale: this.locale, - selectedDate: selected || initialSelectedDate, - selectedDateFormatted: formatSelectedDate(selected, format), + selectedDate: value || initialSelectedDate, + selectedDateFormatted: formatSelectedDate(value, format), typedValue: null, }; } @@ -138,6 +138,10 @@ class SemanticDatepicker extends React.Component< const { selectedDate } = this.state; const { date } = this.props; + if (!selectedDate) { + return date; + } + return this.isRangeInput ? selectedDate[0] : selectedDate || date; } @@ -169,8 +173,8 @@ class SemanticDatepicker extends React.Component< ? RangeDatePicker : BasicDatePicker; - resetState = () => { - const { keepOpenOnClear, onDateChange } = this.props; + resetState = event => { + const { keepOpenOnClear, onChange } = this.props; const newState = { isVisible: keepOpenOnClear, selectedDate: this.isRangeInput ? [] : null, @@ -178,7 +182,7 @@ class SemanticDatepicker extends React.Component< }; this.setState(newState, () => { - onDateChange(null); + onChange(event, { ...this.props, value: null }); }); }; @@ -219,11 +223,11 @@ class SemanticDatepicker extends React.Component< }); }; - handleRangeInput = newDates => { - const { format, keepOpenOnSelect, onDateChange } = this.props; + handleRangeInput = (newDates, event) => { + const { format, keepOpenOnSelect, onChange } = this.props; if (!newDates || !newDates.length) { - this.resetState(); + this.resetState(event); return; } @@ -235,7 +239,7 @@ class SemanticDatepicker extends React.Component< }; this.setState(newState, () => { - onDateChange(newDates); + onChange(event, { ...this.props, value: newDates }); if (newDates.length === 2) { this.setState({ isVisible: keepOpenOnSelect }); @@ -243,11 +247,11 @@ class SemanticDatepicker extends React.Component< }); }; - handleBasicInput = newDate => { + handleBasicInput = (newDate, event) => { const { format, keepOpenOnSelect, - onDateChange, + onChange, clearOnSameDateClick, } = this.props; @@ -256,7 +260,7 @@ class SemanticDatepicker extends React.Component< // then reset the state. This is what was previously the default // behavior, without a specific prop. if (clearOnSameDateClick) { - this.resetState(); + this.resetState(event); } else { // Don't reset the state. Instead, close or keep open the // datepicker according to the value of keepOpenOnSelect. @@ -277,7 +281,7 @@ class SemanticDatepicker extends React.Component< }; this.setState(newState, () => { - onDateChange(newDate); + onChange(event, { ...this.props, value: newDate }); }); }; @@ -301,14 +305,14 @@ class SemanticDatepicker extends React.Component< const areDatesValid = parsedValue.every(isValid); if (areDatesValid) { - this.handleRangeInput(parsedValue); + this.handleRangeInput(parsedValue, event); return; } } else { const isDateValid = isValid(parsedValue); if (isDateValid) { - this.handleBasicInput(parsedValue); + this.handleBasicInput(parsedValue, event); return; } } @@ -316,8 +320,8 @@ class SemanticDatepicker extends React.Component< this.setState({ typedValue: null }); }; - handleChange = (_evt, { value }) => { - const { allowOnlyNumbers, format, onDateChange } = this.props; + handleChange = (event: React.SyntheticEvent, { value }) => { + const { allowOnlyNumbers, format, onChange } = this.props; const formatString = this.isRangeInput ? `${format} - ${format}` : format; const typedValue = allowOnlyNumbers ? onlyNumbers(value) : value; @@ -329,7 +333,7 @@ class SemanticDatepicker extends React.Component< }; this.setState(newState, () => { - onDateChange(null); + onChange(event, { ...this.props, value: null }); }); return; @@ -349,11 +353,11 @@ class SemanticDatepicker extends React.Component< } }; - onDateSelected = dateOrDates => { + onDateSelected = (event: React.SyntheticEvent | undefined, dateOrDates) => { if (this.isRangeInput) { - this.handleRangeInput(dateOrDates); + this.handleRangeInput(dateOrDates, event); } else { - this.handleBasicInput(dateOrDates); + this.handleBasicInput(dateOrDates, event); } }; diff --git a/src/pickers/basic.tsx b/src/pickers/basic.tsx index b7a3f7df..f5a793ff 100644 --- a/src/pickers/basic.tsx +++ b/src/pickers/basic.tsx @@ -3,12 +3,11 @@ import { BasicDatePickerProps } from '../types'; import BaseDatePicker from './base'; class DatePicker extends React.Component { - /* eslint-disable-next-line */ - _handleOnDateSelected = ({ selected, selectable, date }) => { - const { selected: selectedDate, onChange, onDateSelected } = this.props; - if (onDateSelected) { - onDateSelected({ selected, selectable, date }); - } + _handleOnDateSelected = ( + { selectable, date }, + event: React.SyntheticEvent + ) => { + const { selected: selectedDate, onChange } = this.props; if (!selectable) { return; @@ -20,7 +19,7 @@ class DatePicker extends React.Component { } if (onChange) { - onChange(newDate); + onChange(event, newDate); } }; diff --git a/src/pickers/index.tsx b/src/pickers/index.tsx index 1cdfc4d9..9373b0f5 100644 --- a/src/pickers/index.tsx +++ b/src/pickers/index.tsx @@ -2,8 +2,7 @@ https://github.com/deseretdigital/dayzed/pull/25 He didn't publish the components to npm, so I copied his work */ -import BaseDatePicker from './base'; -import DatePicker from './basic'; +import BasicDatePicker from './basic'; import RangeDatePicker from './range'; -export { BaseDatePicker, DatePicker, RangeDatePicker }; +export { BasicDatePicker, RangeDatePicker }; diff --git a/src/pickers/range.tsx b/src/pickers/range.tsx index ec10b903..d18ffe78 100644 --- a/src/pickers/range.tsx +++ b/src/pickers/range.tsx @@ -38,12 +38,11 @@ class RangeDatePicker extends React.Component< this.setHoveredDate(date); } - /* eslint-disable-next-line */ - _handleOnDateSelected = ({ selected, selectable, date }) => { - const { selected: selectedDates, onDateSelected, onChange } = this.props; - if (onDateSelected) { - onDateSelected({ selected, selectable, date }); - } + _handleOnDateSelected = ( + { selectable, date }, + event: React.SyntheticEvent + ) => { + const { selected: selectedDates, onChange } = this.props; if (!selectable) { return; @@ -67,7 +66,7 @@ class RangeDatePicker extends React.Component< } if (onChange) { - onChange(newDates); + onChange(event, newDates); } if (newDates.length === 2) { diff --git a/src/types/index.ts b/src/types/index.ts index 1c52d407..b370b4ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,12 +28,7 @@ export type LocaleOptions = export type PickedDayzedProps = Pick< DayzedProps, - | 'date' - | 'maxDate' - | 'minDate' - | 'firstDayOfWeek' - | 'selected' - | 'showOutsideDays' + 'date' | 'maxDate' | 'minDate' | 'firstDayOfWeek' | 'showOutsideDays' >; export type PickedFormInputProps = Pick< @@ -63,9 +58,13 @@ export type SemanticDatepickerProps = PickedDayzedProps & keepOpenOnSelect: boolean; locale: LocaleOptions; onBlur: (event?: React.SyntheticEvent) => void; - onDateChange: (date: Date | Date[] | null) => void; + onChange: ( + event: React.SyntheticEvent | undefined, + data: SemanticDatepickerProps + ) => void; pointing: 'left' | 'right' | 'top left' | 'top right'; type: 'basic' | 'range'; + value: DayzedProps['selected']; }; export type DayzedProps = { @@ -76,18 +75,18 @@ export type DayzedProps = { minDate?: Date; monthsToDisplay: number; offset: number; - onDateSelected: (props: any) => void; + onDateSelected: (dateObj: any, event: React.SyntheticEvent) => void; onOffsetChanged: () => void; - selected: Date | Date[]; + selected: Date | Date[] | null; showOutsideDays: boolean; }; export type BasicDatePickerProps = DayzedProps & { - onChange: (date: Date | null) => void; + onChange: (event: React.SyntheticEvent, date: Date | null) => void; selected: Date; }; export type RangeDatePickerProps = DayzedProps & { - onChange: (dates: Date[] | null) => void; + onChange: (event: React.SyntheticEvent, dates: Date[] | null) => void; selected: Date[]; }; diff --git a/src/utils.ts b/src/utils.ts index 18dcbd33..d1d3e391 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -93,3 +93,11 @@ export const parseOnBlur = ( }; export const onlyNumbers = (value = '') => value.replace(/[^\d]/g, ''); + +export function getShortDate(date?: Date) { + if (!date) { + return undefined; + } + + return date.toISOString().slice(0, 10); +} diff --git a/stories/basic.stories.tsx b/stories/basic.stories.tsx index e835df23..54e7b7fa 100644 --- a/stories/basic.stories.tsx +++ b/stories/basic.stories.tsx @@ -18,20 +18,20 @@ export default { export const simple = () => ( - + ); export const withReadOnly = () => ( - + ); export const withoutClearOnSameDateClick = () => ( @@ -39,28 +39,19 @@ export const withoutClearOnSameDateClick = () => ( export const withAllowOnlyNumbers = () => ( - + ); export const withFirstDayOfWeek = () => ( - + ); export const withOutsideDays = () => ( - + ); @@ -68,7 +59,7 @@ export const withFormatProp = () => ( ); @@ -76,20 +67,17 @@ export const withFormatProp = () => ( export const withBrazilianPortugueseLocale = () => ( ); export const withKeepOpenOnSelect = () => ( - + ); @@ -100,13 +88,13 @@ export const asFormComponent = () => ( @@ -115,19 +103,13 @@ export const asFormComponent = () => ( export const withLeftPointing = () => ( - + ); export const withRightPointing = () => ( - + ); @@ -144,7 +126,7 @@ export const withTopLeftPointing = () => ( > ); @@ -162,7 +144,7 @@ export const withTopRightPointing = () => ( > ); @@ -171,7 +153,7 @@ export const withFilterDate = () => ( @@ -182,7 +164,7 @@ export const withFilterDateSettingMaxDate = () => ( diff --git a/stories/range.stories.tsx b/stories/range.stories.tsx index 0ba17c25..00855290 100644 --- a/stories/range.stories.tsx +++ b/stories/range.stories.tsx @@ -10,7 +10,7 @@ export default { export const simple = () => ( - + ); @@ -19,7 +19,7 @@ export const withRightPointing = () => ( ); @@ -29,7 +29,7 @@ export const withFirstDayOfWeek = () => ( ); @@ -39,7 +39,7 @@ export const withOutsideDays = () => ( ); @@ -49,7 +49,7 @@ export const withFormatProp = () => ( ); @@ -58,7 +58,7 @@ export const withPolishLocale = () => (