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

[DateRangePicker] support time selection #2895

Merged
Merged
6 changes: 6 additions & 0 deletions packages/datetime/src/_daterangepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
display: none;
}

.#{$ns}-daterangepicker-timepickers {
display: flex;
justify-content: space-around;
clear: both;
Copy link
Contributor

Choose a reason for hiding this comment

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

@Sam-Kramer why did you add this property? it has no effect since we don't use floats.

}

.#{$ns}-daterangepicker-shortcuts + .DayPicker {
border-left: 1px solid $pt-divider-black;
min-width: $datepicker-min-width + $pt-grid-size;
Expand Down
1 change: 1 addition & 0 deletions packages/datetime/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const DATERANGEPICKER_SINGLE_MONTH = `${DATERANGEPICKER}-single-month`;
export const DATERANGEPICKER_DAY_SELECTED_RANGE = `${DATEPICKER_DAY}--selected-range`;
export const DATERANGEPICKER_DAY_HOVERED_RANGE = `${DATEPICKER_DAY}--hovered-range`;
export const DATERANGEPICKER_SHORTCUTS = `${DATERANGEPICKER}-shortcuts`;
export const DATERANGEPICKER_TIMEPICKERS = `${DATERANGEPICKER}-timepickers`;

export const DATETIMEPICKER = `${NS}-datetimepicker`;

Expand Down
62 changes: 58 additions & 4 deletions packages/datetime/src/dateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import DateRange = DateUtils.DateRange;

import * as Errors from "./common/errors";
import { MonthAndYear } from "./common/monthAndYear";

import { DatePickerCaption } from "./datePickerCaption";
import {
combineModifiers,
Expand All @@ -31,6 +30,7 @@ import {
import { DatePickerNavbar } from "./datePickerNavbar";
import { DateRangeSelectionStrategy } from "./dateRangeSelectionStrategy";
import { Shortcuts } from "./shortcuts";
import { TimePicker } from "./timePicker";

export interface IDateRangeShortcut {
label: string;
Expand Down Expand Up @@ -113,6 +113,7 @@ export interface IDateRangePickerState {
leftView?: MonthAndYear;
rightView?: MonthAndYear;
value?: DateRange;
time?: DateRange;
Copy link
Contributor

Choose a reason for hiding this comment

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

my final question is... can this feature be implemented without adding this time state? i was able to do so in DatePicker by simply treating the regular value as both date and time (cuz it is).

}

export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps, IDateRangePickerState> {
Expand All @@ -124,6 +125,7 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps
minDate: getDefaultMinDate(),
reverseMonthAndYearMenus: false,
shortcuts: true,
timePickerProps: {},
};

public static displayName = `${DISPLAYNAME_PREFIX}.DateRangePicker`;
Expand Down Expand Up @@ -166,6 +168,7 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps
public constructor(props: IDateRangePickerProps, context?: any) {
super(props, context);
const value = getInitialValue(props);
const time: DateRange = value;
const initialMonth = getInitialMonth(props, value);

// if the initial month is the last month of the picker's
Expand All @@ -187,7 +190,7 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps
!props.contiguousCalendarMonths && rightDate != null && !DateUtils.areSameMonth(initialMonth, rightDate)
? MonthAndYear.fromDate(rightDate)
: leftView.getNextMonth();
this.state = { leftView, rightView, value, hoverValue: [null, null] };
this.state = { leftView, rightView, value, hoverValue: [null, null], time };
}

public render() {
Expand All @@ -203,7 +206,10 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps
return (
<div className={classes}>
{this.maybeRenderShortcuts()}
{this.renderCalendars(isShowingOneMonth)}
<div>
{this.renderCalendars(isShowingOneMonth)}
{this.maybeRenderTimePickers()}
</div>
</div>
);
}
Expand Down Expand Up @@ -272,6 +278,52 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps
];
}

private maybeRenderTimePickers() {
const { timePrecision, timePickerProps } = this.props;
if (timePrecision == null && timePickerProps === DateRangePicker.defaultProps.timePickerProps) {
return null;
}
return (
<div className={DateClasses.DATERANGEPICKER_TIMEPICKERS}>
<TimePicker
precision={timePrecision}
{...timePickerProps}
onChange={this.handleTimeChangeLeftCalendar}
value={this.state.time[0]}
/>
<TimePicker
precision={timePrecision}
{...timePickerProps}
onChange={this.handleTimeChangeRightCalendar}
value={this.state.time[1]}
/>
</div>
);
}

private handleTimeChange = (newTime: Date, dateIndex: number) => {
Utils.safeInvoke(this.props.timePickerProps.onChange, newTime);
const { value, time } = this.state;
const newValue = DateUtils.getDateTime(
value[dateIndex] != null ? DateUtils.clone(value[dateIndex]) : new Date(),
newTime,
);
const newDateRange: DateRange = [value[0], value[1]];
newDateRange[dateIndex] = newValue;
const newTimeRange: DateRange = [time[0], time[1]];
newTimeRange[dateIndex] = newTime;
Utils.safeInvoke(this.props.onChange, newDateRange);
this.setState({ value: newDateRange, time: newTimeRange });
};

private handleTimeChangeLeftCalendar = (time: Date) => {
this.handleTimeChange(time, 0);
};

private handleTimeChangeRightCalendar = (time: Date) => {
this.handleTimeChange(time, 1);
};

private renderCalendars(isShowingOneMonth: boolean) {
const { contiguousCalendarMonths, dayPickerProps, locale, localeUtils, maxDate, minDate } = this.props;
const dayPickerBaseProps: DayPickerProps = {
Expand Down Expand Up @@ -417,12 +469,14 @@ export class DateRangePicker extends AbstractPureComponent<IDateRangePickerProps

private handleNextState = (nextValue: DateRange) => {
const { value } = this.state;
nextValue[0] = DateUtils.getDateTime(nextValue[0], this.state.time[0]);
nextValue[1] = DateUtils.getDateTime(nextValue[1], this.state.time[1]);

const nextState = getStateChange(value, nextValue, this.state, this.props.contiguousCalendarMonths);

if (this.props.value == null) {
this.setState(nextState);
}

Utils.safeInvoke(this.props.onChange, nextValue);
};

Expand Down
55 changes: 55 additions & 0 deletions packages/datetime/test/dateRangePickerTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
DateRangePicker,
IDatePickerModifiers,
IDateRangePickerProps,
TimePicker,
TimePrecision,
} from "../src/index";
import { assertDayDisabled } from "./common/dateTestUtils";

Expand Down Expand Up @@ -1002,6 +1004,54 @@ describe("<DateRangePicker>", () => {
});
});

