From 69c7e3aac3cc75613015a09c9ea5c4a0f4530b47 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Tue, 30 Aug 2022 18:57:48 +0300
Subject: [PATCH] Make the Between filter easier to use and understand (#25033)

---
 .../metabase/components/Calendar.styled.tsx   |   4 +-
 frontend/src/metabase/components/Calendar.tsx |  15 +-
 .../metabase/components/InputBlurChange.jsx   |   2 +
 .../filters/pickers/DatePicker/DatePicker.tsx |  59 ++++++--
 .../DatePicker/DatePicker.unit.spec.tsx       |   5 +-
 .../DatePicker/DatePickerShortcutOptions.tsx  |   3 +-
 .../DatePicker/RangeDatePicker.styled.tsx     |  11 +-
 .../pickers/DatePicker/RangeDatePicker.tsx    | 141 +++++++++++-------
 .../DatePicker/RangeDatePicker.unit.spec.tsx  |  54 +++++++
 .../pickers/DatePicker/SingleDatePicker.tsx   |   6 +-
 .../DatePicker/SpecificDatePicker.styled.tsx  |  31 +++-
 .../pickers/DatePicker/SpecificDatePicker.tsx |  88 ++++++-----
 12 files changed, 292 insertions(+), 127 deletions(-)
 create mode 100644 frontend/src/metabase/query_builder/components/filters/pickers/DatePicker/RangeDatePicker.unit.spec.tsx

diff --git a/frontend/src/metabase/components/Calendar.styled.tsx b/frontend/src/metabase/components/Calendar.styled.tsx
index 24b2d84e1db..3f9e74a5454 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 c19f355cfa1..815bc8097c3 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 f2357ffb700..fb3ee79421d 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 e3d1be36903..1b402fb67fa 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 6a6277eda5a..50e50c0135f 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 1d6c54b57e6..a0c313f80d4 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 4ee03f48632..1fe373ba69a 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 e2edbd12af1..de84f718b5b 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 00000000000..5f3deeaed4b
--- /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 37a99b6633d..0e6c4033603 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 f918d9c7bf6..4a8322d25e1 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 3e02f668c35..aadc45c41b7 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}
-- 
GitLab