diff --git a/frontend/src/metabase/components/Calendar.styled.tsx b/frontend/src/metabase/components/Calendar.styled.tsx index 24b2d84e1dbd426c3dc4774085852a6eb3afd28a..3f9e74a54544d0efa11fa400789a9166f4da753d 100644 --- a/frontend/src/metabase/components/Calendar.styled.tsx +++ b/frontend/src/metabase/components/Calendar.styled.tsx @@ -31,11 +31,11 @@ export const CalendarDay = styled.div<CalendarDayProps>` color: white; } - ${({ isSelectedStart, isSelectedEnd }) => + ${({ primaryColor, isSelectedStart, isSelectedEnd }) => (isSelectedStart || isSelectedEnd) && css` color: ${color("white")} !important; - background-color: ${color("filter")}; + background-color: ${primaryColor}; z-index: 1; `} `; diff --git a/frontend/src/metabase/components/Calendar.tsx b/frontend/src/metabase/components/Calendar.tsx index c19f355cfa1189303cf16214f1b0426847d6937a..815bc8097c3a27234e4772bb0c344e5f2a0d3cbb 100644 --- a/frontend/src/metabase/components/Calendar.tsx +++ b/frontend/src/metabase/components/Calendar.tsx @@ -1,6 +1,5 @@ /* eslint-disable react/prop-types */ import React, { Component } from "react"; -import PropTypes from "prop-types"; import "./Calendar.css"; @@ -8,7 +7,7 @@ import cx from "classnames"; import moment, { Moment } from "moment-timezone"; import { t } from "ttag"; import Icon from "metabase/components/Icon"; -import { alpha, color } from "metabase/lib/colors"; +import { color } from "metabase/lib/colors"; import { CalendarDay } from "./Calendar.styled"; export type SelectAll = "after" | "before"; @@ -18,12 +17,13 @@ type Props = { selected?: Moment; selectedEnd?: Moment; selectAll?: SelectAll | null; - onChange: ( + onChange?: ( start: string, end: string | null, startMoment: Moment, endMoment?: Moment | null, ) => void; + onChangeDate?: (date: string, dateMoment: Moment) => void; isRangePicker?: boolean; primaryColor?: string; }; @@ -77,18 +77,19 @@ export default class Calendar extends Component<Props, State> { onClickDay = (date: Moment) => { const { selected, selectedEnd, isRangePicker } = this.props; + if (!isRangePicker || !selected || selectedEnd) { - this.props.onChange(date.format("YYYY-MM-DD"), null, date, null); + this.props.onChange?.(date.format("YYYY-MM-DD"), null, date, null); } else if (!selectedEnd) { if (date.isAfter(selected)) { - this.props.onChange( + this.props.onChange?.( selected.format("YYYY-MM-DD"), date.format("YYYY-MM-DD"), selected, date, ); } else { - this.props.onChange( + this.props.onChange?.( date.format("YYYY-MM-DD"), selected.format("YYYY-MM-DD"), date, @@ -96,6 +97,8 @@ export default class Calendar extends Component<Props, State> { ); } } + + this.props.onChangeDate?.(date.format("YYYY-MM-DD"), date); }; previous = () => { diff --git a/frontend/src/metabase/components/InputBlurChange.jsx b/frontend/src/metabase/components/InputBlurChange.jsx index f2357ffb7001a5e410dd25966f06e08290fbd0ec..fb3ee79421d20081e3f3f69e12088e2c5b4d6742 100644 --- a/frontend/src/metabase/components/InputBlurChange.jsx +++ b/frontend/src/metabase/components/InputBlurChange.jsx @@ -22,6 +22,8 @@ export default class InputBlurChange extends Component { className: PropTypes.string, name: PropTypes.string, placeholder: PropTypes.string, + autoFocus: PropTypes.bool, + onFocus: PropTypes.func, onChange: PropTypes.func, onBlurChange: PropTypes.func, }; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.tsx index e3d1be36903f45c69c64a890e5a0f21044ff85c0..1b402fb67fa7fa37eb892d46dc5a15552c77da64 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.tsx @@ -81,8 +81,8 @@ export function getDateTimeFieldTarget(field: any[]) { } // add temporal-unit to fields if any of them have a time component -function getDateTimeFieldAndValues(filter: Filter, count: number) { - let values = filter.slice(2, 2 + count).map(value => value && getDate(value)); +function getDateTimeFieldAndValues(filter: Filter) { + let values = filter.slice(2).map(value => value && getDate(value)); const bucketing = _.any(values, hasTime) ? "minute" : null; const field = getDateTimeField(filter, bucketing); const { hours, minutes } = getTimeComponent(values[0]); @@ -104,6 +104,50 @@ function getDateTimeFieldAndValues(filter: Filter, count: number) { return [field, ...values.filter(value => value !== undefined)]; } +function getOnFilterFieldAndValues(filter: Filter) { + const [op] = filter; + const [field, ...values] = getDateTimeFieldAndValues(filter); + + if (op === "between") { + return [field, values[1]]; + } else { + return [field, values[0]]; + } +} + +function getBeforeFilterFieldAndValues(filter: Filter) { + const [op] = filter; + const [field, ...values] = getDateTimeFieldAndValues(filter); + + if (op === "between") { + return [field, values[1]]; + } else { + return [field, values[0]]; + } +} + +function getAfterFilterFieldAndValues(filter: Filter) { + const [field, ...values] = getDateTimeFieldAndValues(filter); + return [field, values[0]]; +} + +function getBetweenFilterFieldAndValues(filter: Filter) { + const [op] = filter; + const [field, ...values] = getDateTimeFieldAndValues(filter); + + if (op === "=" || op === "<") { + const beforeDate = moment(values[0]).subtract(30, "day"); + const beforeValue = beforeDate.format("YYYY-MM-DD"); + return [field, beforeValue, values[0]]; + } else if (op === ">") { + const afterDate = moment(values[0]).add(30, "day"); + const afterValue = afterDate.format("YYYY-MM-DD"); + return [field, values[0], afterValue]; + } else { + return [field, ...values]; + } +} + export type DatePickerGroup = "relative" | "specific"; export type DateOperator = { @@ -176,10 +220,7 @@ export const DATE_OPERATORS: DateOperator[] = [ { name: "between", displayName: t`Between`, - init: filter => { - const [field, ...values] = getDateTimeFieldAndValues(filter, 2); - return ["between", field, ...values]; - }, + init: filter => ["between", ...getBetweenFilterFieldAndValues(filter)], test: ([op, _field, left, right]) => op === "between" && !isRelativeDatetime(left) && @@ -190,7 +231,7 @@ export const DATE_OPERATORS: DateOperator[] = [ { name: "before", displayName: t`Before`, - init: filter => ["<", ...getDateTimeFieldAndValues(filter, 1)], + init: filter => ["<", ...getBeforeFilterFieldAndValues(filter)], test: ([op]) => op === "<", group: "specific", widget: BeforePicker, @@ -198,7 +239,7 @@ export const DATE_OPERATORS: DateOperator[] = [ { name: "on", displayName: t`On`, - init: filter => ["=", ...getDateTimeFieldAndValues(filter, 1)], + init: filter => ["=", ...getOnFilterFieldAndValues(filter)], test: ([op]) => op === "=", group: "specific", widget: SingleDatePicker, @@ -206,7 +247,7 @@ export const DATE_OPERATORS: DateOperator[] = [ { name: "after", displayName: t`After`, - init: filter => [">", ...getDateTimeFieldAndValues(filter, 1)], + init: filter => [">", ...getAfterFilterFieldAndValues(filter)], test: ([op]) => op === ">", group: "specific", widget: AfterPicker, diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.unit.spec.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.unit.spec.tsx index 6a6277eda5a93685d6b3b3a4c556a1197c83c11d..50e50c0135fc5a0f39495bc63403113df2f4a7ca 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.unit.spec.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePicker.unit.spec.tsx @@ -168,7 +168,7 @@ describe("DatePicker", () => { render(<DatePickerStateWrapper filter={filter} />); userEvent.click(screen.getByText(new RegExp(type, "i"))); - await screen.findByTestId(`${type}-date-picker`); + await screen.findAllByTestId(`${type}-date-picker`); }); }); @@ -235,6 +235,7 @@ describe("DatePicker", () => { <DatePickerStateWrapper filter={filter} onChange={changeSpy} />, ); userEvent.click(screen.getByText(/specific dates/i)); + userEvent.click(screen.getByText("On")); await screen.findByTestId(`specific-date-picker`); userEvent.click(screen.getByText(new RegExp(description, "i"))); const dateField = screen.getByText("21"); @@ -259,7 +260,6 @@ describe("DatePicker", () => { const dateField1 = screen.getByText("17"); const dateField2 = screen.getByText("19"); - userEvent.click(dateField1); // end range userEvent.click(dateField1); // begin range, clears end range userEvent.click(dateField2); // end range @@ -274,6 +274,7 @@ describe("DatePicker", () => { it("can navigate between months on the calendar using arrows", async () => { render(<DatePickerStateWrapper filter={filter} />); userEvent.click(screen.getByText(/specific/i)); + userEvent.click(screen.getByText("On")); await screen.findByText("May 2020"); userEvent.click(await screen.getByLabelText(/chevronright/i)); diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePickerShortcutOptions.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePickerShortcutOptions.tsx index 1d6c54b57e6bdd0b1f3817418c0d73110af9d05f..a0c313f80d46c4fc2d576ce06d545fc9e2852974 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePickerShortcutOptions.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/DatePickerShortcutOptions.tsx @@ -112,8 +112,9 @@ const MISC_OPTIONS: Option[] = [ { displayName: t`Specific dates...`, init: filter => [ - "=", + "between", getDateTimeField(filter[1]), + moment().subtract(30, "day").format("YYYY-MM-DD"), moment().format("YYYY-MM-DD"), ], }, diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.styled.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.styled.tsx index 4ee03f486329b8b8ede57241c6c98f5a391c3d2b..1fe373ba69af3189a029598ad2c97f582c182a60 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.styled.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.styled.tsx @@ -1,8 +1,11 @@ import styled from "@emotion/styled"; -import { space } from "metabase/styled-components/theme"; -export const TimeContainer = styled.div` +export const DateContainer = styled.div` display: flex; - grid-gap: ${space(2)}; - flex-wrap: no-wrap; + grid-gap: 0.25rem; + flex-wrap: nowrap; +`; + +export const DateDivider = styled.div` + margin-top: 0.65rem; `; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.tsx index e2edbd12af1cdcc6979d79400f365ce2aeaa8eab..de84f718b5b695c8882b05457d04dac06c6fdd76 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.tsx @@ -1,82 +1,107 @@ -/* eslint-disable react/prop-types */ -import React from "react"; - -import Calendar from "metabase/components/Calendar"; - -import moment from "moment-timezone"; +import React, { useCallback, useState } from "react"; +import moment, { Moment } from "moment-timezone"; import Filter from "metabase-lib/lib/queries/structured/Filter"; -import { TimeContainer } from "./RangeDatePicker.styled"; -import { - hasTimeComponent, - setTimeComponent, - TIME_SELECTOR_DEFAULT_HOUR, - TIME_SELECTOR_DEFAULT_MINUTE, -} from "metabase/lib/query_time"; +import { setTimeComponent } from "metabase/lib/query_time"; +import Calendar from "metabase/components/Calendar"; import SingleDatePicker, { SingleDatePickerProps } from "./SingleDatePicker"; import SpecificDatePicker from "./SpecificDatePicker"; +import { DateContainer, DateDivider } from "./RangeDatePicker.styled"; -type BetweenPickerProps = { +export interface BetweenPickerProps { className?: string; + filter: Filter | any[]; primaryColor?: string; - filter: Filter; - onFilterChange: (filter: any[]) => void; - hideTimeSelectors?: boolean; -}; + onFilterChange: (filter: any[]) => void; +} export const BetweenPicker = ({ className, filter: [op, field, startValue, endValue], - onFilterChange, - hideTimeSelectors, primaryColor, + hideTimeSelectors, + onFilterChange, }: BetweenPickerProps) => { - let endDatetime = endValue; - if (hasTimeComponent(startValue) && !hasTimeComponent(endValue)) { - endDatetime = setTimeComponent( - endValue, - TIME_SELECTOR_DEFAULT_HOUR, - TIME_SELECTOR_DEFAULT_MINUTE, - ); - } + const [isStartDateActive, setIsStartDateActive] = useState(true); + + const handleStartDateFocus = useCallback(() => { + setIsStartDateActive(true); + }, []); + + const handleEndDateFocus = useCallback(() => { + setIsStartDateActive(false); + }, []); + + const handleDateClick = useCallback( + (newValue: string, newDate: Moment) => { + if (isStartDateActive) { + onFilterChange([op, field, newValue, null]); + } else if (newDate.isBefore(startValue)) { + onFilterChange([op, field, newValue, startValue]); + } else { + onFilterChange([op, field, startValue, newValue]); + } + setIsStartDateActive(isActive => !isActive); + }, + [op, field, startValue, isStartDateActive, onFilterChange], + ); + + const handleStartDateChange = useCallback( + (newValue: string | null) => { + onFilterChange([op, field, newValue, endValue]); + setIsStartDateActive(isActive => !isActive); + }, + [op, field, endValue, onFilterChange], + ); + + const handleEndDateChange = useCallback( + (newValue: string | null) => { + onFilterChange([op, field, startValue, newValue]); + setIsStartDateActive(isActive => !isActive); + }, + [op, field, startValue, onFilterChange], + ); + + const handleEndDateClear = useCallback(() => { + onFilterChange([ + op, + field, + setTimeComponent(startValue), + setTimeComponent(endValue), + ]); + }, [op, field, startValue, endValue, onFilterChange]); + return ( <div className={className} data-testid="between-date-picker"> - <TimeContainer> - <div> - <SpecificDatePicker - value={startValue} - primaryColor={primaryColor} - hideTimeSelectors={hideTimeSelectors} - onChange={value => onFilterChange([op, field, value, endValue])} - /> - </div> - <div> - <SpecificDatePicker - value={endValue} - primaryColor={primaryColor} - hideTimeSelectors={hideTimeSelectors} - onClear={() => - onFilterChange([ - op, - field, - setTimeComponent(startValue), - setTimeComponent(endValue), - ]) - } - onChange={value => onFilterChange([op, field, startValue, value])} - /> - </div> - </TimeContainer> + <DateContainer> + <SpecificDatePicker + value={startValue} + primaryColor={primaryColor} + isActive={isStartDateActive} + hideTimeSelectors={hideTimeSelectors} + autoFocus + onFocus={handleStartDateFocus} + onChange={handleStartDateChange} + /> + <DateDivider>–</DateDivider> + <SpecificDatePicker + value={endValue} + primaryColor={primaryColor} + isActive={!isStartDateActive} + hideTimeSelectors={hideTimeSelectors} + onFocus={handleEndDateFocus} + onChange={handleEndDateChange} + onClear={handleEndDateClear} + /> + </DateContainer> <div className="Calendar--noContext"> <Calendar isRangePicker primaryColor={primaryColor} - initial={startValue} + initial={endValue} selected={startValue && moment(startValue)} selectedEnd={endValue && moment(endValue)} - onChange={(startValue, endValue) => - onFilterChange([op, field, startValue, endValue]) - } + onChangeDate={handleDateClick} /> </div> </div> diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.unit.spec.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f3deeaed4b60f6af01105ef26c9968c68188ae3 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.unit.spec.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { BetweenPicker } from "./RangeDatePicker"; + +interface TestBetweenPickerProps { + initialFilter: any[]; +} + +const TestBetweenPicker = ({ initialFilter }: TestBetweenPickerProps) => { + const [filter, setFilter] = useState(initialFilter); + return <BetweenPicker filter={filter} onFilterChange={setFilter} />; +}; + +describe("BetweenPicker", () => { + const field = ["field", 10, null]; + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2022-05-01 08:00:00")); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it("should change the filter by calendar", () => { + const initialFilter = ["between", field, "2022-08-10", "2022-08-29"]; + + render(<TestBetweenPicker initialFilter={initialFilter} />); + userEvent.click(screen.getByText("12")); + userEvent.click(screen.getByText("20")); + + expect(screen.getByDisplayValue("08/12/2022")).toBeInTheDocument(); + expect(screen.getByDisplayValue("08/20/2022")).toBeInTheDocument(); + }); + + it("should change the filter by keyboard and calendar", () => { + const initialFilter = ["between", field, "2022-08-10", "2022-08-29"]; + + render(<TestBetweenPicker initialFilter={initialFilter} />); + + const startDateInput = screen.getByDisplayValue("08/10/2022"); + userEvent.clear(startDateInput); + userEvent.type(startDateInput, "07/25/2022"); + userEvent.tab(); + + const endDateInput = screen.getByDisplayValue("08/29/2022"); + userEvent.click(screen.getByText("20")); + + expect(startDateInput).toHaveValue("07/25/2022"); + expect(endDateInput).toHaveValue("08/20/2022"); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SingleDatePicker.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SingleDatePicker.tsx index 37a99b6633d26d0c0c88c6d3051f2c2497e08f5b..0e6c4033603951bc2b931e310414c779bd2fa506 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SingleDatePicker.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SingleDatePicker.tsx @@ -11,9 +11,8 @@ export type SingleDatePickerProps = { filter: Filter; selectAll?: SelectAll; primaryColor?: string; - onFilterChange: (filter: any[]) => void; - hideTimeSelectors?: boolean; + onFilterChange: (filter: any[]) => void; }; const SingleDatePicker = ({ @@ -31,8 +30,9 @@ const SingleDatePicker = ({ selectAll={selectAll} onChange={value => onFilterChange([op, field, value])} onClear={() => onFilterChange([op, field, setTimeComponent(value)])} + autoFocus + hasCalendar hideTimeSelectors={hideTimeSelectors} - calendar /> ); diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.styled.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.styled.tsx index f918d9c7bf6ca4adaf2ea5750659ded818579ea9..4a8322d25e1bab8ef939a15627b2f203fdf85484 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.styled.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.styled.tsx @@ -1,6 +1,7 @@ import styled from "@emotion/styled"; -import Icon from "metabase/components/Icon"; import { color } from "metabase/lib/colors"; +import Icon from "metabase/components/Icon"; +import InputBlurChange from "metabase/components/InputBlurChange"; export const CalendarIcon = styled(Icon)` margin-right: 0.5rem; @@ -10,3 +11,31 @@ export const CalendarIcon = styled(Icon)` color: ${color("filter")}; } `; + +export const DateInput = styled(InputBlurChange)` + font-size: 1rem; + font-weight: 700; + width: 100%; + padding: 0.5rem; + border: none; + outline: none; + background: none; +`; + +interface DateInputContainerProps { + isActive?: boolean; +} + +export const DateInputContainer = styled.div<DateInputContainerProps>` + display: flex; + align-items: center; + width: 100%; + margin-bottom: 1rem; + border: 1px solid + ${({ isActive }) => (isActive ? color("brand") : color("border"))}; + border-radius: 0.5rem; + + &:focus-within { + border-color: ${color("brand")}; + } +`; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.tsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.tsx index 3e02f668c35af4e41bddd3e997496232f91fe0d9..aadc45c41b719fa5b158a4549376abc14fdc4cb9 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.tsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/SpecificDatePicker.tsx @@ -1,85 +1,89 @@ -/* eslint-disable react/prop-types */ import React from "react"; import { t } from "ttag"; import { getDateStyleFromSettings } from "metabase/lib/time"; import Calendar, { SelectAll } from "metabase/components/Calendar"; -import InputBlurChange from "metabase/components/InputBlurChange"; import ExpandingContent from "metabase/components/ExpandingContent"; import HoursMinutesInput from "./HoursMinutesInput"; import moment, { Moment } from "moment-timezone"; import { getTimeComponent, setTimeComponent } from "metabase/lib/query_time"; -import { CalendarIcon } from "./SpecificDatePicker.styled"; +import { + CalendarIcon, + DateInput, + DateInputContainer, +} from "./SpecificDatePicker.styled"; -type Props = { +interface SpecificDatePickerProps { className?: string; + value: string; primaryColor?: string; - calendar?: boolean; selectAll?: SelectAll; - + isActive?: boolean; + hasCalendar?: boolean; hideTimeSelectors?: boolean; - value: string; + autoFocus?: boolean; + onFocus?: () => void; onChange: (startValue: string | null, endValue?: string) => void; onClear?: () => void; -}; - -const SpecificDatePicker: React.FC<Props> = props => { - const onChange = ( - date?: string | Moment, - hours?: number | null, - minutes?: number | null, - ) => { - props.onChange(setTimeComponent(date, hours, minutes)); - }; +} - const { - value, - calendar, - hideTimeSelectors, - onClear, - className, - selectAll, - primaryColor, - } = props; +const SpecificDatePicker = ({ + className, + value, + primaryColor, + selectAll, + isActive, + hasCalendar, + hideTimeSelectors, + autoFocus, + onFocus, + onChange, + onClear, +}: SpecificDatePickerProps) => { const [showCalendar, setShowCalendar] = React.useState(true); - const { hours, minutes, date } = getTimeComponent(value); const showTimeSelectors = !hideTimeSelectors && typeof hours === "number" && typeof minutes === "number"; + + const handleChange = ( + date?: string | Moment, + hours?: number | null, + minutes?: number | null, + ) => { + onChange(setTimeComponent(date, hours, minutes)); + }; const dateFormat = getDateStyleFromSettings() || "MM/DD/YYYY"; return ( <div className={className} data-testid="specific-date-picker"> - <div className="mb2 full bordered rounded flex align-center"> - <InputBlurChange + <DateInputContainer isActive={isActive}> + <DateInput placeholder={moment().format(dateFormat)} - className="borderless full p1 h3" - style={{ - outline: "none", - }} value={date ? date.format(dateFormat) : ""} + autoFocus={autoFocus} + onFocus={onFocus} onBlurChange={({ target: { value } }: any) => { const date = moment(value, dateFormat); if (date.isValid()) { - onChange(date, hours, minutes); + handleChange(date, hours, minutes); } else { - onChange(); + handleChange(); } }} /> - {calendar && ( + {hasCalendar && ( <CalendarIcon name="calendar" onClick={() => setShowCalendar(!showCalendar)} tooltip={showCalendar ? t`Hide calendar` : t`Show calendar`} /> )} - </div> + </DateInputContainer> {showTimeSelectors && ( <div> @@ -87,20 +91,22 @@ const SpecificDatePicker: React.FC<Props> = props => { onClear={onClear} hours={hours} minutes={minutes} - onChangeHours={(hours: number) => onChange(date, hours, minutes)} + onChangeHours={(hours: number) => + handleChange(date, hours, minutes) + } onChangeMinutes={(minutes: number) => - onChange(date, hours, minutes) + handleChange(date, hours, minutes) } /> </div> )} - {calendar && ( + {hasCalendar && ( <ExpandingContent isOpen={showCalendar}> <Calendar selected={date} initial={date || moment()} - onChange={value => onChange(value, hours, minutes)} + onChange={value => handleChange(value, hours, minutes)} isRangePicker={false} selectAll={selectAll} primaryColor={primaryColor}