describe("time selection", () => {
const defaultRange: DateRange = [new Date(2012, 2, 5, 6, 5, 40), new Date(2012, 4, 5, 7, 8, 20)];

it("setting timePrecision shows a TimePicker", () => {
const { wrapper } = render();
assert.isFalse(wrapper.find(TimePicker).exists());
wrapper.setProps({ timePrecision: "minute" });
assert.isTrue(wrapper.find(TimePicker).exists());
});

it("setting timePickerProps shows a TimePicker", () => {
const { wrapper } = render({ timePickerProps: {} });
assert.isTrue(wrapper.find(TimePicker).exists());
});

it("onChange fired when the time is changed", () => {
const { wrapper } = render({ timePickerProps: { showArrowButtons: true }, defaultValue: defaultRange });
assert.isTrue(onChangeSpy.notCalled);
wrapper
.find(`.${DateClasses.TIMEPICKER_ARROW_BUTTON}.${DateClasses.TIMEPICKER_HOUR}`)
.first()
.simulate("click");
assert.isTrue(onChangeSpy.calledOnce);
const cbHour = onChangeSpy.firstCall.args[0][0].getHours();
assert.strictEqual(cbHour, defaultRange[0].getHours() + 1);
});

it("changing date does not change time", () => {
render({ timePrecision: "minute", defaultValue: defaultRange }).left.clickDay(16);
assert.isTrue(DateUtils.areSameTime(onChangeSpy.firstCall.args[0][0] as Date, defaultRange[0]));
});

it("changing time does not change date", () => {
render({ timePrecision: "minute", defaultValue: defaultRange }).setTimeInput("minute", 10, "left");
assert.isTrue(DateUtils.areSameDay(onChangeSpy.firstCall.args[0][0] as Date, defaultRange[0]));
});

it("changing time without date uses today", () => {
render({ timePrecision: "minute" }).setTimeInput("minute", 45, "left");
assert.isTrue(DateUtils.areSameDay(onChangeSpy.firstCall.args[0][0] as Date, new Date()));
});

it("clicking a shortcut doesn't change time", () => {
render({ timePrecision: "minute", defaultValue: defaultRange }).clickShortcut();
assert.isTrue(DateUtils.areSameTime(onChangeSpy.firstCall.args[0][0] as Date, defaultRange[0]));
});
});

