Skip to content

Commit

Permalink
[datetime2] feat: DateRangePicker3 using react-day-picker v8 (#6375)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored Sep 18, 2023
1 parent ac471f3 commit 7502803
Show file tree
Hide file tree
Showing 54 changed files with 3,888 additions and 310 deletions.
7 changes: 5 additions & 2 deletions packages/datetime/src/common/dateRangeSelectionStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface DateRangeSelectionState {
/**
* The boundary that would be modified by clicking the provided `day`.
*/
boundary?: Boundary;
boundary: Boundary;

/**
* The date range that would be selected after clicking the provided `day`.
Expand Down Expand Up @@ -124,11 +124,13 @@ export class DateRangeSelectionStrategy {
const [start, end] = selectedRange;

let nextDateRange: DateRange;
let nextBoundary: Boundary = Boundary.START;

if (start == null && end == null) {
nextDateRange = [day, null];
} else if (start != null && end == null) {
nextDateRange = this.createRange(day, start, allowSingleDayRange);
nextBoundary = Boundary.END;
} else if (start == null && end != null) {
nextDateRange = this.createRange(day, end, allowSingleDayRange);
} else {
Expand All @@ -140,12 +142,13 @@ export class DateRangeSelectionStrategy {
nextDateRange = [null, end];
} else if (isEnd) {
nextDateRange = [start, null];
nextBoundary = Boundary.END;
} else {
nextDateRange = [day, null];
}
}

return { dateRange: nextDateRange };
return { dateRange: nextDateRange, boundary: nextBoundary };
}

private static getOtherBoundary(boundary: Boundary) {
Expand Down
4 changes: 3 additions & 1 deletion packages/datetime/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

import * as Classes from "./classes";
import * as DateUtils from "./dateUtils";
import * as Errors from "./errors";
import * as TimezoneNameUtils from "./timezoneNameUtils";
import type { TimezoneWithNames } from "./timezoneTypes";
import * as TimezoneUtils from "./timezoneUtils";

export { Classes, DateUtils, TimezoneUtils, TimezoneWithNames };
export { Classes, DateUtils, Errors, TimezoneNameUtils, TimezoneUtils, TimezoneWithNames };

export { DatePickerBaseProps, DatePickerModifiers } from "./datePickerBaseProps";
export { DateFormatProps } from "./dateFormatProps";
Expand Down
2 changes: 1 addition & 1 deletion packages/datetime/src/common/monthAndYear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { getDateNextMonth, getDatePreviousMonth } from "./dateUtils";

export class MonthAndYear {
public static fromDate(date: Date) {
public static fromDate(date: Date | null) {
return date == null ? undefined : new MonthAndYear(date.getMonth(), date.getFullYear());
}

Expand Down
2 changes: 1 addition & 1 deletion packages/datetime/src/common/timezoneUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { getCurrentTimezone } from "./getTimezone";
import { TimePrecision } from "./timePickerProps";
import { UTC_TIME } from "./timezoneItems";

export { getCurrentTimezone };
export { getCurrentTimezone, UTC_TIME };

const NO_TIME_PRECISION = "date";

Expand Down
4 changes: 2 additions & 2 deletions packages/datetime/src/components/date-picker/datePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AbstractPureComponent, Button, DISPLAYNAME_PREFIX, Divider, Props } fro

import { Classes, DatePickerBaseProps, DateUtils } from "../../common";
import * as Errors from "../../common/errors";
import { DatePickerShortcut, DateRangeShortcut, Shortcuts } from "../shortcuts/shortcuts";
import { DatePickerShortcut, DatePickerShortcutMenu, DateRangeShortcut } from "../shortcuts/shortcuts";
import { TimePicker } from "../time-picker/timePicker";
import { DatePickerCaption } from "./datePickerCaption";
import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore";
Expand Down Expand Up @@ -325,7 +325,7 @@ export class DatePicker extends AbstractPureComponent<DatePickerProps, DatePicke
dateRange: [shortcut.date, undefined],
}));
return [
<Shortcuts
<DatePickerShortcutMenu
key="shortcuts"
{...{
allowSingleDayRange: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { getFormattedDateString } from "../../common/dateFormatProps";
import { measureTextWidth } from "../../common/utils";
import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore";

/**
Expand All @@ -25,4 +26,5 @@ export const DatePickerUtils = {
getDefaultMaxDate,
getDefaultMinDate,
getFormattedDateString,
measureTextWidth,
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
SELECTED_RANGE_MODIFIER,
} from "../date-picker/datePickerCore";
import { DatePickerNavbar } from "../date-picker/datePickerNavbar";
import { DateRangeShortcut, Shortcuts } from "../shortcuts/shortcuts";
import { DatePickerShortcutMenu, DateRangeShortcut } from "../shortcuts/shortcuts";
import { TimePicker } from "../time-picker/timePicker";

export interface DateRangePickerProps extends DatePickerBaseProps, Props {
Expand Down Expand Up @@ -81,7 +81,11 @@ export interface DateRangePickerProps extends DatePickerBaseProps, Props {
* When triggered from mouseenter, it will pass the date range that would result from next click.
* When triggered from mouseleave, it will pass `undefined`.
*/
onHoverChange?: (hoveredDates: DateRange, hoveredDay: Date, hoveredBoundary: Boundary) => void;
onHoverChange?: (
hoveredDates: DateRange | undefined,
hoveredDay: Date,
hoveredBoundary: Boundary | undefined,
) => void;

/**
* Called when the `shortcuts` props is enabled and the user changes the shortcut.
Expand Down Expand Up @@ -330,7 +334,7 @@ export class DateRangePicker extends AbstractPureComponent<DateRangePickerProps,
const { selectedShortcutIndex } = this.state;
const { allowSingleDayRange, maxDate, minDate, timePrecision } = this.props;
return [
<Shortcuts
<DatePickerShortcutMenu
key="shortcuts"
{...{
allowSingleDayRange,
Expand Down
12 changes: 9 additions & 3 deletions packages/datetime/src/components/shortcuts/shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface DatePickerShortcut extends DateShortcutBase {
date: Date;
}

export interface ShortcutsProps {
export interface DatePickerShortcutMenuProps {
allowSingleDayRange: boolean;
minDate: Date;
maxDate: Date;
Expand All @@ -69,8 +69,14 @@ export interface ShortcutsProps {
useSingleDateShortcuts?: boolean;
}

export class Shortcuts extends React.PureComponent<ShortcutsProps> {
public static defaultProps: Partial<ShortcutsProps> = {
/**
* Menu of {@link DateRangeShortcut} items, typically displayed in the UI to the left of a day picker calendar.
*
* This component may be used for single date pickers as well as range pickers by toggling the
* `useSingleDateShortcuts` option.
*/
export class DatePickerShortcutMenu extends React.PureComponent<DatePickerShortcutMenuProps> {
public static defaultProps: Partial<DatePickerShortcutMenuProps> = {
selectedShortcutIndex: -1,
};

Expand Down
9 changes: 8 additions & 1 deletion packages/datetime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type DatePickerLocaleUtils = typeof LocaleUtils;
export { DatePickerLocaleUtils, DatePickerDayModifiers };

export { DateFormatProps } from "./common/dateFormatProps";
export { DateRangeSelectionStrategy, DateRangeSelectionState } from "./common/dateRangeSelectionStrategy";
export { MonthAndYear } from "./common/monthAndYear";
export { TimePickerProps, TimePrecision } from "./common/timePickerProps";

export { DateInput, DateInputProps } from "./components/date-input/dateInput";
Expand All @@ -32,5 +34,10 @@ export { DatePickerBaseProps, DatePickerModifiers } from "./common/datePickerBas
export { DateRangeInput, DateRangeInputProps } from "./components/date-range-input/dateRangeInput";
export { DateRangePicker, DateRangePickerProps } from "./components/date-range-picker/dateRangePicker";
export { TimePicker } from "./components/time-picker/timePicker";
export { DatePickerShortcut, DateRangeShortcut } from "./components/shortcuts/shortcuts";
export {
DatePickerShortcut,
DatePickerShortcutMenu,
DatePickerShortcutMenuProps,
DateRangeShortcut,
} from "./components/shortcuts/shortcuts";
export { TimezoneSelect, TimezoneSelectProps } from "./components/timezone-select/timezoneSelect";
15 changes: 11 additions & 4 deletions packages/datetime/test/components/datePickerTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { Classes, DatePicker, DatePickerModifiers, DatePickerProps, TimePicker,
import { DateUtils, Months } from "../../src/common";
import * as Errors from "../../src/common/errors";
import { DatePickerState } from "../../src/components/date-picker/datePicker";
import { DatePickerShortcut, Shortcuts } from "../../src/components/shortcuts/shortcuts";
import { DatePickerShortcut, DatePickerShortcutMenu } from "../../src/components/shortcuts/shortcuts";
import { assertDayDisabled, assertDayHidden } from "../common/dayPickerTestUtils";

describe("<DatePicker>", () => {
Expand Down Expand Up @@ -478,16 +478,23 @@ describe("<DatePicker>", () => {
it("all shortcuts are displayed as inactive when none are selected", () => {
const { root } = wrap(<DatePicker shortcuts={true} />);

assert.isFalse(root.find(Shortcuts).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`).exists());
assert.isFalse(
root.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`).exists(),
);
});

it("corresponding shortcut is displayed as active when selected", () => {
const selectedShortcut = 0;
const { root } = wrap(<DatePicker shortcuts={true} selectedShortcutIndex={selectedShortcut} />);

assert.isTrue(root.find(Shortcuts).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`).exists());
assert.isTrue(
root.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`).exists(),
);

assert.lengthOf(root.find(Shortcuts).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`), 1);
assert.lengthOf(
root.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${CoreClasses.ACTIVE}`),
1,
);

assert.isTrue(root.state("selectedShortcutIndex") === selectedShortcut);
});
Expand Down
15 changes: 11 additions & 4 deletions packages/datetime/test/components/dateRangePickerTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import * as Errors from "../../src/common/errors";
import { Months } from "../../src/common/months";
import { DatePickerNavbar } from "../../src/components/date-picker/datePickerNavbar";
import { DateRangePickerState } from "../../src/components/date-range-picker/dateRangePicker";
import { DateRangeShortcut, Shortcuts } from "../../src/components/shortcuts/shortcuts";
import { DatePickerShortcutMenu, DateRangeShortcut } from "../../src/components/shortcuts/shortcuts";
import { assertDayDisabled } from "../common/dayPickerTestUtils";

describe("<DateRangePicker>", () => {
Expand Down Expand Up @@ -930,16 +930,23 @@ describe("<DateRangePicker>", () => {
it("all shortcuts are displayed as inactive when none are selected", () => {
const { wrapper } = render();

assert.isFalse(wrapper.find(Shortcuts).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`).exists());
assert.isFalse(
wrapper.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`).exists(),
);
});

it("corresponding shortcut is displayed as active when selected", () => {
const selectedShortcut = 0;
const { wrapper } = render({ selectedShortcutIndex: selectedShortcut });

assert.isTrue(wrapper.find(Shortcuts).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`).exists());
assert.isTrue(
wrapper.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`).exists(),
);

assert.lengthOf(wrapper.find(Shortcuts).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`), 1);
assert.lengthOf(
wrapper.find(DatePickerShortcutMenu).find(Menu).find(MenuItem).find(`.${Classes.ACTIVE}`),
1,
);

assert.isTrue(wrapper.state("selectedShortcutIndex") === selectedShortcut);
});
Expand Down
3 changes: 1 addition & 2 deletions packages/datetime/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* Copyright 2015 Palantir Technologies, Inc. All rights reserved.
*/

// tslint:disable no-submodule-imports
// tslint:disable-next-line no-submodule-imports
import "@blueprintjs/core/lib/css/blueprint.css";
// tslint:enable no-submodule-imports
import "../lib/css/blueprint-datetime.css";
import "./test-debugging-styles.scss";

Expand Down
18 changes: 18 additions & 0 deletions packages/datetime/test/isotest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

require("@blueprintjs/test-commons/bootstrap");
const { generateIsomorphicTests } = require("@blueprintjs/test-commons");
const { add } = require("date-fns");

const DateTime = require("../lib/cjs");

Expand All @@ -25,8 +26,25 @@ describe("DateTime isomorphic rendering", () => {
placeholder: "enter date",
};

const today = new Date();
const maxDate = add(today, { days: 1 });
const minDate = add(today, { years: -4 });

generateIsomorphicTests(DateTime, {
DateInput: { props: formatProps },
DateRangeInput: { props: formatProps },
DatePickerShortcutMenu: {
className: false,
props: {
allowSingleDayRange: true,
maxDate,
minDate,
onShortcutClick: () => {
/* no-op */
},
shortcuts: true,
timePrecision: "second",
},
},
});
});
4 changes: 4 additions & 0 deletions packages/datetime2/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ module.exports = async function (config) {
config.set(
createKarmaConfig({
dirname: __dirname,
coverageExcludes: [
// we stub this out in tests because it relies on dynamic imports
"src/common/dateFnsLocaleUtils.ts",
],
coverageOverrides: {
"src/*": {
lines: 75,
Expand Down
2 changes: 2 additions & 0 deletions packages/datetime2/src/blueprint-datetime2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
// Licensed under the Apache License, Version 2.0.

@import "common/react-day-picker-overrides";
@import "components/date-picker3/date-picker3-caption";
@import "components/date-picker3/date-picker3";
@import "components/date-range-picker3/date-range-picker3";
52 changes: 49 additions & 3 deletions packages/datetime2/src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,68 @@
* limitations under the License.
*/

import classNames from "classnames";
import type { StyledElement } from "react-day-picker";

import { Classes as CoreClasses } from "@blueprintjs/core";
import { Classes as DatetimeClasses } from "@blueprintjs/datetime";

const RDP_DAY = "rdp-day";
const RDP = "rdp";
const RDP_DAY = `${RDP}-day`;
const DATEPICKER_NAV_BUTTON = `${DatetimeClasses.DATEPICKER}-nav-button`;

export const Classes = {
...DatetimeClasses,
const ReactDayPickerClasses = {
RDP,
RDP_CAPTION_LABEL: `${RDP}-caption_label`,
RDP_DAY,
RDP_VHIDDEN: `${RDP}-vhidden`,
};

const DatePicker3Classes = {
// these classes need the "3" suffix because they overlap with DatePicker v1 / react-day-picker v7 classes
DATEPICKER3_DAY: RDP_DAY,
DATEPICKER3_DAY_DISABLED: `${RDP_DAY}_disabled`,
DATEPICKER3_DAY_IS_TODAY: `${RDP_DAY}_today`,
DATEPICKER3_DAY_OUTSIDE: `${RDP_DAY}_outside`,
DATEPICKER3_DAY_SELECTED: `${RDP_DAY}_selected`,
// these classes intentionally left without "3" suffix because they do not overlap with DatePicker v1, and this way we don't need to migrate them later, which reduces code churn
DATEPICKER_DROPDOWN_CONTAINER: `${DatetimeClasses.DATEPICKER}-dropdown-container`,
DATEPICKER_DROPDOWN_MONTH: `${RDP}-dropdown_month`,
DATEPICKER_DROPDOWN_YEAR: `${RDP}-dropdown_year`,
DATEPICKER_HIGHLIGHT_CURRENT_DAY: `${DatetimeClasses.DATEPICKER}-highlight-current-day`,
DATEPICKER_NAV_BUTTON,
DATEPICKER_NAV_BUTTON_HIDDEN: `${DATEPICKER_NAV_BUTTON}-hidden`,
DATEPICKER_NAV_BUTTON_NEXT: `${DATEPICKER_NAV_BUTTON}-next`,
DATEPICKER_NAV_BUTTON_PREVIOUS: `${DATEPICKER_NAV_BUTTON}-previous`,
};

const DateRangePicker3Classes = {
DATERANGEPICKER3_DAY_HOVERED_RANGE: `${RDP_DAY}_hovered`,
DATERANGEPICKER3_DAY_HOVERED_RANGE_END: `${RDP_DAY}_hovered_end`,
DATERANGEPICKER3_DAY_HOVERED_RANGE_START: `${RDP_DAY}_hovered_start`,
DATERANGEPICKER3_DAY_RANGE_END: `${RDP_DAY}_range_end`,
DATERANGEPICKER3_DAY_RANGE_MIDDLE: `${RDP_DAY}_range_middle`,
DATERANGEPICKER3_DAY_RANGE_START: `${RDP_DAY}_range_start`,
DATERANGEPICKER_REVERSE_MONTH_AND_YEAR: `${DatetimeClasses.DATERANGEPICKER}-reverse-month-and-year`,
};

export const Classes = {
...DatetimeClasses,
...DatePicker3Classes,
...DateRangePicker3Classes,
...ReactDayPickerClasses,
};

/**
* Class name overrides for components rendered by react-day-picker. These are helpful so that @blueprintjs/datetime2
* can have more predictable and standard DOM selectors in custom styles & tests.
*/
export const dayPickerClassNameOverrides: Partial<StyledElement<string>> = {
/* eslint-disable camelcase */
button: classNames(CoreClasses.BUTTON, CoreClasses.MINIMAL),
button_reset: undefined,
nav_button: Classes.DATEPICKER_NAV_BUTTON,
nav_button_next: Classes.DATEPICKER_NAV_BUTTON_NEXT,
nav_button_previous: Classes.DATEPICKER_NAV_BUTTON_PREVIOUS,
/* eslint-enable camelcase */
};
Loading

1 comment on commit 7502803

@adidahiya
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[datetime2] feat: DateRangePicker3 using react-day-picker v8 (#6375)

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.