Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: add a snap flag to enable snapping to plausible steps on inc & dec in TimeView #603

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 7 additions & 3 deletions DateTime.d.ts
Expand Up @@ -16,9 +16,9 @@ declare namespace ReactDatetimeClass {
export type ViewMode = "years" | "months" | "days" | "time";

export interface TimeConstraint {
min: number;
max: number;
step: number;
min?: number;
max?: number;
step?: number;
}

export interface TimeConstraints {
Expand Down Expand Up @@ -170,6 +170,10 @@ declare namespace ReactDatetimeClass {
close it.
*/
disableOnClickOutside?: boolean;
/*
Enables snapping to the relevant value on increase/decrease based on step.
*/
snap?: boolean;
}

export interface DatetimepickerState {
Expand Down
5 changes: 3 additions & 2 deletions DateTime.js
Expand Up @@ -42,7 +42,8 @@ var Datetime = createClass({
open: TYPES.bool,
strictParsing: TYPES.bool,
closeOnSelect: TYPES.bool,
closeOnTab: TYPES.bool
closeOnTab: TYPES.bool,
snap: TYPES.bool,
},

getInitialState: function() {
Expand Down Expand Up @@ -416,7 +417,7 @@ var Datetime = createClass({
},

componentProps: {
fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints'],
fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints', 'snap'],
fromState: ['viewDate', 'selectedDate', 'updateOn'],
fromThis: ['setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment', 'handleClickOutside']
},
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -70,6 +70,7 @@ render: function() {
| **closeOnTab** | `boolean` | `true` | When `true` and the input is focused, pressing the `tab` key will close the datepicker.
| **timeConstraints** | `object` | `null` | Add some constraints to the timepicker. It accepts an `object` with the format `{ hours: { min: 9, max: 15, step: 2 }}`, this example means the hours can't be lower than `9` and higher than `15`, and it will change adding or subtracting `2` hours everytime the buttons are clicked. The constraints can be added to the `hours`, `minutes`, `seconds` and `milliseconds`.
| **disableCloseOnClickOutside** | `boolean` | `false` | When `true`, keep the datepicker open when click event is triggered outside of component. When `false`, close it.
| **snap** | `boolean` | `false` | When `true`, in `TimeView`: increasing/decreasing a value (hour, minute, etc) would snap to the next/previous _plausible_ value (determined by `step` in `TimeConstrains`) Example: consider `value: 12` and `step: 5`; increasing the value would snap to `15` instead of `17` and decreasing would snap to `10` instead of `7`. When `false`, default behavior (just add/substract the `step` value from the current `value`).

## i18n
Different language and date formats are supported by react-datetime. React uses [Moment.js](http://momentjs.com/) to format the dates, and the easiest way of changing the language of the calendar is [changing the Moment.js locale](http://momentjs.com/docs/#/i18n/changing-locale/).
Expand Down
4 changes: 4 additions & 0 deletions react-datetime.d.ts
Expand Up @@ -157,6 +157,10 @@ declare module ReactDatetime {
close it.
*/
disableOnClickOutside?: boolean;
/*
Enables snapping to the relevant value on increase/decrease based on step.
*/
snap?: boolean;
}

interface DatetimeComponent extends React.ComponentClass<DatetimepickerProps> {
Expand Down
36 changes: 27 additions & 9 deletions src/TimeView.js
Expand Up @@ -201,24 +201,42 @@ var DateTimePickerTime = createClass({
},

toggleDayPart: function( type ) { // type is always 'hours'
var value = parseInt( this.state[ type ], 10) + 12;
var value = parseInt( this.state[ type ], 10 ) + 12;
if ( value > this.timeConstraints[ type ].max )
value = this.timeConstraints[ type ].min + ( value - ( this.timeConstraints[ type ].max + 1 ) );
return this.pad( type, value );
},

increase: function( type ) {
var value = parseInt( this.state[ type ], 10) + this.timeConstraints[ type ].step;
if ( value > this.timeConstraints[ type ].max )
value = this.timeConstraints[ type ].min + ( value - ( this.timeConstraints[ type ].max + 1 ) );
return this.pad( type, value );
var step = this.timeConstraints[ type ].step;
var previousValue = parseInt( this.state[ type ], 10 );
var isSnapEnabled = Boolean( this.props.snap );

var nextValue = ( isSnapEnabled && previousValue % step !== 0 && step > 1 ) ?
previousValue + ( step - previousValue % step ) :
previousValue + step;

if ( nextValue > this.timeConstraints[ type ].max ) {
nextValue = this.timeConstraints[ type ].min + ( nextValue - ( this.timeConstraints[ type ].max + 1 ) );
}

return this.pad( type, nextValue );
},

decrease: function( type ) {
var value = parseInt( this.state[ type ], 10) - this.timeConstraints[ type ].step;
if ( value < this.timeConstraints[ type ].min )
value = this.timeConstraints[ type ].max + 1 - ( this.timeConstraints[ type ].min - value );
return this.pad( type, value );
var step = this.timeConstraints[ type ].step;
var previousValue = parseInt( this.state[ type ], 10 );
var isSnapEnabled = Boolean( this.props.snap );

var nextValue = ( isSnapEnabled && previousValue % step !== 0 && step > 1 ) ?
previousValue - ( previousValue % step ) :
previousValue - step;

if ( nextValue < this.timeConstraints[ type ].min ) {
nextValue = this.timeConstraints[ type ].max + 1 - ( this.timeConstraints[ type ].min - nextValue );
}

return this.pad( type, nextValue );
},

pad: function( type, value ) {
Expand Down
76 changes: 75 additions & 1 deletion test/tests.spec.js
Expand Up @@ -512,7 +512,7 @@ describe('Datetime', () => {
expect(utils.isOpen(component)).toBeTruthy();
});

it('disableCloseOnClickOutside=false', () => {
it('disableCloseOnClickOutside=false', () => {
const date = new Date(2000, 0, 15, 2, 2, 2, 2),
component = utils.createDatetime({ value: date, disableCloseOnClickOutside: false });

Expand Down Expand Up @@ -556,6 +556,43 @@ describe('Datetime', () => {
expect(utils.getSeconds(component)).toEqual('03');
});

it('should snap to next plausible step on increase with snap enabled', () => {
const component = utils.createDatetime({
snap: true,
timeFormat: 'HH:mm:ss:SSS',
viewMode: 'time',
defaultValue: new Date(2000, 0, 15, 2, 2, 50, 2),
timeConstraints: {
hours: {
step: 2
},
minutes: {
step: 5
},
seconds: {
step: 15
},
milliseconds: {
step: 100
}
},
});

expect(utils.getHours(component)).toEqual('2');
utils.increaseHour(component);
expect(utils.getHours(component)).toEqual('4');

expect(utils.getMinutes(component)).toEqual('02');
utils.increaseMinute(component);
expect(utils.getMinutes(component)).toEqual('05');

expect(utils.getSeconds(component)).toEqual('50');
utils.increaseSecond(component);
expect(utils.getSeconds(component)).toEqual('00');
utils.increaseSecond(component);
expect(utils.getSeconds(component)).toEqual('15');
});

it('decrease time', () => {
let i = 0;
const date = new Date(2000, 0, 15, 2, 2, 2, 2),
Expand Down Expand Up @@ -588,6 +625,43 @@ describe('Datetime', () => {
expect(utils.getSeconds(component)).toEqual('01');
});

it('should snap to previews plausible step on decrease with snap enabled', () => {
const component = utils.createDatetime({
snap: true,
timeFormat: 'HH:mm:ss:SSS',
viewMode: 'time',
defaultValue: new Date(2000, 0, 15, 5, 2, 2, 2),
timeConstraints: {
hours: {
step: 2
},
minutes: {
step: 5
},
seconds: {
step: 15
},
milliseconds: {
step: 100
}
},
});

expect(utils.getHours(component)).toEqual('5');
utils.decreaseHour(component);
expect(utils.getHours(component)).toEqual('4');

expect(utils.getMinutes(component)).toEqual('02');
utils.decreaseMinute(component);
expect(utils.getMinutes(component)).toEqual('00');

expect(utils.getSeconds(component)).toEqual('02');
utils.decreaseSecond(component);
expect(utils.getSeconds(component)).toEqual('00');
utils.decreaseSecond(component);
expect(utils.getSeconds(component)).toEqual('45');
});

it('long increase time', (done) => {
const date = new Date(2000, 0, 15, 2, 2, 2, 2),
component = utils.createDatetime({ timeFormat: 'HH:mm:ss:SSS', viewMode: 'time', defaultValue: date });
Expand Down