function dayNotOutside(day: ReactWrapper) {
return !day.hasClass(DateClasses.DATEPICKER_DAY_OUTSIDE);
}
Expand Down Expand Up @@ -1060,6 +1110,11 @@ describe("<DateRangePicker>", () => {
getDays: (className: string) => {
return wrapper.find(`.${className}`).filterWhere(dayNotOutside);
},
setTimeInput: (precision: TimePrecision | "hour", value: number, which: "left" | "right") =>
harness.wrapper
.find(`.${DateClasses.TIMEPICKER}-${precision}`)
.at(which === "left" ? 0 : 1)
.simulate("blur", { target: { value } }),
};
return harness;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ export const MomentDate: React.SFC<{ date: Date; format?: string; withTime?: boo
}
};

export const MomentDateRange: React.SFC<{ range: DateRange; format?: string } & IProps> = ({
export const MomentDateRange: React.SFC<{ range: DateRange; format?: string; withTime?: boolean } & IProps> = ({
className,
range: [start, end],
format = FORMAT,
withTime = false,
format = withTime ? FORMAT_TIME : FORMAT,
}) => (
<div className={classNames("docs-date-range", className)}>
<MomentDate date={start} format={format} />
<MomentDate withTime={withTime} date={start} format={format} />
<Icon icon="arrow-right" />
<MomentDate date={end} format={format} />
<MomentDate withTime={withTime} date={end} format={format} />
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
*/

import { Classes, H5, HTMLSelect, Label, Switch } from "@blueprintjs/core";
import { Example, handleBooleanChange, handleNumberChange, IExampleProps } from "@blueprintjs/docs-theme";
import {
Example,
handleBooleanChange,
handleNumberChange,
handleStringChange,
IExampleProps,
} from "@blueprintjs/docs-theme";
import moment from "moment";
import * as React from "react";

import { DateRange, DateRangePicker } from "@blueprintjs/datetime";
import { DateRange, DateRangePicker, TimePrecision } from "@blueprintjs/datetime";
import { MomentDateRange } from "./common/momentDate";
import { PrecisionSelect } from "./common/precisionSelect";

export interface IDateRangePickerExampleState {
allowSingleDayRange?: boolean;
Expand All @@ -20,6 +27,7 @@ export interface IDateRangePickerExampleState {
minDateIndex?: number;
reverseMonthAndYearMenus?: boolean;
shortcuts?: boolean;
timePrecision?: TimePrecision;
}

interface IDateOption {
Expand Down Expand Up @@ -66,6 +74,9 @@ export class DateRangePickerExample extends React.PureComponent<IExampleProps, I

private handleMaxDateIndexChange = handleNumberChange(maxDateIndex => this.setState({ maxDateIndex }));
private handleMinDateIndexChange = handleNumberChange(minDateIndex => this.setState({ minDateIndex }));
private handlePrecisionChange = handleStringChange((timePrecision: TimePrecision | undefined) =>
this.setState({ timePrecision }),
);

private toggleReverseMonthAndYearMenus = handleBooleanChange(reverseMonthAndYearMenus =>
this.setState({ reverseMonthAndYearMenus }),
Expand All @@ -77,22 +88,19 @@ export class DateRangePickerExample extends React.PureComponent<IExampleProps, I
});

public render() {
const minDate = MIN_DATE_OPTIONS[this.state.minDateIndex].value;
const maxDate = MAX_DATE_OPTIONS[this.state.maxDateIndex].value;

const { minDateIndex, maxDateIndex, ...props } = this.state;
const minDate = MIN_DATE_OPTIONS[minDateIndex].value;
const maxDate = MAX_DATE_OPTIONS[maxDateIndex].value;
return (
<Example options={this.renderOptions()} showOptionsBelowExample={true} {...this.props}>
<DateRangePicker
allowSingleDayRange={this.state.allowSingleDayRange}
contiguousCalendarMonths={this.state.contiguousCalendarMonths}
{...props}
className={Classes.ELEVATION_1}
maxDate={maxDate}
minDate={minDate}
onChange={this.handleDateChange}
reverseMonthAndYearMenus={this.state.reverseMonthAndYearMenus}
shortcuts={this.state.shortcuts}
/>
<MomentDateRange range={this.state.dateRange} />
<MomentDateRange withTime={props.timePrecision !== undefined} range={this.state.dateRange} />
</Example>
);
}
Expand Down Expand Up @@ -133,6 +141,14 @@ export class DateRangePickerExample extends React.PureComponent<IExampleProps, I
this.handleMaxDateIndexChange,
)}
</div>
<div>
<PrecisionSelect
allowNone={true}
label="Time precision"
value={this.state.timePrecision}
onChange={this.handlePrecisionChange}
/>
</div>
</>
);
}
Expand Down