From c8e46ba0c67137594d46b372e70b88825240c37a Mon Sep 17 00:00:00 2001 From: Miroslav Petrik Date: Fri, 15 Mar 2024 09:32:16 +0100 Subject: [PATCH] fix(Datepicker): expose focus() and clear() methods via ref. Closes #1299 --- src/components/Datepicker/Datepicker.spec.tsx | 32 +++++++- src/components/Datepicker/Datepicker.tsx | 74 +++++++++++++------ 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/components/Datepicker/Datepicker.spec.tsx b/src/components/Datepicker/Datepicker.spec.tsx index 8db97a386..e291eeaea 100644 --- a/src/components/Datepicker/Datepicker.spec.tsx +++ b/src/components/Datepicker/Datepicker.spec.tsx @@ -1,8 +1,10 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, renderHook, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; +import type { DatepickerRef } from './Datepicker'; import { Datepicker } from './Datepicker'; import { getFormattedDate } from './helpers'; import userEvent from '@testing-library/user-event'; +import { useRef } from 'react'; describe('Components / Datepicker', () => { it("should display today's date by default", () => { @@ -74,4 +76,32 @@ describe('Components / Datepicker', () => { await userEvent.click(screen.getByRole('textbox')); await userEvent.click(document.body); }); + + it('should focus the input when ref.current.focus is called', () => { + const { + result: { current: ref }, + } = renderHook(() => useRef(null)); + render(); + + act(() => ref.current?.focus()); + + expect(screen.getByRole('textbox')).toHaveFocus(); + }); + + it('should clear the value when ref.current.clear is called', async () => { + const todaysDateInDefaultLanguage = getFormattedDate('en', new Date()); + const todaysDayOfMonth = new Date().getDate(); + const anotherDay = todaysDayOfMonth === 1 ? 2 : 1; + + const { + result: { current: ref }, + } = renderHook(() => useRef(null)); + render(); + + await userEvent.click(screen.getByRole('textbox')); + await userEvent.click(screen.getAllByText(anotherDay)[0]); + act(() => ref.current?.clear()); + + expect(screen.getByDisplayValue(todaysDateInDefaultLanguage)).toBeInTheDocument(); + }); }); diff --git a/src/components/Datepicker/Datepicker.tsx b/src/components/Datepicker/Datepicker.tsx index a793bf46a..d4b057442 100644 --- a/src/components/Datepicker/Datepicker.tsx +++ b/src/components/Datepicker/Datepicker.tsx @@ -1,7 +1,7 @@ 'use client'; -import type { FC, ReactNode } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import type { ForwardRefRenderFunction, ReactNode } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { HiArrowLeft, HiArrowRight, HiCalendar } from 'react-icons/hi'; import { twMerge } from 'tailwind-merge'; import { mergeDeep } from '../../helpers/merge-deep'; @@ -71,6 +71,17 @@ export interface FlowbiteDatepickerPopupTheme { }; } +export interface DatepickerRef { + /** + * Focus the datepicker input. + */ + focus: () => void; + /** + * Clears the datepicker value back to the defaultDate. + */ + clear: () => void; +} + export interface DatepickerProps extends Omit { open?: boolean; inline?: boolean; @@ -88,25 +99,28 @@ export interface DatepickerProps extends Omit { onSelectedDateChanged?: (date: Date) => void; } -export const Datepicker: FC = ({ - title, - open, - inline = false, - autoHide = true, // Hide when selected the day - showClearButton = true, - labelClearButton = 'Clear', - showTodayButton = true, - labelTodayButton = 'Today', - defaultDate = new Date(), - minDate, - maxDate, - language = 'en', - weekStart = WeekStart.Sunday, - className, - theme: customTheme = {}, - onSelectedDateChanged, - ...props -}) => { +const DatepickerRender: ForwardRefRenderFunction = ( + { + title, + open, + inline = false, + autoHide = true, // Hide when selected the day + showClearButton = true, + labelClearButton = 'Clear', + showTodayButton = true, + labelTodayButton = 'Today', + defaultDate = new Date(), + minDate, + maxDate, + language = 'en', + weekStart = WeekStart.Sunday, + className, + theme: customTheme = {}, + onSelectedDateChanged, + ...props + }, + ref, +) => { const theme = mergeDeep(getTheme().datepicker, customTheme); // Default date should respect the range @@ -135,6 +149,22 @@ export const Datepicker: FC = ({ } }; + const clearDate = () => { + changeSelectedDate(defaultDate, true); + if (defaultDate) { + setViewDate(defaultDate); + } + }; + + useImperativeHandle(ref, () => ({ + focus() { + inputRef.current?.focus(); + }, + clear() { + clearDate(); + }, + })); + // Render the DatepickerView* node const renderView = (type: Views): ReactNode => { switch (type) { @@ -325,4 +355,6 @@ export const Datepicker: FC = ({ ); }; +export const Datepicker = forwardRef(DatepickerRender); + Datepicker.displayName = 'Datepicker';