From 415e64c9a7e774155d3697fdaedb71abe64f14d8 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Fri, 10 Feb 2023 10:42:23 -0500 Subject: [PATCH 01/26] WIP create DatePicker2 with react-day-picker v8 --- packages/datetime2/package.json | 2 + packages/datetime2/src/common/dateUtils.ts | 19 + .../components/date-picker2/datePicker2.tsx | 474 ++++++++++++++++++ packages/datetime2/src/tsconfig.json | 1 + yarn.lock | 5 + 5 files changed, 501 insertions(+) create mode 100644 packages/datetime2/src/components/date-picker2/datePicker2.tsx diff --git a/packages/datetime2/package.json b/packages/datetime2/package.json index aaff821ea9a..a5335d5b0e8 100644 --- a/packages/datetime2/package.json +++ b/packages/datetime2/package.json @@ -43,6 +43,7 @@ "date-fns": "^2.28.0", "date-fns-tz": "^1.3.7", "lodash": "^4.17.21", + "react-day-picker": "7.4.9 || ^8.5.1", "tslib": "~2.3.1" }, "peerDependencies": { @@ -66,6 +67,7 @@ "mocha": "^10.1.0", "npm-run-all": "^4.1.5", "react": "^16.14.0", + "react-day-picker": "^8.5.1", "react-dom": "^16.14.0", "react-test-renderer": "^16.14.0", "typescript": "~4.6.2", diff --git a/packages/datetime2/src/common/dateUtils.ts b/packages/datetime2/src/common/dateUtils.ts index 77e20cd8031..722ed20d17a 100644 --- a/packages/datetime2/src/common/dateUtils.ts +++ b/packages/datetime2/src/common/dateUtils.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +/** + * @fileoverview Some of these functions are similar to the implementations found in DateUtils from + * "@blueprintjs/datetime", but these have some improvements to use the "date-fns" library and compile with + * TypeScript strict null checks. + */ + import { isSameDay, isWithinInterval } from "date-fns"; import { DateRange, isNonNullRange } from "./dateRange"; @@ -26,6 +32,19 @@ export function isDateValid(date: Date | false | null): date is Date { return date instanceof Date && !isNaN(date.valueOf()); } +export function areSameMonth(date1: Date | null, date2: Date | null) { + return ( + date1 != null && + date2 != null && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ); +} + +export function areSameDay(date1: Date | null, date2: Date | null) { + return areSameMonth(date1, date2) && date1?.getDate() === date2?.getDate(); +} + export function isSameTime(d1: Date | null, d2: Date | null) { // N.B. do not use date-fns helper fns here, since we don't want to return false when the month/day/year is different return ( diff --git a/packages/datetime2/src/components/date-picker2/datePicker2.tsx b/packages/datetime2/src/components/date-picker2/datePicker2.tsx new file mode 100644 index 00000000000..f78c93c81ab --- /dev/null +++ b/packages/datetime2/src/components/date-picker2/datePicker2.tsx @@ -0,0 +1,474 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import type { Locale } from "date-fns"; +import * as React from "react"; +import { CaptionLabelProps, DayModifiers, DayPicker, NavigationContextValue } from "react-day-picker"; + +import { AbstractPureComponent2, Button, DISPLAYNAME_PREFIX, Divider, Props } from "@blueprintjs/core"; +import { + Classes, + DatePickerProps, + DatePickerUtils, + DateRangeShortcut, + DateUtils as DateUtilsV1, + TimePicker, +} from "@blueprintjs/datetime"; +// tslint:disable no-submodule-imports +import * as Errors from "@blueprintjs/datetime/lib/esm/common/errors"; +import { DatePickerCaption } from "@blueprintjs/datetime/lib/esm/datePickerCaption"; +import { DatePickerNavbar } from "@blueprintjs/datetime/lib/esm/datePickerNavbar"; +import { Shortcuts } from "@blueprintjs/datetime/lib/esm/shortcuts"; +// tslint:enable no-submodule-imports + +import { DateRange, DateUtils } from "../../common"; + +/** Props shared between DatePicker v1 and v2 */ +type DatePickerSharedProps = Omit; + +export interface DatePicker2Props extends DatePickerSharedProps, Props { + /** + * Initial day the calendar will display as selected. + * This should not be set if `value` is set. + */ + defaultValue?: Date; + + /** + * Called when the user selects a day. + * If being used in an uncontrolled manner, `selectedDate` will be `null` if the user clicks the currently selected + * day. If being used in a controlled manner, `selectedDate` will contain the day clicked no matter what. + * `isUserChange` is true if the user selected a day, and false if the date was automatically changed + * by the user navigating to a new month or year rather than explicitly clicking on a date in the calendar. + */ + onChange?: (selectedDate: Date | null, isUserChange: boolean) => void; + + /** + * The currently selected day. If this prop is provided, the component acts in a controlled manner. + */ + value?: Date | null; +} + +interface DatePicker2State { + displayMonth: number; + displayYear: number; + locale: Locale | undefined; + selectedDay: number | null; + value: Date | null; + selectedShortcutIndex?: number; +} + +/** + * Date picker (v2) component. + * + * @see https://blueprintjs.com/docs/#datetime2/date-picker2 + */ +export class DatePicker extends AbstractPureComponent2 { + public static defaultProps: DatePicker2Props = { + canClearSelection: true, + clearButtonText: "Clear", + dayPickerProps: {}, + highlightCurrentDay: false, + maxDate: DatePickerUtils.getDefaultMaxDate(), + minDate: DatePickerUtils.getDefaultMinDate(), + reverseMonthAndYearMenus: false, + shortcuts: false, + showActionsBar: false, + todayButtonText: "Today", + }; + + public static displayName = `${DISPLAYNAME_PREFIX}.DatePicker`; + + private ignoreNextMonthChange = false; + + public constructor(props: DatePicker2Props) { + super(props); + const value = getInitialValue(props); + const initialMonth = getInitialMonth(props, value); + this.state = { + displayMonth: initialMonth.getMonth(), + displayYear: initialMonth.getFullYear(), + locale: undefined, + selectedDay: value == null ? null : value.getDate(), + selectedShortcutIndex: + this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1, + value, + }; + } + + public render() { + const { className, dayPickerProps, footerElement, localeUtils, maxDate, minDate, showActionsBar } = this.props; + const { displayMonth, displayYear, locale } = this.state; + + return ( +
+ {this.maybeRenderShortcuts()} +
+ + {this.maybeRenderTimePicker()} + {showActionsBar && this.renderOptionsBar()} + {footerElement} +
+
+ ); + } + + public async componentDidMount() { + await this.loadLocale(this.props.locale); + } + + public async componentDidUpdate(prevProps: DatePicker2Props, prevState: DatePicker2State) { + super.componentDidUpdate(prevProps, prevState); + + const { value } = this.props; + if (value === prevProps.value) { + // no action needed + return; + } else if (value == null) { + // clear the value + this.setState({ value: null }); + } else { + this.setState({ + displayMonth: value.getMonth(), + displayYear: value.getFullYear(), + selectedDay: value.getDate(), + value, + }); + } + + if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) { + this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex }); + } + + if (this.props.locale !== prevProps.locale) { + await this.loadLocale(this.props.locale); + } + } + + protected validateProps(props: DatePickerProps) { + const { defaultValue, initialMonth, maxDate, minDate, value } = props; + if (defaultValue != null && !DateUtils.isDayInRange(defaultValue, [minDate!, maxDate!])) { + console.error(Errors.DATEPICKER_DEFAULT_VALUE_INVALID); + } + + if (initialMonth != null && !DateUtilsV1.isMonthInRange(initialMonth, [minDate!, maxDate!])) { + console.error(Errors.DATEPICKER_INITIAL_MONTH_INVALID); + } + + if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.areSameDay(maxDate, minDate)) { + console.error(Errors.DATEPICKER_MAX_DATE_INVALID); + } + + if (value != null && !DateUtils.isDayInRange(value, [minDate!, maxDate!])) { + console.error(Errors.DATEPICKER_VALUE_INVALID); + } + } + + private async loadLocale(localeCode: string | undefined) { + if (localeCode === undefined) { + return; + } else if (this.state.locale?.code === localeCode) { + return; + } + const locale = await import(`date-fns/locale/${localeCode}`); + this.setState({ locale }); + } + + private shouldHighlightCurrentDay = (date: Date) => { + const { highlightCurrentDay } = this.props; + + return highlightCurrentDay === true && DateUtilsV1.isToday(date); + }; + + private getDatePickerModifiers = () => { + const { modifiers } = this.props; + + return { + isToday: this.shouldHighlightCurrentDay, + ...modifiers, + } as DayModifiers; + }; + + private renderDay = (day: Date) => { + const date = day.getDate(); + + return
{date}
; + }; + + private disabledDays = (day: Date) => !DateUtils.isDayInRange(day, [this.props.minDate!, this.props.maxDate!]); + + private getDisabledDaysModifier = () => { + const { dayPickerProps } = this.props; + + return Array.isArray(dayPickerProps?.disabledDays) + ? [this.disabledDays, ...dayPickerProps!.disabledDays] + : [this.disabledDays, dayPickerProps?.disabledDays]; + }; + + // TODO(@adidahiya) + private renderCaption = (props: CaptionElementProps) => ( + + ); + + // TODO(@adidahiya) + private renderNavbar = (props: NavbarElementProps) => ( + + ); + + private renderOptionsBar() { + const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props; + const todayEnabled = isTodayEnabled(minDate!, maxDate!); + return [ + , +
+
, + ]; + } + + private maybeRenderTimePicker() { + const { timePrecision, timePickerProps, minDate, maxDate } = this.props; + if (timePrecision == null && timePickerProps === undefined) { + return null; + } + const applyMin = DateUtils.areSameDay(this.state.value, minDate!); + const applyMax = DateUtils.areSameDay(this.state.value, maxDate!); + return ( +
+ +
+ ); + } + + 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: DateRangeShortcut[] | true = + shortcuts === true + ? true + : shortcuts.map(shortcut => ({ + ...shortcut, + dateRange: [shortcut.date, null], + })); + return [ + , + , + ]; + } + + private handleDayClick = (day: Date, modifiers: DayModifiers, e: React.MouseEvent) => { + 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 : DateUtilsV1.getDateTime(day, this.state.value); + this.updateValue(newValue, true); + }; + + private handleShortcutClick = (shortcut: DateRangeShortcut, selectedShortcutIndex: number) => { + const { onShortcutChange, selectedShortcutIndex: currentShortcutIndex } = this.props; + const { dateRange, includeTime } = shortcut; + + const newDate = dateRange[0]; + const newValue = includeTime ? newDate : DateUtilsV1.getDateTime(newDate, this.state.value); + + if (newDate == null) { + return; + } + + this.updateDay(newDate); + this.updateValue(newValue, true); + + if (currentShortcutIndex === undefined) { + this.setState({ selectedShortcutIndex }); + } + + const datePickerShortcut = { ...shortcut, date: newDate }; + onShortcutChange?.(datePickerShortcut, selectedShortcutIndex); + }; + + private updateDay = (day: Date) => { + if (this.props.value === undefined) { + // set now if uncontrolled, otherwise they'll be updated in `componentDidUpdate` + this.setState({ + displayMonth: day.getMonth(), + displayYear: day.getFullYear(), + selectedDay: day.getDate(), + }); + } + if (this.state.value != null && this.state.value.getMonth() !== day.getMonth()) { + this.ignoreNextMonthChange = true; + } + }; + + private computeValidDateInSpecifiedMonthYear(displayYear: number, displayMonth: number): Date { + const { minDate, maxDate } = this.props; + const { selectedDay } = this.state; + // month is 0-based, date is 1-based. date 0 is last day of previous month. + const maxDaysInMonth = new Date(displayYear, displayMonth + 1, 0).getDate(); + const displayDate = selectedDay == null ? 1 : Math.min(selectedDay, maxDaysInMonth); + + // 12:00 matches the underlying react-day-picker timestamp behavior + const value = DateUtilsV1.getDateTime(new Date(displayYear, displayMonth, displayDate, 12), this.state.value); + // clamp between min and max dates + if (value < minDate!) { + return minDate!; + } else if (value > maxDate!) { + return maxDate!; + } + return value; + } + + private handleClearClick = () => this.updateValue(null, true); + + private handleMonthChange = (newDate: Date) => { + const date = this.computeValidDateInSpecifiedMonthYear(newDate.getFullYear(), newDate.getMonth()); + this.setState({ displayMonth: date.getMonth(), displayYear: date.getFullYear() }); + if (this.state.value !== null) { + // if handleDayClick just got run (so this flag is set), then the + // user selected a date in a new month, so don't invoke onChange a + // second time + this.updateValue(date, false, this.ignoreNextMonthChange); + this.ignoreNextMonthChange = false; + } + this.props.dayPickerProps?.onMonthChange?.(date); + }; + + private handleTodayClick = () => { + const value = new Date(); + const displayMonth = value.getMonth(); + const displayYear = value.getFullYear(); + const selectedDay = value.getDate(); + this.setState({ displayMonth, displayYear, selectedDay }); + this.updateValue(value, true); + }; + + private handleTimeChange = (time: Date) => { + this.props.timePickerProps?.onChange?.(time); + const { value } = this.state; + const newValue = DateUtilsV1.getDateTime(value != null ? value : new Date(), time); + this.updateValue(newValue, true); + }; + + /** + * Update `value` by invoking `onChange` (always) and setting state (if uncontrolled). + */ + private updateValue(value: Date | null, isUserChange: boolean, skipOnChange = false) { + if (!skipOnChange) { + this.props.onChange?.(value, isUserChange); + } + if (this.props.value === undefined) { + this.setState({ value }); + } + } +} + +function getInitialValue(props: DatePicker2Props): Date | null { + // !== because `null` is a valid value (no date) + if (props.value !== undefined) { + return props.value; + } + if (props.defaultValue !== undefined) { + return props.defaultValue; + } + return null; +} + +function getInitialMonth(props: DatePicker2Props, value: Date | null): Date { + const rangeFromProps: DateRange = [props.minDate ?? null, props.maxDate ?? null]; + const today = new Date(); + // != because we must have a real `Date` to begin the calendar on. + if (props.initialMonth != null) { + return props.initialMonth; + } else if (value != null) { + return value; + } else if (DateUtils.isDayInRange(today, rangeFromProps)) { + return today; + } else { + return DateUtilsV1.getDateBetween(rangeFromProps); + } +} + +function isTodayEnabled(minDate: Date, maxDate: Date): boolean { + const today = new Date(); + return DateUtils.isDayInRange(today, [minDate, maxDate]); +} diff --git a/packages/datetime2/src/tsconfig.json b/packages/datetime2/src/tsconfig.json index b4fa3674bf5..0a4474803ec 100644 --- a/packages/datetime2/src/tsconfig.json +++ b/packages/datetime2/src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../../config/tsconfig.base", "compilerOptions": { + "module": "es2020", "outDir": "../lib/esm" } } diff --git a/yarn.lock b/yarn.lock index b2b4bafa650..d0cb4dd6836 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9947,6 +9947,11 @@ react-day-picker@7.4.9: dependencies: prop-types "^15.6.2" +"react-day-picker@7.4.9 || ^8.5.1": + version "8.5.1" + resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.5.1.tgz#c0ab712f33753b70cd5e2aab85665bb4e947c69c" + integrity sha512-ee3+PUGAEiLHaS3als6LYO7NQW2MPw00cvDSk5oNPLk4axIE5JhXRbIr//r9eX5Zsg1S1NFQpJoeJJ5LtJ0/SA== + react-dom@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" From f80d86fceff7c412185c11d94a3aa729dd64e12d Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Fri, 10 Feb 2023 11:01:41 -0500 Subject: [PATCH 02/26] suppress some compiler errors --- .../datetime2/src/components/date-picker2/datePicker2.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/datetime2/src/components/date-picker2/datePicker2.tsx b/packages/datetime2/src/components/date-picker2/datePicker2.tsx index f78c93c81ab..80c54900ed8 100644 --- a/packages/datetime2/src/components/date-picker2/datePicker2.tsx +++ b/packages/datetime2/src/components/date-picker2/datePicker2.tsx @@ -123,6 +123,7 @@ export class DatePicker extends AbstractPureComponent2 ( + private renderCaption = (props: any /* CaptionElementProps */) => ( ( + private renderNavbar = (props: any /* NavbarElementProps */) => ( ); @@ -331,6 +332,7 @@ export class DatePicker extends AbstractPureComponent2 { + // @ts-ignore -- HACKHACK(@adidahiya): multiple versions of rdp types in this monorepo create a type conflict here this.props.dayPickerProps?.onDayClick?.(day, modifiers, e); if (modifiers.disabled) { return; From c054c92bee7522cf2d5187a8554737150d1c2468 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Wed, 30 Aug 2023 15:44:00 -0400 Subject: [PATCH 03/26] Add docs example, fixup basic styling --- .../src/_react-day-picker-overrides.scss | 22 ++ .../datetime2/src/blueprint-datetime2.scss | 3 +- .../date-picker2/_date-picker2.scss | 271 ++++++++++++++++++ .../components/date-picker2/date-picker2.md | 33 +++ .../components/date-picker2/datePicker2.tsx | 42 +-- packages/datetime2/src/index.md | 50 ++++ packages/datetime2/src/index.ts | 7 +- packages/docs-app/src/_nav.md | 1 + packages/docs-app/src/blueprint.md | 1 + .../datetime2-examples/datePicker2Example.tsx | 143 +++++++++ .../src/examples/datetime2-examples/index.ts | 17 ++ packages/docs-app/src/index.scss | 1 + packages/docs-app/src/tags/reactExamples.ts | 2 + 13 files changed, 562 insertions(+), 31 deletions(-) create mode 100644 packages/datetime2/src/_react-day-picker-overrides.scss create mode 100644 packages/datetime2/src/components/date-picker2/_date-picker2.scss create mode 100644 packages/datetime2/src/components/date-picker2/date-picker2.md create mode 100644 packages/datetime2/src/index.md create mode 100644 packages/docs-app/src/examples/datetime2-examples/datePicker2Example.tsx create mode 100644 packages/docs-app/src/examples/datetime2-examples/index.ts diff --git a/packages/datetime2/src/_react-day-picker-overrides.scss b/packages/datetime2/src/_react-day-picker-overrides.scss new file mode 100644 index 00000000000..4fbc79bffe6 --- /dev/null +++ b/packages/datetime2/src/_react-day-picker-overrides.scss @@ -0,0 +1,22 @@ +// Copyright 2023 Palantir Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. + +@import "@blueprintjs/core/lib/scss/variables"; + +// omit file extension, otherwise this will emit a `@import url()` rule +@import "react-day-picker/dist/style"; + +.#{$ns}-datepicker-content .rdp { + --rdp-cell-size: #{$pt-grid-size * 3}; + --rdp-accent-color: #{$blue3}; + --rdp-background-color: #{$white}; + /* Switch to dark colors for dark themes */ + --rdp-accent-color-dark: #{$blue2}; + --rdp-background-color-dark: #{$dark-gray3}; + /* Outline border for focused elements */ + --rdp-outline: 2px solid var(--rdp-accent-color); + /* Outline border for focused and selected elements */ + --rdp-outline-selected: 2px solid rgba(0, 0, 0, 0.75); + + margin: 0; +} diff --git a/packages/datetime2/src/blueprint-datetime2.scss b/packages/datetime2/src/blueprint-datetime2.scss index 7cb8a96aba6..74c0ddd5d56 100644 --- a/packages/datetime2/src/blueprint-datetime2.scss +++ b/packages/datetime2/src/blueprint-datetime2.scss @@ -1,4 +1,5 @@ // Copyright 2022 Palantir Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. -// This file intentionaly left blank +@import "react-day-picker-overrides"; +@import "components/date-picker2/date-picker2"; diff --git a/packages/datetime2/src/components/date-picker2/_date-picker2.scss b/packages/datetime2/src/components/date-picker2/_date-picker2.scss new file mode 100644 index 00000000000..2883da4ddb4 --- /dev/null +++ b/packages/datetime2/src/components/date-picker2/_date-picker2.scss @@ -0,0 +1,271 @@ +// Copyright 2023 Palantir Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. + +@import "@blueprintjs/colors/lib/scss/colors"; +@import "@blueprintjs/core/src/components/popover/common"; +@import "@blueprintjs/datetime/src/common"; + +$cell-size: $pt-grid-size * 3 !default; +$header-height: $pt-grid-size * 4 !default; +$header-margin: ($header-height - $pt-input-height) * 0.5 !default; + +@mixin calendar-day() { + display: table-cell; + height: $datepicker-day-size; + line-height: 1; + text-align: center; + vertical-align: middle; + width: $datepicker-day-size; +} + +.#{$ns}-datepicker { + .rdp { + display: inline-block; + min-width: $datepicker-min-width; + position: relative; + vertical-align: top; + + &:focus { + outline: none; + } + } + + .#{$ns}-datepicker-day-wrapper { + border-radius: $pt-border-radius; + padding: 7px; + } + + .rdp-month { + border-collapse: collapse; + border-spacing: 0; + display: inline-table; + margin: 0 $datepicker-padding; + user-select: none; + + // create space between months (selector matches all but first month) + & + & { + margin-left: $pt-grid-size; + } + } + + .rdp-caption { + display: table-caption; + } + + .rdp-weekdays { + display: table-header-group; + } + + .rdp-head_cell { + // @include calendar-day(); + font-size: inherit; + font-weight: 600; + padding-top: $datepicker-padding; + text-decoration: none; + } + + .rdp-tbody { + display: table-row-group; + } + + .rdp-row { + display: table-row; + } + + // TODO(@adahiya): verify that this is the correct selector + .rdp-week-number { + @include calendar-day(); + color: $pt-text-color-disabled; + font-size: $pt-font-size; + } + + .rdp-day { + // @include calendar-day(); + border-radius: $pt-border-radius; + cursor: pointer; + + // spelling out full name so these are equal specificity to pseudo-classes (.DayPicker-Day:hover) + &.rdp-day_outside { + color: $pt-text-color-disabled; + } + + &.rdp-day_today { + .#{$ns}-datepicker-day-wrapper { + border: 1px solid $pt-divider-black; + } + } + + // need some extra specificity here to override rdp default styles + &:not([disabled]):not(.rdp-day_selected) { + &:hover, + &:focus { + background: $datepicker-day-background-color-hover; + color: $pt-text-color; + } + + &:active { + background: $datepicker-day-background-color-active; + } + } + + // TODO(@adahiya): verify that this is the correct selector + &.rdp-day_selected { + background-color: $blue3; + border-radius: $pt-border-radius; + color: $white; + + &:hover { + background-color: $blue2; + } + + &:active { + background-color: $blue1; + } + } + + // putting it last so it overrides equally specific selectors above. + &.rdp-day_disabled { + background: none; + color: $pt-text-color-disabled; + cursor: not-allowed; + } + } +} + +.#{$ns}-datepicker-navbar { + align-items: center; + display: flex; + height: $pt-button-height; + left: 0; + position: absolute; + right: 0; + top: 0; + + > .rdp-nav_button_previous { + margin-right: auto; + } + + > .rdp-nav_button_next { + margin-left: auto; + } +} + +.#{$ns}-datepicker-caption { + @include pt-flex-container(row, $fill: ":first-child"); + justify-content: space-between; + margin: 0 ($pt-button-height - $datepicker-padding) $datepicker-padding; + + // override HTMLSelect styles for a narrower appearance + // N.B. these styles are important to achieve accurate measurements to position the dropdown arrows in + .#{$ns}-html-select select { + font-weight: 600; + padding-left: $datepicker-padding; + padding-right: $pt-icon-size-standard; + + + .#{$ns}-icon { + right: 2px; + } + } + + + .#{$ns}-divider { + margin: 0; + } +} + +.#{$ns}-datepicker-month-select { + flex-shrink: 1; +} + +.#{$ns}-datepicker-year-select { + flex-shrink: 1; + min-width: 60px; +} + +.#{$ns}-datepicker-caption-measure { + font-weight: 600; + padding-left: $datepicker-padding; +} + +.#{$ns}-datepicker-content { + align-items: center; + display: flex; + flex-direction: column; + gap: 5px; + + > .#{$ns}-divider { + margin: 0; // margin is already applied via flex gap + width: calc(100% - #{$datepicker-padding * 2}); + } +} + +.#{$ns}-datepicker-footer { + display: flex; + justify-content: space-between; + width: 100%; +} + +.#{$ns}-dark .#{$ns}-datepicker { + background: $dark-datepicker-background-color; + + // TODO(@adahiya): verify that this is the correct selector + .rdp-week-number { + color: $pt-dark-text-color-disabled; + } + + .rdp-day { + &.rdp-day_outside { + color: $pt-dark-text-color-disabled; + } + + &.rdp-day_today { + .#{$ns}-datepicker-day-wrapper { + border: 1px solid $pt-dark-divider-white; + } + } + + // need some extra specificity here to override rdp default styles + &:not([disabled]):not(.rdp-day_selected) { + &:hover, + &:focus { + background: $dark-datepicker-day-background-color-hover; + color: $white; + } + + &:active { + background: $dark-datepicker-day-background-color-active; + } + } + + &.rdp-day_selected { + background-color: $blue3; + + &:hover { + background-color: $blue2; + } + + &:active { + background-color: $blue1; + } + } + + &.rdp-day_disabled { + background: none; + color: $pt-dark-text-color-disabled; + } + } + + .#{$ns}-datepicker-footer { + border-top-color: $pt-dark-divider-black; + } +} + +.#{$ns}-datepicker-timepicker-wrapper { + align-items: center; + display: flex; + flex-direction: column; + + .#{$ns}-timepicker-arrow-row:empty + .#{$ns}-timepicker-input-row { + // when timepicker arrows are not displayed in the datepicker, we need a bit of extra margin + margin: $datepicker-padding 0; + } +} diff --git a/packages/datetime2/src/components/date-picker2/date-picker2.md b/packages/datetime2/src/components/date-picker2/date-picker2.md new file mode 100644 index 00000000000..cd86051d5ba --- /dev/null +++ b/packages/datetime2/src/components/date-picker2/date-picker2.md @@ -0,0 +1,33 @@ +--- +tag: new +--- + +@# DatePicker2 + +
+
+ +Migrating from [DatePicker](#datetime/datepicker)? + +
+ +DatePicker2 is a replacement for DatePicker and will replace it in Blueprint v6. +You are encouraged to use this new API now to ease the transition to the next major version of Blueprint. +See the [react-day-picker v8 migration guide](https://github.com/palantir/blueprint/wiki/react-day-picker-8-migration) +on the wiki. + +
+ +__DatePicker2__ has the same functionality as [DatePicker](#datetime/datepicker) but uses +[react-day-picker v8](https://react-day-picker.js.org/) instead of [v7](https://react-day-picker-v7.netlify.app/) +to render its calendar. + +@reactExample DatePicker2Example + +@## Usage + +Please refer to [DatePicker usage documentation](#datetime/datepicker.usage). + +@## Props interface + +@interface DatePicker2Props diff --git a/packages/datetime2/src/components/date-picker2/datePicker2.tsx b/packages/datetime2/src/components/date-picker2/datePicker2.tsx index 845949e7b2e..3d39b9b58ae 100644 --- a/packages/datetime2/src/components/date-picker2/datePicker2.tsx +++ b/packages/datetime2/src/components/date-picker2/datePicker2.tsx @@ -17,9 +17,9 @@ import classNames from "classnames"; import type { Locale } from "date-fns"; import * as React from "react"; -import { /* CaptionLabelProps, */ DayModifiers, DayPicker /*, NavigationContextValue */ } from "react-day-picker"; +import { /* CaptionLabelProps, */ ActiveModifiers, DayPicker /*, NavigationContextValue */ } from "react-day-picker"; -import { AbstractPureComponent2, Button, DISPLAYNAME_PREFIX, Divider, Props } from "@blueprintjs/core"; +import { AbstractPureComponent, Button, DISPLAYNAME_PREFIX, Divider, Props } from "@blueprintjs/core"; import { Classes, DatePickerProps, @@ -37,7 +37,7 @@ import { Shortcuts } from "@blueprintjs/datetime/lib/esm/components/shortcuts/sh // tslint:enable no-submodule-imports /** Props shared between DatePicker v1 and v2 */ -type DatePickerSharedProps = Omit; +type DatePickerSharedProps = Omit; export interface DatePicker2Props extends DatePickerSharedProps, Props { /** @@ -75,7 +75,7 @@ interface DatePicker2State { * * @see https://blueprintjs.com/docs/#datetime2/date-picker2 */ -export class DatePicker extends AbstractPureComponent2 { +export class DatePicker2 extends AbstractPureComponent { public static defaultProps: DatePicker2Props = { canClearSelection: true, clearButtonText: "Clear", @@ -121,19 +121,17 @@ export class DatePicker extends AbstractPureComponent2 @@ -202,25 +200,10 @@ export class DatePicker extends AbstractPureComponent2 { - const { highlightCurrentDay } = this.props; - - return highlightCurrentDay === true && DateUtils.isToday(date); - }; - - private getDatePickerModifiers = () => { - const { modifiers } = this.props; - - return { - isToday: this.shouldHighlightCurrentDay, - ...modifiers, - } as DayModifiers; - }; - private renderDay = (day: Date) => { const date = day.getDate(); @@ -330,10 +313,13 @@ export class DatePicker extends AbstractPureComponent2 { + private handleDaySelect = (day: Date | undefined, _selectedDay: Date | undefined, activeModifiers: ActiveModifiers, e: React.MouseEvent) => { // @ts-ignore -- HACKHACK(@adidahiya): multiple versions of rdp types in this monorepo create a type conflict here this.props.dayPickerProps?.onDayClick?.(day, modifiers, e); - if (modifiers.disabled) { + if (activeModifiers.disabled) { + return; + } else if (day === undefined) { + this.handleClearClick(); return; } @@ -341,7 +327,7 @@ export class DatePicker extends AbstractPureComponent2 +
Additional CSS required
+ +This library relies on some components from other Blueprint packages, so you will need to pull in those +packages' CSS files as well (if you are not doing this already): + +```scss +@import "~@blueprintjs/datetime/lib/css/blueprint-datetime.css"; +@import "~@blueprintjs/select/lib/css/blueprint-select.css"; +``` + + +@page date-picker2 diff --git a/packages/datetime2/src/index.ts b/packages/datetime2/src/index.ts index a9f8b4fd42b..0c6c7403e97 100644 --- a/packages/datetime2/src/index.ts +++ b/packages/datetime2/src/index.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +export { DatePicker2, DatePicker2Props } from "./components/date-picker2/datePicker2"; +export * as DateInput2MigrationUtils from "./dateInput2MigrationUtils"; + /* eslint-disable deprecation/deprecation */ export { @@ -34,11 +37,11 @@ export { /** @deprecated import from `@blueprintjs/datetime` instead */ getTimezoneMetadata, /** @deprecated import from `@blueprintjs/datetime` instead */ + TimePrecision, + /** @deprecated import from `@blueprintjs/datetime` instead */ TimezoneSelect, /** @deprecated import from `@blueprintjs/datetime` instead */ TimezoneSelectProps, /** @deprecated import from `@blueprintjs/datetime` instead */ TimezoneDisplayFormat, } from "@blueprintjs/datetime"; - -export * as DateInput2MigrationUtils from "./dateInput2MigrationUtils"; diff --git a/packages/docs-app/src/_nav.md b/packages/docs-app/src/_nav.md index 56fe4e087b0..c5245ba3890 100644 --- a/packages/docs-app/src/_nav.md +++ b/packages/docs-app/src/_nav.md @@ -5,6 +5,7 @@ This file enumerates the exact order of root pages in the left sidebar. @page blueprint @page core @page datetime +@page datetime2 @page icons @page select @page table diff --git a/packages/docs-app/src/blueprint.md b/packages/docs-app/src/blueprint.md index 8ecd3f88ae2..6d2839f217f 100644 --- a/packages/docs-app/src/blueprint.md +++ b/packages/docs-app/src/blueprint.md @@ -28,6 +28,7 @@ yarn add @blueprintjs/core react react-dom Additional UI components and APIs are available in: - [**@blueprintjs/icons**](https://www.npmjs.com/package/@blueprintjs/icons) - [**@blueprintjs/datetime**](https://www.npmjs.com/package/@blueprintjs/datetime) +- [**@blueprintjs/datetime2**](https://www.npmjs.com/package/@blueprintjs/datetime2) - [**@blueprintjs/select**](https://www.npmjs.com/package/@blueprintjs/select) - [**@blueprintjs/table**](https://www.npmjs.com/package/@blueprintjs/table) diff --git a/packages/docs-app/src/examples/datetime2-examples/datePicker2Example.tsx b/packages/docs-app/src/examples/datetime2-examples/datePicker2Example.tsx new file mode 100644 index 00000000000..6c99766d888 --- /dev/null +++ b/packages/docs-app/src/examples/datetime2-examples/datePicker2Example.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { format } from "date-fns"; +import * as React from "react"; + +import { Callout, Classes, H5, Switch, Tag } from "@blueprintjs/core"; +import { DatePicker2, TimePrecision } from "@blueprintjs/datetime2"; +import { Example, ExampleProps, handleBooleanChange, handleValueChange } from "@blueprintjs/docs-theme"; + +import { PrecisionSelect } from "../datetime-examples/common/precisionSelect"; + +const exampleFooterElement = This additional footer component can be displayed below the date picker; + +export interface DatePicker2ExampleState { + date: Date | null; + highlightCurrentDay: boolean; + reverseMonthAndYearMenus: boolean; + shortcuts: boolean; + showActionsBar: boolean; + timePrecision: TimePrecision | undefined; + showTimeArrowButtons: boolean; + useAmPm?: boolean; + showFooterElement: boolean; +} + +export class DatePicker2Example extends React.PureComponent { + public state: DatePicker2ExampleState = { + date: null, + highlightCurrentDay: false, + reverseMonthAndYearMenus: false, + shortcuts: false, + showActionsBar: false, + showFooterElement: false, + showTimeArrowButtons: false, + timePrecision: undefined, + useAmPm: false, + }; + + private toggleHighlight = handleBooleanChange(highlightCurrentDay => this.setState({ highlightCurrentDay })); + + private toggleActionsBar = handleBooleanChange(showActionsBar => this.setState({ showActionsBar })); + + private toggleShowFooterElement = handleBooleanChange(showFooterElement => this.setState({ showFooterElement })); + + private toggleShortcuts = handleBooleanChange(shortcuts => this.setState({ shortcuts })); + + private toggleReverseMenus = handleBooleanChange(reverse => this.setState({ reverseMonthAndYearMenus: reverse })); + + private handlePrecisionChange = handleValueChange((p: TimePrecision | "none") => + this.setState({ timePrecision: p === "none" ? undefined : p }), + ); + + private toggleTimepickerArrowButtons = handleBooleanChange(showTimeArrowButtons => + this.setState({ showTimeArrowButtons }), + ); + + private toggleUseAmPm = handleBooleanChange(useAmPm => this.setState({ useAmPm })); + + public render() { + const { date, showTimeArrowButtons, useAmPm, ...props } = this.state; + const showTimePicker = this.state.timePrecision !== undefined; + + const options = ( + <> +
Props
+ + + + + + + + + + ); + + const timePickerProps = showTimePicker + ? { + showArrowButtons: showTimeArrowButtons, + useAmPm, + } + : undefined; + + return ( + + + + {date == null ? "No date" : format(date, "PPPppp")} + + + ); + } + + private handleDateChange = (date: Date) => this.setState({ date }); +} diff --git a/packages/docs-app/src/examples/datetime2-examples/index.ts b/packages/docs-app/src/examples/datetime2-examples/index.ts new file mode 100644 index 00000000000..9551fd58124 --- /dev/null +++ b/packages/docs-app/src/examples/datetime2-examples/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from "./datePicker2Example"; diff --git a/packages/docs-app/src/index.scss b/packages/docs-app/src/index.scss index a3dd6478c82..bf715152b54 100644 --- a/packages/docs-app/src/index.scss +++ b/packages/docs-app/src/index.scss @@ -10,6 +10,7 @@ Licensed under the Apache License, Version 2.0. // import these first because they're the base layer @import "@blueprintjs/core/lib/css/blueprint.css"; @import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"; +@import "@blueprintjs/datetime2/lib/css/blueprint-datetime2.css"; @import "@blueprintjs/icons/lib/css/blueprint-icons.css"; @import "@blueprintjs/select/lib/css/blueprint-select.css"; @import "@blueprintjs/table/lib/css/table.css"; diff --git a/packages/docs-app/src/tags/reactExamples.ts b/packages/docs-app/src/tags/reactExamples.ts index ef00500f917..937eb5c0726 100644 --- a/packages/docs-app/src/tags/reactExamples.ts +++ b/packages/docs-app/src/tags/reactExamples.ts @@ -21,6 +21,7 @@ import { ExampleMap, ExampleProps } from "@blueprintjs/docs-theme"; import { getTheme } from "../components/blueprintDocs"; import * as CoreExamples from "../examples/core-examples"; import * as DatetimeExamples from "../examples/datetime-examples"; +import * as Datetime2Examples from "../examples/datetime2-examples"; import * as SelectExamples from "../examples/select-examples"; import * as TableExamples from "../examples/table-examples"; import { BlueprintExampleData } from "./types"; @@ -47,6 +48,7 @@ export const reactExamples: ExampleMap = (() => { return { ...getPackageExamples("core", CoreExamples as any), ...getPackageExamples("datetime", DatetimeExamples as any), + ...getPackageExamples("datetime2", Datetime2Examples as any), ...getPackageExamples("select", SelectExamples as any), ...getPackageExamples("table", TableExamples as any), }; From 8ab1a46668dba3d11424f4d65d6e7e175ab7e057 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Thu, 31 Aug 2023 14:08:52 -0400 Subject: [PATCH 04/26] Complete rdp v8 migration --- packages/datetime2/src/classes.ts | 22 +++ .../date-picker2/_date-picker2.scss | 56 ++---- .../components/date-picker2/datePicker2.tsx | 140 ++++---------- .../date-picker2/datePicker2Caption.tsx | 175 ++++++++++++++++++ .../date-picker2/datePicker2Context.tsx | 34 ++++ .../date-picker2/datePicker2Props.ts | 62 +++++++ .../date-picker2/datePicker2State.ts | 26 +++ packages/datetime2/src/index.ts | 3 +- .../datetime2-examples/datePicker2Example.tsx | 20 +- 9 files changed, 389 insertions(+), 149 deletions(-) create mode 100644 packages/datetime2/src/classes.ts create mode 100644 packages/datetime2/src/components/date-picker2/datePicker2Caption.tsx create mode 100644 packages/datetime2/src/components/date-picker2/datePicker2Context.tsx create mode 100644 packages/datetime2/src/components/date-picker2/datePicker2Props.ts create mode 100644 packages/datetime2/src/components/date-picker2/datePicker2State.ts diff --git a/packages/datetime2/src/classes.ts b/packages/datetime2/src/classes.ts new file mode 100644 index 00000000000..df456cb4f10 --- /dev/null +++ b/packages/datetime2/src/classes.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Classes as DatetimeClasses } from "@blueprintjs/datetime"; + +export const Classes = { + ...DatetimeClasses, + DATEPICKER_HIGHLIGHT_CURRENT_DAY: `${DatetimeClasses.DATEPICKER}-highlight-current-day`, +}; diff --git a/packages/datetime2/src/components/date-picker2/_date-picker2.scss b/packages/datetime2/src/components/date-picker2/_date-picker2.scss index 2883da4ddb4..bf96641a486 100644 --- a/packages/datetime2/src/components/date-picker2/_date-picker2.scss +++ b/packages/datetime2/src/components/date-picker2/_date-picker2.scss @@ -30,11 +30,6 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; } } - .#{$ns}-datepicker-day-wrapper { - border-radius: $pt-border-radius; - padding: 7px; - } - .rdp-month { border-collapse: collapse; border-spacing: 0; @@ -48,10 +43,6 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; } } - .rdp-caption { - display: table-caption; - } - .rdp-weekdays { display: table-header-group; } @@ -72,11 +63,8 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; display: table-row; } - // TODO(@adahiya): verify that this is the correct selector - .rdp-week-number { - @include calendar-day(); + .rdp-weeknumber { color: $pt-text-color-disabled; - font-size: $pt-font-size; } .rdp-day { @@ -90,9 +78,8 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; } &.rdp-day_today { - .#{$ns}-datepicker-day-wrapper { - border: 1px solid $pt-divider-black; - } + // override rdp's default of making today's date bold + font-weight: 400; } // need some extra specificity here to override rdp default styles @@ -108,7 +95,6 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; } } - // TODO(@adahiya): verify that this is the correct selector &.rdp-day_selected { background-color: $blue3; border-radius: $pt-border-radius; @@ -130,30 +116,17 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; cursor: not-allowed; } } -} - -.#{$ns}-datepicker-navbar { - align-items: center; - display: flex; - height: $pt-button-height; - left: 0; - position: absolute; - right: 0; - top: 0; - - > .rdp-nav_button_previous { - margin-right: auto; - } - > .rdp-nav_button_next { - margin-left: auto; + &.#{$ns}-datepicker-highlight-current-day .rdp-day.rdp-day_today { + border: 1px solid $pt-divider-black; } } -.#{$ns}-datepicker-caption { - @include pt-flex-container(row, $fill: ":first-child"); +// use extra specificity to override DatePicker v1 styles +.#{$ns}-datepicker-caption.rdp-caption { + @include pt-flex-container(row); justify-content: space-between; - margin: 0 ($pt-button-height - $datepicker-padding) $datepicker-padding; + margin: 0 0 $datepicker-padding; // override HTMLSelect styles for a narrower appearance // N.B. these styles are important to achieve accurate measurements to position the dropdown arrows in @@ -207,7 +180,6 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; .#{$ns}-dark .#{$ns}-datepicker { background: $dark-datepicker-background-color; - // TODO(@adahiya): verify that this is the correct selector .rdp-week-number { color: $pt-dark-text-color-disabled; } @@ -217,12 +189,6 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; color: $pt-dark-text-color-disabled; } - &.rdp-day_today { - .#{$ns}-datepicker-day-wrapper { - border: 1px solid $pt-dark-divider-white; - } - } - // need some extra specificity here to override rdp default styles &:not([disabled]):not(.rdp-day_selected) { &:hover, @@ -254,6 +220,10 @@ $header-margin: ($header-height - $pt-input-height) * 0.5 !default; } } + &.#{$ns}-datepicker-highlight-current-day .rdp-day.rdp-day_today { + border: 1px solid $pt-dark-divider-white; + } + .#{$ns}-datepicker-footer { border-top-color: $pt-dark-divider-black; } diff --git a/packages/datetime2/src/components/date-picker2/datePicker2.tsx b/packages/datetime2/src/components/date-picker2/datePicker2.tsx index 3d39b9b58ae..1340041e7b8 100644 --- a/packages/datetime2/src/components/date-picker2/datePicker2.tsx +++ b/packages/datetime2/src/components/date-picker2/datePicker2.tsx @@ -15,14 +15,11 @@ */ import classNames from "classnames"; -import type { Locale } from "date-fns"; import * as React from "react"; -import { /* CaptionLabelProps, */ ActiveModifiers, DayPicker /*, NavigationContextValue */ } from "react-day-picker"; +import { ActiveModifiers, DayPicker } from "react-day-picker"; -import { AbstractPureComponent, Button, DISPLAYNAME_PREFIX, Divider, Props } from "@blueprintjs/core"; +import { AbstractPureComponent, Button, DISPLAYNAME_PREFIX, Divider } from "@blueprintjs/core"; import { - Classes, - DatePickerProps, DatePickerUtils, DateRange, DateRangeShortcut, @@ -31,44 +28,14 @@ import { } from "@blueprintjs/datetime"; // tslint:disable no-submodule-imports import * as Errors from "@blueprintjs/datetime/lib/esm/common/errors"; -import { DatePickerCaption } from "@blueprintjs/datetime/lib/esm/components/date-picker/datePickerCaption"; -import { DatePickerNavbar } from "@blueprintjs/datetime/lib/esm/components/date-picker/datePickerNavbar"; import { Shortcuts } from "@blueprintjs/datetime/lib/esm/components/shortcuts/shortcuts"; // tslint:enable no-submodule-imports -/** Props shared between DatePicker v1 and v2 */ -type DatePickerSharedProps = Omit; - -export interface DatePicker2Props extends DatePickerSharedProps, Props { - /** - * Initial day the calendar will display as selected. - * This should not be set if `value` is set. - */ - defaultValue?: Date; - - /** - * Called when the user selects a day. - * If being used in an uncontrolled manner, `selectedDate` will be `null` if the user clicks the currently selected - * day. If being used in a controlled manner, `selectedDate` will contain the day clicked no matter what. - * `isUserChange` is true if the user selected a day, and false if the date was automatically changed - * by the user navigating to a new month or year rather than explicitly clicking on a date in the calendar. - */ - onChange?: (selectedDate: Date | null, isUserChange: boolean) => void; - - /** - * The currently selected day. If this prop is provided, the component acts in a controlled manner. - */ - value?: Date | null; -} - -interface DatePicker2State { - displayMonth: number; - displayYear: number; - locale: Locale | undefined; - selectedDay: number | null; - value: Date | null; - selectedShortcutIndex?: number; -} +import { Classes } from "../../classes"; +import { DatePicker2Caption } from "./datePicker2Caption"; +import { DatePicker2Props } from "./datePicker2Props"; +import { DatePicker2State } from "./datePicker2State"; +import { DatePicker2Provider } from "./datePicker2Context"; /** * Date picker (v2) component. @@ -89,7 +56,7 @@ export class DatePicker2 extends AbstractPureComponent +
{this.maybeRenderShortcuts()}
- - {this.maybeRenderTimePicker()} - {showActionsBar && this.renderOptionsBar()} - {footerElement} + + + {this.maybeRenderTimePicker()} + {showActionsBar && this.renderOptionsBar()} + {footerElement} +
); } public async componentDidMount() { - await this.loadLocale(this.props.locale); + await this.loadLocale(this.props.localeCode); } public async componentDidUpdate(prevProps: DatePicker2Props, prevState: DatePicker2State) { @@ -170,12 +140,12 @@ export class DatePicker2 extends AbstractPureComponent { - const date = day.getDate(); - - return
{date}
; - }; - - private disabledDays = (day: Date) => !DateUtils.isDayInRange(day, [this.props.minDate!, this.props.maxDate!]); - - private getDisabledDaysModifier = () => { - const { dayPickerProps } = this.props; - - return Array.isArray(dayPickerProps?.disabledDays) - ? [this.disabledDays, ...dayPickerProps!.disabledDays] - : [this.disabledDays, dayPickerProps?.disabledDays]; - }; - - // TODO(@adidahiya) - private renderCaption = (props: any /* CaptionElementProps */) => ( - - ); - - // TODO(@adidahiya) - private renderNavbar = (props: any /* NavbarElementProps */) => ( - - ); - private renderOptionsBar() { const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props; const todayEnabled = isTodayEnabled(minDate!, maxDate!); diff --git a/packages/datetime2/src/components/date-picker2/datePicker2Caption.tsx b/packages/datetime2/src/components/date-picker2/datePicker2Caption.tsx new file mode 100644 index 00000000000..6876e97d8a6 --- /dev/null +++ b/packages/datetime2/src/components/date-picker2/datePicker2Caption.tsx @@ -0,0 +1,175 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import { format } from "date-fns"; +import * as React from "react"; +import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker"; + +import { Button, DISPLAYNAME_PREFIX, Divider, HTMLSelect, OptionProps } from "@blueprintjs/core"; +import { Classes, DateUtils } from "@blueprintjs/datetime"; +import { ChevronLeft, ChevronRight, IconSize } from "@blueprintjs/icons"; + +// tslint:disable-next-line no-submodule-imports +import { measureTextWidth } from "@blueprintjs/datetime/lib/esm/common/utils"; + +import { DatePicker2Context } from "./datePicker2Context"; + +export function DatePicker2Caption(captionProps: CaptionProps) { + const { classNames: rdpClassNames, fromDate, toDate, labels } = useDayPicker(); + const { locale, reverseMonthAndYearMenus } = React.useContext(DatePicker2Context); + + // non-null assertion because we define these values in defaultProps + const minYear = fromDate!.getFullYear(); + const maxYear = toDate!.getFullYear(); + + const displayMonth = captionProps.displayMonth.getMonth(); + const displayYear = captionProps.displayMonth.getFullYear(); + + const containerElement = React.useRef(null); + const [monthRightOffset, setMonthRightOffset] = React.useState(0); + const { currentMonth, goToMonth, nextMonth, previousMonth } = useNavigation(); + + const handlePreviousClick = React.useCallback(() => previousMonth && goToMonth(previousMonth), [previousMonth, goToMonth]); + const handleNextClick = React.useCallback(() => nextMonth && goToMonth(nextMonth), [nextMonth, goToMonth]); + + const prevButton = ( +