feat: add visual time picker to DateTimePicker (#15057) (#17952)

## Summary
Added a visual time picker dropdown for DateTime fields, replacing the
previous text input. Users can now select hours and minutes through an
intuitive scrollable interface. (Fixes #15057

 ## Changes
- **New component**: Add a `TimePickerDropdown` - Visual picker with
scrollable hour/minute columns
- **Updated**: `DateTimePickerHeader` - Implemented time picker dropdown
in `DateTimePickerHeader` and Move month/year picker to right side
  ## Snapshots
  
<img width="493" height="421" alt="image"
src="https://github.com/user-attachments/assets/3bd1f0a0-0ac2-473d-935e-d9f28b0e40e2"
/>


https://github.com/user-attachments/assets/daa5cba5-c86c-46aa-a634-0f5c04523af1



**If there is no enough place at right, auto-move month/year selector to
the left side**

Hi, @Bonapara I followed the Figma you shared to complete this feature.
Could you please review it for me? Thanks a lot.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
BugIsGod
2026-03-10 13:39:46 +00:00
committed by GitHub
parent dee55b635f
commit 033d297695
17 changed files with 731 additions and 196 deletions
@@ -135,8 +135,17 @@ type Story = StoryObj<typeof DateFieldInputWithContext>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const calendarButton = await canvas.findByRole(
'button',
{ name: 'Select month and year' },
{ timeout: 10000 },
);
await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
const div = await canvas.findByText('January', {}, { timeout: 10000 });
const div = await body.findByText('January', {}, { timeout: 10000 });
await expect(div.innerText).toContain('January');
},
@@ -145,13 +154,23 @@ export const Default: Story = {
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
const calendarButton = await canvas.findByRole(
'button',
{ name: 'Select month and year' },
{ timeout: 10000 },
);
await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
await canvas.findByText('January', {}, { timeout: 10000 });
await body.findByText('January', {}, { timeout: 10000 });
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await userEvent.click(emptyDiv);
await userEvent.click(emptyDiv);
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
},
@@ -161,9 +180,19 @@ export const Escape: Story = {
play: async ({ canvasElement }) => {
await expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const calendarButton = await canvas.findByRole(
'button',
{ name: 'Select month and year' },
{ timeout: 10000 },
);
await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
await canvas.findByText('January', {}, { timeout: 10000 });
await body.findByText('January', {}, { timeout: 10000 });
await userEvent.keyboard('{escape}');
await userEvent.keyboard('{escape}');
await expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
@@ -174,9 +203,19 @@ export const Enter: Story = {
play: async ({ canvasElement }) => {
await expect(handleEnterMocked).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const calendarButton = await canvas.findByRole(
'button',
{ name: 'Select month and year' },
{ timeout: 10000 },
);
await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
await canvas.findByText('January', {}, { timeout: 10000 });
await body.findByText('January', {}, { timeout: 10000 });
await userEvent.keyboard('{enter}');
await userEvent.keyboard('{enter}');
await expect(handleEnterMocked).toHaveBeenCalledTimes(1);