-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🪟 🎉 Add datepicker for date/date-time fields in connector form (#19678)
Co-authored-by: Tim Roes <tim@airbyte.io> Co-authored-by: Lake Mossman <lake@airbyte.io>
- Loading branch information
1 parent
9cb5714
commit 6e155d3
Showing
15 changed files
with
5,389 additions
and
15,010 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
airbyte-webapp/src/components/ui/DatePicker/DatePicker.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/* stylelint-disable selector-class-pattern, no-descending-specificity */ | ||
|
||
@use "scss/colors"; | ||
@use "scss/fonts"; | ||
@use "scss/variables"; | ||
@use "scss/z-indices"; | ||
|
||
.wrapper { | ||
position: relative; | ||
} | ||
|
||
.datepickerButtonContainer { | ||
position: absolute; | ||
right: 0; | ||
top: 0; | ||
display: flex; | ||
height: 100%; | ||
align-items: center; | ||
justify-content: center; | ||
border: none; | ||
} | ||
|
||
.datepickerButton svg { | ||
font-size: 14px; | ||
} | ||
|
||
.popup { | ||
z-index: z-indices.$datepicker; | ||
} | ||
|
||
.input { | ||
padding-right: 25px; | ||
} | ||
|
||
:global(.react-datepicker) { | ||
display: flex; | ||
color: colors.$dark-blue-900; | ||
font-family: fonts.$primary; | ||
border: variables.$border-thick solid colors.$grey-100; | ||
border-radius: variables.$border-radius-md; | ||
box-shadow: 2px 2px 20px -6px colors.$grey-200; | ||
} | ||
|
||
/** Main calendar area **/ | ||
:global(.react-datepicker__header) { | ||
background-color: colors.$grey-50; | ||
border-top-right-radius: variables.$border-radius-md; | ||
border-top-left-radius: variables.$border-radius-md; | ||
border-bottom: none; | ||
} | ||
|
||
:global(.react-datepicker__header.react-datepicker__header--has-time-select) { | ||
border-top-right-radius: 0; | ||
} | ||
|
||
:global(.react-datepicker__header:not(.react-datepicker__header--has-time-select)) { | ||
border-top-right-radius: variables.$border-radius-md; | ||
} | ||
|
||
:global(.react-datepicker__day-name) { | ||
color: colors.$grey-300; | ||
} | ||
|
||
:global(.react-datepicker__current-month) { | ||
color: colors.$dark-blue-900; | ||
font-weight: 500; | ||
margin-bottom: variables.$spacing-md; | ||
} | ||
|
||
:global(.react-datepicker__day) { | ||
color: colors.$dark-blue-900; | ||
background-color: transparent; | ||
border-radius: variables.$border-radius-xs; | ||
|
||
&:hover { | ||
background-color: colors.$grey-100; | ||
} | ||
} | ||
|
||
:global(.react-datepicker__day--outside-month) { | ||
color: colors.$grey-300; | ||
} | ||
|
||
:global(.react-datepicker__day--today) { | ||
background-color: colors.$grey-50; | ||
font-weight: 700; | ||
} | ||
|
||
:global(.react-datepicker__day--selected) { | ||
color: colors.$white; | ||
background-color: colors.$blue-400; | ||
font-weight: 700; | ||
|
||
&:hover { | ||
background-color: colors.$blue-500; | ||
} | ||
} | ||
|
||
:global(.react-datepicker__navigation-icon::before) { | ||
border-width: 2px 2px 0 0; | ||
} | ||
|
||
/** Time **/ | ||
:global(.react-datepicker__time-container) { | ||
border-left-color: colors.$grey-100; | ||
border-left-width: variables.$border-thick; | ||
border-bottom-right-radius: variables.$border-radius-md; | ||
overflow: hidden; | ||
} | ||
|
||
:global(.react-datepicker-time__header) { | ||
font-weight: 500; | ||
color: colors.$dark-blue-900; | ||
} | ||
|
||
:global(.react-datepicker__time-list-item) { | ||
background-color: transparent; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
|
||
&:hover { | ||
background-color: colors.$grey-100; | ||
} | ||
} | ||
|
||
:global(.react-datepicker__time-container | ||
.react-datepicker__time | ||
.react-datepicker__time-box | ||
ul.react-datepicker__time-list | ||
li.react-datepicker__time-list-item) { | ||
justify-content: center; | ||
} | ||
|
||
:global(.react-datepicker__time-container | ||
.react-datepicker__time | ||
.react-datepicker__time-box | ||
ul.react-datepicker__time-list | ||
li.react-datepicker__time-list-item--selected) { | ||
color: colors.$white; | ||
background-color: colors.$blue-400; | ||
|
||
&:hover { | ||
background-color: colors.$blue-500; | ||
} | ||
} |
144 changes: 144 additions & 0 deletions
144
airbyte-webapp/src/components/ui/DatePicker/DatePicker.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
import dayjs from "dayjs"; | ||
import { TestWrapper } from "test-utils/testutils"; | ||
import timezoneMock from "timezone-mock"; | ||
|
||
import { DatePicker, toEquivalentLocalTime } from "./DatePicker"; | ||
|
||
describe(`${toEquivalentLocalTime.name}`, () => { | ||
// Seems silly, but dayjs has a bug when formatting years, so this is a useful test: | ||
// https://github.com/iamkun/dayjs/issues/1745 | ||
it("handles a date in the year 1", () => { | ||
const TEST_UTC_TIMESTAMP = "0001-12-01T09:00:00Z"; | ||
|
||
const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP); | ||
|
||
expect(result).toEqual(undefined); | ||
}); | ||
|
||
it("handles an invalid date", () => { | ||
const TEST_UTC_TIMESTAMP = "not a date"; | ||
|
||
const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP); | ||
|
||
expect(result).toEqual(undefined); | ||
}); | ||
|
||
it("outputs the same YYYY-MM-DDTHH:mm:ss", () => { | ||
const TEST_UTC_TIMESTAMP = "2000-01-01T12:00:00Z"; | ||
|
||
const result = toEquivalentLocalTime(TEST_UTC_TIMESTAMP); | ||
|
||
// Regardless of the timezone, the local time should be the same | ||
expect(result?.toISOString().substring(0, 19)).toEqual(TEST_UTC_TIMESTAMP.substring(0, 19)); | ||
}); | ||
|
||
it("converts utc time to equivalent local time in PST", () => { | ||
timezoneMock.register("US/Pacific"); | ||
const TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES = 480; // corresponds to the registered mock timezone | ||
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z"; | ||
|
||
const expectedDateObject = dayjs | ||
.utc(TEST_UTC_TIMESTAMP) | ||
.add(TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES, "minutes") | ||
.toDate(); | ||
|
||
expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject); | ||
}); | ||
|
||
it("converts utc time to equivalent local time in EST", () => { | ||
timezoneMock.register("US/Eastern"); | ||
const TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES = 300; // corresponds to the registered mock timezone | ||
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z"; | ||
|
||
const expectedDateObject = dayjs | ||
.utc(TEST_UTC_TIMESTAMP) | ||
.add(TEST_TIMEZONE_UTC_OFFSET_IN_MINUTES, "minutes") | ||
.toDate(); | ||
|
||
expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject); | ||
}); | ||
|
||
it("keeps a utc timestamp exactly the same", () => { | ||
timezoneMock.register("UTC"); | ||
const TEST_UTC_TIMESTAMP = "2022-01-01T00:00:00Z"; | ||
|
||
const expectedDateObject = dayjs.utc(TEST_UTC_TIMESTAMP).toDate(); | ||
|
||
expect(toEquivalentLocalTime(TEST_UTC_TIMESTAMP)).toEqual(expectedDateObject); | ||
}); | ||
|
||
afterEach(() => { | ||
// Return global Date() object to system behavior | ||
timezoneMock.unregister(); | ||
}); | ||
}); | ||
|
||
describe(`${DatePicker.name}`, () => { | ||
it("allows typing a date manually", async () => { | ||
const MOCK_DESIRED_DATETIME = "2010-09-12T00:00:00Z"; | ||
let mockValue = ""; | ||
render( | ||
<TestWrapper> | ||
<DatePicker | ||
onChange={(value) => { | ||
// necessary for controlled inputs https://github.com/testing-library/user-event/issues/387#issuecomment-819868799 | ||
mockValue = mockValue + value; | ||
}} | ||
value={mockValue} | ||
/> | ||
</TestWrapper> | ||
); | ||
|
||
const input = screen.getByTestId("input"); | ||
await userEvent.type(input, MOCK_DESIRED_DATETIME, { delay: 1 }); | ||
|
||
expect(mockValue).toEqual(MOCK_DESIRED_DATETIME); | ||
}); | ||
|
||
it("allows selecting a date from the datepicker", async () => { | ||
jest.useFakeTimers().setSystemTime(new Date("2010-09-05")); | ||
const MOCK_DESIRED_DATETIME = "2010-09-12"; | ||
let mockValue = ""; | ||
render( | ||
<TestWrapper> | ||
<DatePicker | ||
onChange={(value) => { | ||
// necessary for controlled inputs https://github.com/testing-library/user-event/issues/387#issuecomment-819868799 | ||
mockValue = mockValue + value; | ||
}} | ||
value={mockValue} | ||
/> | ||
</TestWrapper> | ||
); | ||
|
||
const datepicker = screen.getByLabelText("Open datepicker"); | ||
userEvent.click(datepicker); | ||
const date = screen.getByLabelText("Choose Sunday, September 12th, 2010"); | ||
userEvent.click(date); | ||
|
||
expect(mockValue).toEqual(MOCK_DESIRED_DATETIME); | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it("focuses the input after selecting a date from the datepicker", async () => { | ||
jest.useFakeTimers().setSystemTime(new Date("2010-09-05")); | ||
let mockValue = ""; | ||
render( | ||
<TestWrapper> | ||
<DatePicker onChange={(value) => (mockValue = value)} value={mockValue} /> | ||
</TestWrapper> | ||
); | ||
|
||
const datepicker = screen.getByLabelText("Open datepicker"); | ||
userEvent.click(datepicker); | ||
const date = screen.getByLabelText("Choose Sunday, September 12th, 2010"); | ||
userEvent.click(date); | ||
|
||
const input = screen.getByTestId("input"); | ||
|
||
expect(input).toHaveFocus(); | ||
jest.useRealTimers(); | ||
}); | ||
}); |
Oops, something went wrong.