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

Add shortcuts to DatePicker #3818

Merged
merged 7 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/datetime/src/_datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ $header-margin: ($header-height - $pt-input-height) / 2 !default;
// react-day-picker does not conform to our naming scheme
// stylelint-disable selector-class-pattern
.#{$ns}-datepicker {
display: flex;
position: relative;
border-radius: $pt-border-radius;
background: $datepicker-background-color;
Expand Down
17 changes: 17 additions & 0 deletions packages/datetime/src/dateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { isDateValid, isDayInRange } from "./common/dateUtils";
import { getFormattedDateString, IDateFormatProps } from "./dateFormat";
import { DatePicker } from "./datePicker";
import { getDefaultMaxDate, getDefaultMinDate, IDatePickerBaseProps } from "./datePickerCore";
import { IDatePickerShortcut } from "./shortcuts";

export interface IDateInputProps extends IDatePickerBaseProps, IDateFormatProps, IProps {
/**
Expand Down Expand Up @@ -125,6 +126,15 @@ export interface IDateInputProps extends IDatePickerBaseProps, IDateFormatProps,
*/
showActionsBar?: boolean;

/**
* Whether shortcuts to quickly select a date are displayed or not.
* If `true`, preset shortcuts will be displayed.
* If `false`, no shortcuts will be displayed.
* If an array is provided, the custom shortcuts will be displayed.
* @default false
*/
shortcuts?: boolean | IDatePickerShortcut[];

/**
* The currently selected day. If this prop is provided, the component acts in a controlled manner.
* To display no date in the input field, pass `null` to the value prop. To display an invalid date error
Expand All @@ -145,6 +155,7 @@ export interface IDateInputState {
valueString: string;
isInputFocused: boolean;
isOpen: boolean;
selectedShortcutIndex?: number;
}

@polyfill
Expand Down Expand Up @@ -200,6 +211,8 @@ export class DateInput extends AbstractPureComponent2<IDateInputProps, IDateInpu
dayPickerProps={dayPickerProps}
onChange={this.handleDateChange}
value={dateValue}
onShortcutChange={this.handleShortcutChange}
selectedShortcutIndex={this.state.selectedShortcutIndex}
/>
</div>
);
Expand Down Expand Up @@ -439,6 +452,10 @@ export class DateInput extends AbstractPureComponent2<IDateInputProps, IDateInpu
}
};

private handleShortcutChange = (_: IDatePickerShortcut, selectedShortcutIndex: number) => {
this.setState({ selectedShortcutIndex });
};

/** safe wrapper around invoking input props event handler (prop defaults to undefined) */
private safeInvokeInputProp(name: keyof HTMLInputProps, e: React.SyntheticEvent<HTMLElement>) {
const { inputProps = {} } = this.props;
Expand Down
137 changes: 112 additions & 25 deletions packages/datetime/src/datePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as Errors from "./common/errors";
import { DatePickerCaption } from "./datePickerCaption";
import { getDefaultMaxDate, getDefaultMinDate, IDatePickerBaseProps } from "./datePickerCore";
import { DatePickerNavbar } from "./datePickerNavbar";
import { IDatePickerShortcut, IDateRangeShortcut, Shortcuts } from "./shortcuts";
import { TimePicker } from "./timePicker";

export interface IDatePickerProps extends IDatePickerBaseProps, IProps {
Expand Down Expand Up @@ -66,12 +67,31 @@ export interface IDatePickerProps extends IDatePickerBaseProps, IProps {
*/
onChange?: (selectedDate: Date, isUserChange: boolean) => void;

/**
* Called when the `shortcuts` props is enabled and the user changes the shortcut.
*/
onShortcutChange?: (shortcut: IDatePickerShortcut, index: number) => void;

/**
* Whether the bottom bar displaying "Today" and "Clear" buttons should be shown.
* @default false
*/
showActionsBar?: boolean;

/**
* Whether shortcuts to quickly select a date are displayed or not.
* If `true`, preset shortcuts will be displayed.
* If `false`, no shortcuts will be displayed.
* If an array is provided, the custom shortcuts will be displayed.
*/
shortcuts?: boolean | IDatePickerShortcut[];

/**
* The currently selected shortcut.
* If this prop is provided, the component acts in a controlled manner.
*/
selectedShortcutIndex?: number;

/**
* Text for the today button in the action bar.
* @default "Today"
Expand All @@ -95,6 +115,7 @@ export interface IDatePickerState {
displayYear: number;
selectedDay: number | null;
value: Date | null;
selectedShortcutIndex?: number;
}

@polyfill
Expand All @@ -107,6 +128,7 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi
maxDate: getDefaultMaxDate(),
minDate: getDefaultMinDate(),
reverseMonthAndYearMenus: false,
shortcuts: false,
showActionsBar: false,
timePickerProps: {},
todayButtonText: "Today",
Expand All @@ -124,6 +146,8 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi
displayMonth: initialMonth.getMonth(),
displayYear: initialMonth.getFullYear(),
selectedDay: value == null ? null : value.getDate(),
selectedShortcutIndex:
this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
value,
};
}
Expand All @@ -134,26 +158,29 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi

return (
<div className={classNames(Classes.DATEPICKER, className)}>
<DayPicker
showOutsideDays={true}
locale={locale}
localeUtils={localeUtils}
modifiers={this.getDatePickerModifiers()}
{...dayPickerProps}
canChangeMonth={true}
captionElement={this.renderCaption}
navbarElement={this.renderNavbar}
disabledDays={this.getDisabledDaysModifier()}
fromMonth={minDate}
month={new Date(displayYear, displayMonth)}
onDayClick={this.handleDayClick}
onMonthChange={this.handleMonthChange}
selectedDays={this.state.value}
toMonth={maxDate}
renderDay={this.renderDay}
/>
{this.maybeRenderTimePicker()}
{showActionsBar && this.renderOptionsBar()}
{this.maybeRenderShortcuts()}
<div>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will this div cause issues? Adding this div and the maybeRenderShortcuts is all that changed here

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, it's a very slight HTML break... but relying on > selectors is not good practice IMO... I'm fine with this change

<DayPicker
showOutsideDays={true}
locale={locale}
localeUtils={localeUtils}
modifiers={this.getDatePickerModifiers()}
{...dayPickerProps}
canChangeMonth={true}
captionElement={this.renderCaption}
navbarElement={this.renderNavbar}
disabledDays={this.getDisabledDaysModifier()}
fromMonth={minDate}
month={new Date(displayYear, displayMonth)}
onDayClick={this.handleDayClick}
onMonthChange={this.handleMonthChange}
selectedDays={this.state.value}
toMonth={maxDate}
renderDay={this.renderDay}
/>
{this.maybeRenderTimePicker()}
{showActionsBar && this.renderOptionsBar()}
</div>
</div>
);
}
Expand All @@ -175,6 +202,10 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi
value,
});
}

if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) {
this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex });
}
}

protected validateProps(props: IDatePickerProps) {
Expand Down Expand Up @@ -269,11 +300,72 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi
);
}

private maybeRenderShortcuts() {
const { shortcuts } = this.props;
if (shortcuts == null || shortcuts === false) {
return null;
}

const { selectedShortcutIndex } = this.state;
const { maxDate, minDate, timePrecision } = this.props;
// Reuse the existing date range shortcuts and only care about start date
const dateRangeShortcuts: IDateRangeShortcut[] | true =
shortcuts === true
? true
: shortcuts.map(shortcut => ({
...shortcut,
dateRange: [shortcut.date, undefined],
}));
return [
<Shortcuts
key="shortcuts"
{...{
allowSingleDayRange: true,
maxDate,
minDate,
selectedShortcutIndex,
shortcuts: dateRangeShortcuts,
timePrecision,
}}
onShortcutClick={this.handleShortcutClick}
useSingleDateShortcuts={true}
/>,
<Divider key="div" />,
];
}

private handleDayClick = (day: Date, modifiers: DayModifiers, e: React.MouseEvent<HTMLDivElement>) => {
Utils.safeInvoke(this.props.dayPickerProps.onDayClick, day, modifiers, e);
if (modifiers.disabled) {
return;
}

this.updateDay(day);

// allow toggling selected date by clicking it again (if prop enabled)
const newValue =
this.props.canClearSelection && modifiers.selected ? null : DateUtils.getDateTime(day, this.state.value);
this.updateValue(newValue, true);
};

private handleShortcutClick = (shortcut: IDateRangeShortcut, selectedShortcutIndex: number) => {
const { onShortcutChange, selectedShortcutIndex: currentShortcutIndex } = this.props;
const { dateRange, includeTime } = shortcut;
const newDate = dateRange[0];
const newValue = includeTime ? newDate : DateUtils.getDateTime(newDate, this.state.value);

this.updateDay(newDate);
this.updateValue(newValue, true);

if (currentShortcutIndex === undefined) {
this.setState({ selectedShortcutIndex });
}

const datePickerShortcut = { ...shortcut, date: shortcut.dateRange[0] };
Utils.safeInvoke(onShortcutChange, datePickerShortcut, selectedShortcutIndex);
};

private updateDay = (day: Date) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pulled out this common logic for when clicking a shortcut vs. a date on the calendar

if (this.props.value === undefined) {
// set now if uncontrolled, otherwise they'll be updated in `componentDidUpdate`
this.setState({
Expand All @@ -285,11 +377,6 @@ export class DatePicker extends AbstractPureComponent2<IDatePickerProps, IDatePi
if (this.state.value != null && this.state.value.getMonth() !== day.getMonth()) {
this.ignoreNextMonthChange = true;
}

// allow toggling selected date by clicking it again (if prop enabled)
const newValue =
this.props.canClearSelection && modifiers.selected ? null : DateUtils.getDateTime(day, this.state.value);
this.updateValue(newValue, true);
};

private computeValidDateInSpecifiedMonthYear(displayYear: number, displayMonth: number): Date {
Expand Down
3 changes: 2 additions & 1 deletion packages/datetime/src/dateRangeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import { areSameTime, DateRange, isDateValid, isDayInRange } from "./common/date
import * as Errors from "./common/errors";
import { getFormattedDateString, IDateFormatProps } from "./dateFormat";
import { getDefaultMaxDate, getDefaultMinDate, IDatePickerBaseProps } from "./datePickerCore";
import { DateRangePicker, IDateRangeShortcut } from "./dateRangePicker";
import { DateRangePicker } from "./dateRangePicker";
import { IDateRangeShortcut } from "./shortcuts";

export interface IDateRangeInputProps extends IDatePickerBaseProps, IDateFormatProps, IProps {
/**
Expand Down
5 changes: 2 additions & 3 deletions packages/datetime/src/dateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ import { DateRangeSelectionStrategy } from "./dateRangeSelectionStrategy";
import { IDateRangeShortcut, Shortcuts } from "./shortcuts";
import { TimePicker } from "./timePicker";

export { IDateRangeShortcut };

export interface IDateRangePickerProps extends IDatePickerBaseProps, IProps {
/**
* Whether the start and end dates of the range can be the same day.
Expand Down Expand Up @@ -223,7 +221,8 @@ export class DateRangePicker extends AbstractPureComponent2<IDateRangePickerProp
hoverValue: [null, null],
leftView,
rightView,
selectedShortcutIndex: this.props.selectedShortcutIndex || -1,
selectedShortcutIndex:
this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
time,
value,
};
Expand Down
27 changes: 27 additions & 0 deletions packages/datetime/src/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ A `DatePicker` shows a monthly calendar and allows the user to choose a single d

@reactExample DatePickerExample

@## Shortcuts

The menu on the left of the calendars provides "shortcuts" that allow users to
quickly select common dates. The items in this menu are controlled through
the `shortcuts` prop: `true` to show presets, `false` to hide (default), or an
array of `IDatePickerShortcut` objects to define custom shortcuts.

The **preset shortcuts** can be seen in the example above. They are as follows:

- Today
- Yesterday
- 1 week ago
- 1 month ago
- 3 months ago
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a pretty long list of presets. let's remove this one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hesitant to remove 3 months ago as that is a default that users are already used to in our existing platform where we will be consuming this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove 6 months ago instead

- 1 year ago

**Custom shortcuts** use the following interface:

@interface IDatePickerShortcut

@## Props

Use the `onChange` prop to listen for changes to the set date range. You can
control the selected date range by setting the `value` prop, or use the
component in uncontrolled mode and specify an initial date range by setting
`defaultValue`.

@## Modifiers

You can use the `modifiers` prop to conditionally apply styles to days.
Expand Down
3 changes: 2 additions & 1 deletion packages/datetime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export { DatePicker, IDatePickerProps } from "./datePicker";
export { IDatePickerModifiers } from "./datePickerCore";
export { DateTimePicker, IDateTimePickerProps } from "./dateTimePicker";
export { DateRangeInput, IDateRangeInputProps } from "./dateRangeInput";
export { DateRangePicker, IDateRangePickerProps, IDateRangeShortcut } from "./dateRangePicker";
export { DateRangePicker, IDateRangePickerProps } from "./dateRangePicker";
export { ITimePickerProps, TimePicker, TimePrecision } from "./timePicker";
export { IDatePickerShortcut, IDateRangeShortcut } from "./shortcuts";
Loading