Skip to content

Commit

Permalink
feat(datepicker): add formatOptions prop (#661)
Browse files Browse the repository at this point in the history
* added formatOptions prop (#631)

* added format options for date-fns, necessary to support locales

* fixed the property

Co-authored-by: Denis Chumachenko <[email protected]>

* fix: update dependencies and types

* docs: update README

Co-authored-by: Denis <[email protected]>
Co-authored-by: Denis Chumachenko <[email protected]>
  • Loading branch information
3 people authored Oct 1, 2021
1 parent c663207 commit c026caa
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 45 deletions.
45 changes: 23 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,29 @@ More examples [here](https://react-semantic-ui-datepickers.now.sh).

### Own Props

| property | type | required | default | description |
| -------------------- | ----------------------------------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| allowOnlyNumbers | boolean | no | true | Allows the user enter only numbers |
| autoComplete | string | no | -- | Specifies if the input should have autocomplete enabled |
| clearIcon | SemanticICONS \| React.ReactElement | no | 'close' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
| clearOnSameDateClick | boolean | no | true | Controls whether the datepicker's state resets if the same date is selected in succession. |
| clearable | boolean | no | true | Allows the user to clear the value |
| datePickerOnly | boolean | no | false | Allows the date to be selected only via the date picker and disables the text input |
| filterDate | function | no | (date) => true | Function that receives each date and returns a boolean to indicate whether it is enabled or not |
| format | string | no | 'YYYY-MM-DD' | Specifies how the date will be formatted using the [date-fns' format](https://date-fns.org/v1.29.0/docs/format) |
| icon | SemanticICONS \| React.ReactElement | no | 'calendar' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
| inline | boolean | no | false | Uses an inline variant, without the input and the features related to it, e.g. clearable datepicker |
| keepOpenOnClear | boolean | no | false | Keeps the datepicker open (or opens it if it's closed) when the clear icon is clicked |
| 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 | (event) => {} | Callback fired when the input loses focus |
| onFocus | function | no | (event) => {} | Callback fired when the input gets focused focus |
| onChange | function | no | (event, data) => {} | 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' |
| showToday | boolean | no | true | Hides the "Today" button if false |
| type | string | no | basic | Type of input to render. Available options: 'basic' and 'range' |
| value | Date\|Date[] | no | -- | The value of the datepicker |
| property | type | required | default | description |
| -------------------- | ------------------------------------------------------------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| allowOnlyNumbers | boolean | no | true | Allows the user enter only numbers |
| autoComplete | string | no | -- | Specifies if the input should have autocomplete enabled |
| clearIcon | SemanticICONS \| React.ReactElement | no | 'close' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
| clearOnSameDateClick | boolean | no | true | Controls whether the datepicker's state resets if the same date is selected in succession. |
| clearable | boolean | no | true | Allows the user to clear the value |
| datePickerOnly | boolean | no | false | Allows the date to be selected only via the date picker and disables the text input |
| filterDate | function | no | (date) => true | Function that receives each date and returns a boolean to indicate whether it is enabled or not |
| format | string | no | 'YYYY-MM-DD' | Specifies how to format the date using the [date-fns' format](https://date-fns.org/v2.24.0/docs/format) |
| formatOptions | [options](https://date-fns.org/v2.24.0/docs/format#arguments) | no | -- | Specifies the options to format the date using the [date-fns' format](https://date-fns.org/v2.24.0/docs/format) |
| icon | SemanticICONS \| React.ReactElement | no | 'calendar' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
| inline | boolean | no | false | Uses an inline variant, without the input and the features related to it, e.g. clearable datepicker |
| keepOpenOnClear | boolean | no | false | Keeps the datepicker open (or opens it if it's closed) when the clear icon is clicked |
| 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 | (event) => {} | Callback fired when the input loses focus |
| onFocus | function | no | (event) => {} | Callback fired when the input gets focused focus |
| onChange | function | no | (event, data) => {} | 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' |
| showToday | boolean | no | true | Hides the "Today" button if false |
| type | string | no | basic | Type of input to render. Available options: 'basic' and 'range' |
| value | Date\|Date[] | no | -- | The value of the datepicker |

### Form.Input Props

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"babel-jest": "27.2.4",
"babel-loader": "8.2.2",
"cssnano": "4.1.11",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-prettier": "3.4.1",
"husky": "7.0.2",
"jest-transform-css": "2.1.0",
"prettier": "2.4.1",
Expand Down
25 changes: 20 additions & 5 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import parseISO from 'date-fns/parseISO';
import startOfDay from 'date-fns/startOfDay';
import localeEn from '../locales/en-US.json';
import ruLocale from 'date-fns/locale/ru';
import enLocale from '../locales/en-US.json';
import {
formatDate,
formatSelectedDate,
Expand All @@ -24,11 +25,11 @@ const june28 = parseISO('2018-06-28');

describe('moveElementsByN', () => {
it('should return the same array if `n` is zero', () => {
expect(moveElementsByN(0, localeEn.weekdays)).toEqual(localeEn.weekdays);
expect(moveElementsByN(0, enLocale.weekdays)).toEqual(enLocale.weekdays);
});

it('should return the correct array if `n` is different than zero', () => {
expect(moveElementsByN(3, localeEn.weekdays)).toEqual([
expect(moveElementsByN(3, enLocale.weekdays)).toEqual([
'Wednesday',
'Thursday',
'Friday',
Expand All @@ -38,7 +39,7 @@ describe('moveElementsByN', () => {
'Tuesday',
]);

expect(moveElementsByN(5, localeEn.weekdays)).toEqual([
expect(moveElementsByN(5, enLocale.weekdays)).toEqual([
'Friday',
'Saturday',
'Sunday',
Expand Down Expand Up @@ -75,9 +76,15 @@ describe('formatDate', () => {
expect(formatDate(null, 'DD/MM/YYYY')).toBe(undefined);
});

it('should format correctly if a valid date is given', () => {
it('should format correctly if a valid date and a locale are not given', () => {
expect(formatDate(dateTest, 'DD/MM/YYYY')).toBe('21/06/2018');
});

it('should format correctly if a valid date and a locale are given', () => {
expect(formatDate(dateTest, 'EEE, d MMM YYYY', { locale: ruLocale })).toBe(
'чтв, 4 июнь 2018'
);
});
});

describe('isSelectable', () => {
Expand Down Expand Up @@ -150,6 +157,14 @@ describe('formatSelectedDate', () => {
'14/06/2018 - 20/06/2018'
);
});

it('should return the correct result if a valid array of dates is given', () => {
expect(
formatSelectedDate([june14, june20], 'EEE, d MMM YYYY', {
locale: ruLocale,
})
).toBe('чтв, 4 июнь 2018 - срд, 3 июнь 2018');
});
});

describe('parseFormatString', () => {
Expand Down
23 changes: 16 additions & 7 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,14 @@ class SemanticDatepicker extends React.Component<
}

get initialState() {
const { format, value } = this.props;
const { format, value, formatOptions } = this.props;
const initialSelectedDate = this.isRangeInput ? [] : null;

return {
isVisible: false,
locale: this.locale,
selectedDate: value || initialSelectedDate,
selectedDateFormatted: formatSelectedDate(value, format),
selectedDateFormatted: formatSelectedDate(value, format, formatOptions),
typedValue: null,
};
}
Expand Down Expand Up @@ -264,7 +264,7 @@ class SemanticDatepicker extends React.Component<
};

handleRangeInput = (newDates, event) => {
const { format, keepOpenOnSelect, onChange } = this.props;
const { format, keepOpenOnSelect, onChange, formatOptions } = this.props;

if (!newDates || !newDates.length) {
this.resetState(event);
Expand All @@ -274,7 +274,11 @@ class SemanticDatepicker extends React.Component<

const newState = {
selectedDate: newDates,
selectedDateFormatted: formatSelectedDate(newDates, format),
selectedDateFormatted: formatSelectedDate(
newDates,
format,
formatOptions
),
typedValue: null,
};

Expand All @@ -288,8 +292,13 @@ class SemanticDatepicker extends React.Component<
};

handleBasicInput = (newDate, event) => {
const { format, keepOpenOnSelect, onChange, clearOnSameDateClick } =
this.props;
const {
format,
keepOpenOnSelect,
onChange,
clearOnSameDateClick,
formatOptions,
} = this.props;

if (!newDate) {
// if clearOnSameDateClick is true (this is the default case)
Expand All @@ -313,7 +322,7 @@ class SemanticDatepicker extends React.Component<
const newState = {
isVisible: keepOpenOnSelect,
selectedDate: newDate,
selectedDateFormatted: formatSelectedDate(newDate, format),
selectedDateFormatted: formatSelectedDate(newDate, format, formatOptions),
typedValue: null,
};

Expand Down
4 changes: 4 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import format from 'date-fns/format';
import { Props as DayzedProps, RenderProps } from 'dayzed';
import { FormInputProps, SemanticICONS } from 'semantic-ui-react';

Expand Down Expand Up @@ -59,6 +60,8 @@ export type PickedFormInputProps = Pick<
| 'readOnly'
>;

export type FnsFormatOptions = Parameters<typeof format>[2];

export type SemanticDatepickerProps = PickedDayzedProps &
PickedFormInputProps & {
allowOnlyNumbers: boolean;
Expand All @@ -68,6 +71,7 @@ export type SemanticDatepickerProps = PickedDayzedProps &
clearIcon?: SemanticICONS | React.ReactElement;
filterDate: (date: Date) => boolean;
format: string;
formatOptions?: FnsFormatOptions;
keepOpenOnClear: boolean;
keepOpenOnSelect: boolean;
icon?: SemanticICONS | React.ReactElement;
Expand Down
21 changes: 15 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import isBefore from 'date-fns/isBefore';
import parse from 'date-fns/parse';
import startOfDay from 'date-fns/startOfDay';
import { DateObj } from 'dayzed';
import { Object } from './types';
import { Object, FnsFormatOptions } from './types';

export const keys = {
enter: 13,
Expand Down Expand Up @@ -36,8 +36,14 @@ export const getToday = (minDate?: Date, maxDate?: Date): DateObj => {
};
};

export const formatDate = (date: Date | null, dateFormat: string) =>
date ? format(startOfDay(date), convertTokens(dateFormat)) : undefined;
export const formatDate = (
date: Date | null,
dateFormat: string,
formatOptions?: FnsFormatOptions
) =>
date
? format(startOfDay(date), convertTokens(dateFormat), formatOptions)
: undefined;

export const omit = (keysToOmit: string[], obj: Object) => {
const newObj = { ...obj };
Expand All @@ -62,15 +68,18 @@ export const moveElementsByN = <T>(n: number, arr: T[]) =>

export const formatSelectedDate = (
selectedDate: Date | Date[] | null | undefined,
dateFormat: string
dateFormat: string,
formatOptions?: FnsFormatOptions
) => {
if (!selectedDate) {
return '';
}

return Array.isArray(selectedDate)
? selectedDate.map((date) => formatDate(date, dateFormat)).join(' - ')
: formatDate(selectedDate, dateFormat);
? selectedDate
.map((date) => formatDate(date, dateFormat, formatOptions))
.join(' - ')
: formatDate(selectedDate, dateFormat, formatOptions);
};

export const parseFormatString = (formatString: string) =>
Expand Down
Loading

1 comment on commit c026caa

@vercel
Copy link

@vercel vercel bot commented on c026caa Oct 1, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.